diff --git a/docs/changelog.md b/docs/changelog.md index b06a46183..22289fca3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,6 +11,7 @@ - Re-introduced tilt correction (from old core) to the scanning probe toolchain. - Improved support for Stanford Research Systems signal generators - Expanded documentation of the microwave interface +- Added ADC from spectrum instrumentation as `FastCounterInterface` hardware. ### Other diff --git a/setup.py b/setup.py index b1210e71d..5875f4548 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,8 @@ 'PySide2', # get fixed version from core 'PyVisa>=1.12.0', 'scipy>=1.9.1', - 'zaber_motion>=2.14.6' + 'zaber_motion>=2.14.6', + 'spcm>=1.2.1' ] windows_dep = [ @@ -33,7 +34,8 @@ 'PySide2', # get fixed version from core 'PyVisa>=1.12.0', 'scipy>=1.9.1', - 'zaber_motion>=2.14.6' + 'zaber_motion>=2.14.6', + 'spcm>=1.2.1' ] with open('VERSION', 'r') as file: diff --git a/src/qudi/gui/time_series/time_series_gui.py b/src/qudi/gui/time_series/time_series_gui.py index 45f93a887..ce1bf074c 100644 --- a/src/qudi/gui/time_series/time_series_gui.py +++ b/src/qudi/gui/time_series/time_series_gui.py @@ -326,7 +326,7 @@ def update_channel_settings(self, enabled, averaged): units=different_units[0]) self._mw.trace_plot_widget.setLabel('right', self._channels_per_axis[1][0], - units=different_units[1]) + rev units=different_units[1]) else: self._mw.trace_plot_widget.setLabel('left', 'Signal', units=different_units[0]) self._mw.trace_plot_widget.setLabel('right', 'Signal', units=different_units[1]) diff --git a/src/qudi/hardware/fast_adc/spectrum/si_commands/buffer_commands.py b/src/qudi/hardware/fast_adc/spectrum/si_commands/buffer_commands.py new file mode 100644 index 000000000..c38c61f4b --- /dev/null +++ b/src/qudi/hardware/fast_adc/spectrum/si_commands/buffer_commands.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +""" +This file contains buffer command classes used for spectrum instrumentation ADC. + +Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" +import pyspcm as spcm +from ctypes import byref + + +class DataBufferCommands: + """ + This class contains the methods which wrap the commands to control the data buffer handling. + Refer to the chapter 'Buffer handling' in the manual for more details. + """ + + def __init__(self, card): + """ + @param str card: The card handle. + """ + self._card = card + + def get_status(self): + status = spcm.int32() + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_M2STATUS, byref(status)) + return status.value + + def get_avail_user_len_B(self): + c_avail_user_len = spcm.c_int64(0) + spcm.spcm_dwGetParam_i64(self._card, spcm.SPC_DATA_AVAIL_USER_LEN, byref(c_avail_user_len)) + return c_avail_user_len.value + + def get_avail_user_pos_B(self): + c_avail_user_pos = spcm.c_int64(0) + spcm.spcm_dwGetParam_i64(self._card, spcm.SPC_DATA_AVAIL_USER_POS, byref(c_avail_user_pos)) + return c_avail_user_pos.value + + def get_avail_card_len_B(self): + c_avail_card_len_B = spcm.c_int64() + spcm.spcm_dwGetParam_i64(self._card, spcm.SPC_DATA_AVAIL_CARD_LEN, byref(c_avail_card_len_B)) + return c_avail_card_len_B.value + + def set_avail_card_len_B(self, avail_card_len_B): + c_avail_card_len_B = spcm.c_int32(avail_card_len_B) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_DATA_AVAIL_CARD_LEN, c_avail_card_len_B) + return + + def get_trig_counter(self): + c_trig_counter = spcm.c_int64() + spcm.spcm_dwGetParam_i64(self._card, spcm.SPC_TRIGGERCOUNTER, byref(c_trig_counter)) + return c_trig_counter.value + + def get_bits_per_sample(self): + c_bits_per_sample = spcm.c_int32(0) + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_MIINST_BITSPERSAMPLE, byref(c_bits_per_sample)) + return c_bits_per_sample.value + +class TsBufferCommands: + """ + This class contains the methods which wrap the commands to control the timestamp buffer handling. + Refer to the chapter 'Timestamps' in the manual for more details. + """ + + def __init__(self, card): + """ + @param str card: The card handle. + """ + self._card = card + + def reset_ts_counter(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TIMESTAMP_CMD, spcm.SPC_TS_RESET) + + def start_extra_dma(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_EXTRA_STARTDMA) + + def wait_extra_dma(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_EXTRA_WAITDMA) + + def stop_extra_dma(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_EXTRA_STOPDMA) + + def poll_extra_dma(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_EXTRA_POLL) + + def get_gate_len_alignment(self): + c_gate_len_alignment = spcm.c_int64(0) + spcm.spcm_dwGetParam_i64(self._card, spcm.SPC_GATE_LEN_ALIGNMENT, byref(c_gate_len_alignment)) + return c_gate_len_alignment.value + + def get_ts_avail_user_len_B(self): + c_ts_avail_user_len = spcm.c_int64(0) + spcm.spcm_dwGetParam_i64(self._card, spcm.SPC_TS_AVAIL_USER_LEN, byref(c_ts_avail_user_len)) + return c_ts_avail_user_len.value + + def get_ts_avail_user_pos_B(self): + c_ts_avail_user_pos = spcm.c_int64(0) + spcm.spcm_dwGetParam_i64(self._card, spcm.SPC_TS_AVAIL_USER_POS, byref(c_ts_avail_user_pos)) + return c_ts_avail_user_pos.value + + def get_ts_avail_card_len_B(self): + c_ts_avail_card_len_B = spcm.c_int64() + spcm.spcm_dwGetParam_i64(self._card, spcm.SPC_TS_AVAIL_CARD_LEN, byref(c_ts_avail_card_len_B)) + return c_ts_avail_card_len_B.value + + def set_ts_avail_card_len_B(self, ts_avail_card_len_B): + c_ts_avail_card_len_B = spcm.c_int64(ts_avail_card_len_B) + spcm.spcm_dwSetParam_i64(self._card, spcm.SPC_TS_AVAIL_CARD_LEN, c_ts_avail_card_len_B) + return + + def get_timestamp_command(self): + c_ts_timestamp_command = spcm.c_int64() + spcm.spcm_dwGetParam_i64(self._card, spcm.SPC_TIMESTAMP_CMD, byref(c_ts_timestamp_command)) + return c_ts_timestamp_command.value diff --git a/src/qudi/hardware/fast_adc/spectrum/si_commands/card_commands.py b/src/qudi/hardware/fast_adc/spectrum/si_commands/card_commands.py new file mode 100644 index 000000000..62c512e32 --- /dev/null +++ b/src/qudi/hardware/fast_adc/spectrum/si_commands/card_commands.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +""" +This file contains card commands classes used for spectrum instrumentation ADC. + +Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" +import pyspcm as spcm + + +class CardCommands: + """ + This class contains the methods which wrap the commands to control the SI card. + Refer to the chapter 'Acquisition modes', the section 'Commands' for more information. + """ + + def __init__(self, card): + """ + @param str card: The card handle. + """ + self._card = card + + def start_all(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_CARD_START | spcm.M2CMD_CARD_ENABLETRIGGER + | spcm.M2CMD_DATA_STARTDMA) + + def start_all_with_extradma(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_CARD_START | spcm.M2CMD_CARD_ENABLETRIGGER + | spcm.M2CMD_DATA_STARTDMA | spcm.M2CMD_EXTRA_STARTDMA) + + def start_all_with_poll(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_CARD_START | spcm.M2CMD_CARD_ENABLETRIGGER + | spcm.M2CMD_DATA_STARTDMA | spcm.M2CMD_EXTRA_POLL) + + def card_start(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_CARD_START) + + def card_stop(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_CARD_STOP) + + def card_reset(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_CARD_RESET) + + def enable_trigger(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_CARD_ENABLETRIGGER) + + def disable_trigger(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_CARD_DISABLETRIGGER) + + def force_trigger(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_CARD_FORCETRIGGER) + + def start_dma(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_DATA_STARTDMA) + + def stop_dma(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_DATA_STOPDMA) + + def wait_dma(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_M2CMD, spcm.M2CMD_DATA_WAITDMA) + diff --git a/src/qudi/hardware/fast_adc/spectrum/si_commands/configure_commands.py b/src/qudi/hardware/fast_adc/spectrum/si_commands/configure_commands.py new file mode 100644 index 000000000..b4eebdc94 --- /dev/null +++ b/src/qudi/hardware/fast_adc/spectrum/si_commands/configure_commands.py @@ -0,0 +1,560 @@ +# -*- coding: utf-8 -*- + +""" +This file contains configure commands classes used for spectrum instrumentation ADC. + +QCopyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" +import ctypes +from functools import reduce +import pyspcm as spcm +from qudi.hardware.fast_adc.spectrum.si_utils.si_settings import CardSettings + + +class ConfigureCommands: + """ + This class instantiates all the classes for configuration and input the card settings. + """ + + def __init__(self, card, log): + """ + @param str card: The card handle. + @param log: qudi logger from the SpectrumInstrumentation. + """ + + self._card = card + self._log = log + + self._c_buf_ptr = ctypes.c_void_p() + self._c_ts_buf_ptr = ctypes.c_void_p() + + self._ai = AnalogInputConfigureCommands(self._card, self._log) + self._acq = AcquisitionConfigureCommands(self._card, self._log) + self._trig = TriggerConfigureCommands(self._card, self._log) + self._buf = DataTransferConfigureCommands(self._card, self._log) + self._ts = TimestampConfigureCommands(self._card, self._log) + + self._csr = ConfigureRegisterChecker(self._card, self._log) + + def configure_all(self, cs: CardSettings): + """ + Collection of all the setting methods. + """ + + self._ai.set_analog_input_conditions(cs.ai_ch) + if 'CH0' in cs.ai_ch: + self._ai.set_ch0(cs.ai0_range_mV, cs.ai0_offset_mV, cs.ai0_term, cs.ai0_coupling) + if 'CH1' in cs.ai_ch: + self._ai.set_ch1(cs.ai1_range_mV, cs.ai1_offset_mV, cs.ai1_term, cs.ai1_coupling) + + self._acq.set_acquisition_mode(cs.acq_mode, cs.acq_pre_trigs_S, cs.acq_post_trigs_S, + cs.acq_mem_size_S, cs.acq_seg_size_S, + cs.acq_loops, cs.acq_HW_avg_num) + self._acq.set_sampling_clock(cs.clk_ref_Hz, cs.clk_samplerate_Hz) + self._trig.set_trigger(cs.trig_mode, cs.trig_level_mV) + self._buf.invalidate_buffer(spcm.SPCM_BUF_DATA) + self._c_buf_ptr = self._buf.configure_data_transfer(spcm.SPCM_BUF_DATA, self._c_buf_ptr, + cs.buf_size_B, cs.buf_notify_size_B) + if cs.gated: + self._buf.invalidate_buffer(spcm.SPCM_BUF_TIMESTAMP) + self._c_ts_buf_ptr = self._buf.configure_data_transfer(spcm.SPCM_BUF_TIMESTAMP, self._c_ts_buf_ptr, + cs.ts_buf_size_B, cs.ts_buf_notify_size_B) + self._ts.configure_ts_standard() + + def return_c_buf_ptr(self): + return self._c_buf_ptr + + def return_c_ts_buf_ptr(self): + return self._c_ts_buf_ptr + + +class AnalogInputConfigureCommands: + """ + This class configures the analog input. + Refer to the chapter 'Analog Inputs' in the manual for more information. + """ + ai_ch_dict = {'CH0': spcm.CHANNEL0, 'CH1': spcm.CHANNEL1} + ai_term_dict = {'1MOhm': 0, '50Ohm': 1} + ai_coupling_dict = {'DC': 0, 'AC': 1} + + def __init__(self, card, log): + """ + @param str card: The card handle. + @param log: qudi logger from the SpectrumInstrumentation. + """ + self._card = card + self._log = log + + def set_analog_input_conditions(self, ai_ch): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TIMEOUT, 5000) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_CHENABLE, self._get_ch_bitmap(ai_ch)) + return + + def _get_ch_bitmap(self, ai_ch): + if len(ai_ch) == 1: + enabled_ch_bitmap = self.ai_ch_dict[ai_ch[0]] + else: + enabled_ch_bitmap = reduce(lambda x, y: self.ai_ch_dict[x]|self.ai_ch_dict[y], ai_ch) + return enabled_ch_bitmap + + def set_ch0(self, ai_range_mV, ai_offset_mV, ai_term, ai_coupling): + """ + @param int ai_range_mV: Analog input range up to 10 V. + @param int ai_offset_mV: Analog input offset. + @param str ai_term: Analog input termination. '1MOhm' or '50Ohm' + @param str ai_coupling: Analog input coupling. 'DC' or 'AC' + """ + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_AMP0, ai_range_mV) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_OFFS0, ai_offset_mV) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_50OHM0, self.ai_term_dict[ai_term]) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_ACDC0, self.ai_coupling_dict[ai_coupling]) + + def set_ch1(self, ai_range_mV, ai_offset_mV, ai_term, ai_coupling): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_AMP1, ai_range_mV) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_OFFS1, ai_offset_mV) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_50OHM1, self.ai_term_dict[ai_term]) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_ACDC1, self.ai_coupling_dict[ai_coupling]) + +class AcquisitionConfigureCommands: + """ + This class configures the acquisition mode of the card. + Refer to the chapter 'Acquisition modes' in the manual for more information. + """ + def __init__(self, card, log): + """ + @param str card: The card handle. + @param log: qudi logger from the SpectrumInstrumentation. + """ + self._card = card + self._log = log + + def set_acquisition_mode(self, mode, pre_trigs_S, post_trigs_S, mem_size_S, seg_size_S, loops, HW_avg_num): + if 'STD' in mode: + if 'GATE' in mode: + self._set_STD_gate_mode(mode, pre_trigs_S,post_trigs_S, mem_size_S) + else: + self._set_STD_trigger_mode(mode, post_trigs_S, seg_size_S, mem_size_S) + + elif 'FIFO' in mode: + if 'GATE' in mode: + self._set_FIFO_gate_mode(mode, pre_trigs_S, post_trigs_S, loops) + else: + self._set_FIFO_trigger_mode(mode, pre_trigs_S, post_trigs_S, seg_size_S, loops, HW_avg_num) + + else: + raise ValueError('The acquisition mode is not proper') + + def set_sampling_clock(self, clk_ref_Hz, clk_samplerate_Hz): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_CLOCKMODE, spcm.SPC_CM_INTPLL) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_REFERENCECLOCK, clk_ref_Hz) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_SAMPLERATE, clk_samplerate_Hz) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_CLOCKOUT, 1) + return + + def _set_STD_trigger_mode(self, acq_mode, post_trigs_S, seg_size_S, mem_size_S): + if acq_mode == 'STD_SINGLE': + mem_size_S = post_trigs_S + self._mode_STD_SINGLE(post_trigs_S, mem_size_S) + + elif acq_mode == 'STD_MULTI': + self._mode_STD_MULTI(post_trigs_S, seg_size_S, mem_size_S) + + else: + raise ValueError('The used acquistion mode is not defined') + + def _set_STD_gate_mode(self, acq_mode, pre_trigs_S, post_trigs_S, mem_size_S): + if acq_mode == 'STD_GATE': + self._mode_STD_GATE(pre_trigs_S, post_trigs_S, mem_size_S) + + else: + raise ValueError('The used acquistion mode is not defined') + + def _set_FIFO_trigger_mode(self, acq_mode, pre_trigs_S, post_trigs_S, seg_size_S, loops, HW_avg_num=0): + if acq_mode == 'FIFO_SINGLE': + self._mode_FIFO_SINGLE(pre_trigs_S, seg_size_S, loops) + + elif acq_mode == 'FIFO_MULTI': + self._mode_FIFO_MULTI(post_trigs_S, seg_size_S, loops) + + elif acq_mode == 'FIFO_AVERAGE': + self._mode_FIFO_AVERAGE(post_trigs_S, seg_size_S, loops, HW_avg_num) + + else: + raise ValueError('The used acquistion mode is not defined') + + def _set_FIFO_gate_mode(self, acq_mode, pre_trigs_S, post_trigs_S, loops): + + if acq_mode == 'FIFO_GATE': + self._mode_FIFO_GATE(pre_trigs_S, post_trigs_S, loops) + + else: + raise ValueError('The used acquistion mode is not defined') + + def _mode_STD_SINGLE(self, post_trigs_S, memsize_S): + """ + In this mode, pre trigger = memsize - post trigger. + @params int post_trig_S: the number of samples to be recorded after the trigger event has been detected. + @params int memsize_S: the total number of samples to be recorded + """ + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_CARDMODE, spcm.SPC_REC_STD_SINGLE) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_MEMSIZE, memsize_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_POSTTRIGGER, post_trigs_S) + return + + def _mode_STD_MULTI(self, post_trigs_S, seg_size_S, mem_size_S): + """ + SEGMENTSIZE is the numbe of samples recorded after detection of one trigger + including the pre trigger. + MEMSIZE defines the total number of samples to be recorded per channel. + + @params int post_trig_S: + @params int seg_size_S: + @params int reps: The number of repetitions. + """ + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_CARDMODE, spcm.SPC_REC_STD_MULTI) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_SEGMENTSIZE, seg_size_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_MEMSIZE, mem_size_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_POSTTRIGGER, post_trigs_S) + return + + def _mode_STD_GATE(self, pre_trigs_S, post_trigs_S, mem_size_S): + """ + @params int pre_trigs_S: the number of samples to be recorded prior to the gate start + @params int post_trigs_S: the number of samples to be recorded after the gate end + @params int mem_size_S: the total number of samples to be recorded + """ + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_CARDMODE, spcm.SPC_REC_STD_GATE) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_PRETRIGGER, pre_trigs_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_POSTTRIGGER, post_trigs_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_MEMSIZE, mem_size_S) + return + + def _mode_FIFO_SINGLE(self, pre_trigs_S, seg_size_S, loops=1): + """ + SEGMENTSIZE is the numbe of samples recorded after detection of one trigger + including the pre trigger. + + @params int pre_trigs_S: the number of samples to be recorded prior to the gate start + @params int seg_size_S: the numbe of samples recorded after detection of one trigger + including the pre trigger. + @params int loops: the total number of loops + """ + + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_CARDMODE, spcm.SPC_REC_FIFO_SINGLE) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_PRETRIGGER, pre_trigs_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_SEGMENTSIZE, seg_size_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_LOOPS, loops) + return + + def _mode_FIFO_MULTI(self, post_trigs_S, seg_size_S, loops=0): + """ + SEGMENTSIZE is the numbe of samples recorded after detection of one trigger + including the pre trigger. + + @params int pre_trigs_S: the number of samples to be recorded after the gate start + @params int seg_size_S: the numbe of samples recorded after detection of one trigger + including the pre trigger. + @params int loops: the total number of loops + """ + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_CARDMODE, spcm.SPC_REC_FIFO_MULTI) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_POSTTRIGGER, post_trigs_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_SEGMENTSIZE, seg_size_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_LOOPS, loops) + return + + def _mode_FIFO_AVERAGE(self, post_trigs_S, seg_size_S, loops, HW_avg_num): + max_post_trigs_S = 127984 + + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_CARDMODE, spcm.SPC_REC_FIFO_AVERAGE) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_AVERAGES, HW_avg_num) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_POSTTRIGGER, post_trigs_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_SEGMENTSIZE, seg_size_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_LOOPS, loops) + return + + def _mode_FIFO_GATE(self, pre_trigs_S, post_trigs_S, loops): + """ + @params int pre_trigs_S: the number of samples to be recorded prior to the gate start + @params int post_trigs_S: the number of samples to be recorded after the gate end + @params int loops: the total number of loops + """ + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_CARDMODE, spcm.SPC_REC_FIFO_GATE) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_PRETRIGGER, pre_trigs_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_POSTTRIGGER, post_trigs_S) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_LOOPS, loops) + return + + +class TriggerConfigureCommands: + """' + This class configures the trigger modes and the input parameters accordingly. + Refer to the chapter 'Trigger modes and appendant registers' for more information. + """ + def __init__(self, card, log): + """ + @param str card: The card handle. + @param log: qudi logger from the SpectrumInstrumentation. + """ + self._card = card + self._log = log + + def set_trigger(self, trig_mode, trig_level_mV): + """ + set the trigger settings. + @param str card: the handle of the card + @param str trig_mode: trigger mode + @param int trig_level_mV: the voltage level for triggering in mV + """ + if trig_mode == 'EXT': + self._trigger_EXT(trig_level_mV) + + elif trig_mode == 'SW': + self._trigger_SW() + + elif trig_mode == 'CH0': + self._trigger_CH0(trig_level_mV) + + else: + self._log.error('wrong trigger mode') + + def _trigger_EXT(self, trig_level_mV): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_TERM, 0) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_EXT0_ACDC, 0) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_EXT0_MODE, spcm.SPC_TM_POS) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_EXT0_LEVEL0, trig_level_mV) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_ORMASK, spcm.SPC_TMASK_EXT0) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_ANDMASK, 0) + + def _trigger_SW(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_ORMASK, spcm.SPC_TMASK_SOFTWARE) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_ANDMASK, 0) + + def _trigger_CH0(self, trig_level_mV): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_ORMASK, spcm.SPC_TMASK_NONE) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_CH_ANDMASK0, spcm.SPC_TMASK0_CH0) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_CH0_LEVEL0, trig_level_mV) + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TRIG_CH0_MODE, spcm.SPC_TM_POS) + + +class DataTransferConfigureCommands: + """ + This class configures the transfer buffer dependent on the buffer type specified in the argument. + Refer to the chapter 'Commands', the section 'Data transfer' for more information. + """ + def __init__(self, card, log): + """ + @param str card: The card handle. + @param log: qudi logger from the SpectrumInstrumentation. + """ + self._card = card + self._log = log + + def configure_data_transfer(self, buf_type, c_buf_ptr, buf_size_B, buf_notify_size_B): + """ + Configure the data transfer buffer + + @param str buf_type: register of data or timestamp buffer + @param c_buf_ptr: ctypes pointer for the buffer + @param int buf_size_B: length of the buffer size in bytes + @param int buf_notify_size_B: length of the notify size of the buffer in bytes + + @return c_buf_ptr: + """ + c_buf_ptr = self._set_buffer(c_buf_ptr, buf_size_B) + self._set_data_transfer(buf_type, c_buf_ptr, buf_size_B, buf_notify_size_B) + return c_buf_ptr + + def _set_buffer(self, c_buf_ptr, buf_size_B): + """ + Set the continuous buffer if possible. See the documentation for the details. + """ + cont_buf_len = self._get_cont_buf_len(c_buf_ptr) + if cont_buf_len > buf_size_B: + self._log.info('using continuous buffer') + else: + c_buf_ptr = self._pvAllocMemPageAligned(buf_size_B) + self._log.info('using scatter gather') + return c_buf_ptr + + def _get_cont_buf_len(self, c_buf_ptr): + """ + Get length of the continuous buffer set in the card. + Check also the Spectrum Control Center. + + @param c_buf_ptr: ctypes pointer for the buffer + + @return int: length of the available continuous buffer + """ + c_cont_buf_len = spcm.uint64(0) + spcm.spcm_dwGetContBuf_i64(self._card, + spcm.SPCM_BUF_DATA, + ctypes.byref(c_buf_ptr), + ctypes.byref(c_cont_buf_len)) + return c_cont_buf_len.value + + def _pvAllocMemPageAligned(self, qwBytes): + """ + Taken from the example + """ + dwAlignment = 4096 + dwMask = dwAlignment - 1 + + # allocate non-aligned, slightly larger buffer + qwRequiredNonAlignedBytes = qwBytes * ctypes.sizeof(ctypes.c_char) + dwMask + pvNonAlignedBuf = (ctypes.c_char * qwRequiredNonAlignedBytes)() + + # get offset of next aligned address in non-aligned buffer + misalignment = ctypes.addressof(pvNonAlignedBuf) & dwMask + if misalignment: + dwOffset = dwAlignment - misalignment + else: + dwOffset = 0 + buffer = (ctypes.c_char * qwBytes).from_buffer(pvNonAlignedBuf, dwOffset) + return buffer + + def _set_data_transfer(self, buf_type, c_buf_ptr, buf_size_B, buf_notify_size_B): + """ + set the data transfer buffer + + @param str buf_type: register of data or timestamp buffer + @param c_buf_ptr: ctypes pointer for the buffer + @param int buf_size_B: length of the buffer size in bytes + @param int buf_notify_size_B: length of the notify size of the buffer in bytes + """ + + c_buf_offset = spcm.uint64(0) + c_buf_size_B = spcm.uint64(buf_size_B) + spcm.spcm_dwDefTransfer_i64(self._card, + buf_type, + spcm.SPCM_DIR_CARDTOPC, + buf_notify_size_B, + ctypes.byref(c_buf_ptr), + c_buf_offset, + c_buf_size_B + ) + return + + def invalidate_buffer(self, buf_type): + try: + spcm.spcm_dwInvalidateBuf(self._card, buf_type) + except: + pass + + +class TimestampConfigureCommands: + """ + This class configures the timestamp mode. + Refer to the chapter 'Timestamps', the section 'Timestamps mode' for more information. + """ + def __init__(self, card, log): + """ + @param str card: The card handle. + @param log: qudi logger from the SpectrumInstrumentation. + """ + self._card = card + self._log = log + + def configure_ts_standard(self): + self.ts_standard_mode() + + def ts_standard_mode(self): + spcm.spcm_dwSetParam_i32(self._card, + spcm.SPC_TIMESTAMP_CMD, + spcm.SPC_TSMODE_STANDARD | spcm.SPC_TSCNT_INTERNAL | spcm.SPC_TSFEAT_NONE) + + def ts_internal_clock(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TIMESTAMP_CMD, spcm.SPC_TSCNT_INTERNAL) + + def ts_no_additional_timestamp(self): + spcm.spcm_dwSetParam_i32(self._card, spcm.SPC_TIMESTAMP_CMD, spcm.SPC_TSFEAT_NONE) + + +class ConfigureRegisterChecker: + """ + This class can be used to check if the card settings are correctly input. + The registers can be obtained from the card to the CardSettings (csr). + """ + def __init__(self, card, log): + self._card = card + self._log = log + + self.csr = None + + def check_cs_registers(self): + """ + Use this method to fetch the card settings stored in the card registers + and check them all with csr. + """ + + self.csr = CardSettings() + self._check_csr_ai() + self._check_csr_acq() + self._check_csr_clk() + self._check_csr_trig() + + def _check_csr_ai(self): + ai_term_dict = {0:'1Mohm', 1:'50Ohm'} + ai_coupling_dict = {0:'DC', 1:'AC'} + + c_ai_range_mV = spcm.c_int32() + c_ai_offset_mV = spcm.c_int32() + c_ai_term = spcm.c_int32() + c_ai_coupling = spcm.c_int32() + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_AMP0, ctypes.byref(c_ai_range_mV)) + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_OFFS0, ctypes.byref(c_ai_offset_mV)) + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_50OHM0, ctypes.byref(c_ai_term)) + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_ACDC0, ctypes.byref(c_ai_coupling)) + self.csr.ai_range_mV = int(c_ai_range_mV.value) + self.csr.ai_offset_mV = int(c_ai_offset_mV.value) + self.csr.ai_term = ai_term_dict[c_ai_term.value] + self.csr.ai_coupling = ai_coupling_dict[c_ai_coupling.value] + + def _check_csr_acq(self): + c_acq_mode = spcm.c_int32() + c_acq_HW_avg_num = spcm.c_int32() + c_acq_pre_trigs_S = spcm.c_int32() + c_acq_post_trigs_S = spcm.c_int32() + c_acq_mem_size_S = spcm.c_int32() + c_acq_seg_size_S = spcm.c_int32() + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_CARDMODE, ctypes.byref(c_acq_mode)) + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_AVERAGES, ctypes.byref(c_acq_HW_avg_num)) + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_PRETRIGGER, ctypes.byref(c_acq_pre_trigs_S)) + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_POSTTRIGGER, ctypes.byref(c_acq_post_trigs_S)) + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_MEMSIZE, ctypes.byref(c_acq_mem_size_S)) + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_SEGMENTSIZE, ctypes.byref(c_acq_seg_size_S)) + self.csr.acq_mode = c_acq_mode.value + self.csr.acq_HW_avg_num = int(c_acq_HW_avg_num.value) + self.csr.acq_pre_trigs_S = int(c_acq_pre_trigs_S.value) + self.csr.acq_post_trigs_S = int(c_acq_post_trigs_S.value) + self.csr.acq_mem_size_S = int(c_acq_mem_size_S.value) + self.csr.acq_seg_size_S = int(c_acq_seg_size_S.value) + + def _check_csr_clk(self): + c_clk_samplerate_Hz = spcm.c_int32() + c_clk_ref_Hz = spcm.c_int32() + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_REFERENCECLOCK, ctypes.byref(c_clk_ref_Hz)) + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_SAMPLERATE, ctypes.byref(c_clk_samplerate_Hz)) + self.csr.clk_samplerate_Hz = int(c_clk_samplerate_Hz.value) + self.csr.clk_ref_Hz = int(c_clk_ref_Hz.value) + + def _check_csr_trig(self): + c_trig_mode = spcm.c_int32() + c_trig_level_mV = spcm.c_int32() + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_TRIG_EXT0_MODE, ctypes.byref(c_trig_mode)) + spcm.spcm_dwGetParam_i32(self._card, spcm.SPC_TRIG_EXT0_LEVEL0, ctypes.byref(c_trig_level_mV)) + self.csr.trig_mode = c_trig_mode.value + self.csr.trig_level_mV = int(c_trig_level_mV.value) diff --git a/src/qudi/hardware/fast_adc/spectrum/si_commands/si_commands.py b/src/qudi/hardware/fast_adc/spectrum/si_commands/si_commands.py new file mode 100644 index 000000000..52916918b --- /dev/null +++ b/src/qudi/hardware/fast_adc/spectrum/si_commands/si_commands.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +""" +This file contains commands classes used for spectrum instrumentation ADC. + +Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" +import time +import numpy as np + +from qudi.hardware.fast_adc.spectrum.si_commands.card_commands import CardCommands +from qudi.hardware.fast_adc.spectrum.si_commands.buffer_commands \ + import DataBufferCommands, TsBufferCommands +from qudi.hardware.fast_adc.spectrum.si_commands.configure_commands import ConfigureCommands + +from qudi.hardware.fast_adc.spectrum.si_utils.si_settings import MeasurementSettings + + +class Commands: + """ + This class has hardware commands and actions used in the data process. + The commands class do not possess any measurement information. + The action class has measurement information and interprets the information from the commands based on that. + """ + def __init__(self, card, log): + """ + @param str card: The card handle. + @param log: qudi logger from the SpectrumInstrumentation. + """ + self.card = CardCommands(card) + self.data_buf = DataBufferCommands(card) + self.ts_buf = TsBufferCommands(card) + self.cfg = ConfigureCommands(card, log) + self.process = ProcessAction(self) + + +class ProcessAction: + """ + This class has actions used in the data process. They modify the commands to be useful in the measurement. + """ + def __init__(self, commands:Commands): + self.cmd = commands + + self.wait_time_interval = 0.01 + + self.trigger_enabled = False + self._seq_size_B = 0 + self._total_gate = 0 + self._ts_seq_size_B = 0 + + def input_ms(self, ms: MeasurementSettings): + self._seq_size_B = ms.seq_size_B + self._assign_methods(ms.gated) + self._total_gate = ms.total_gate + if ms.gated: + self._ts_seq_size_B = ms.ts_seq_size_B + + def _assign_methods(self, gated): + if gated: + self.get_trig_reps = self._get_trig_reps_gated + self.get_curr_avail_reps = self._get_curr_avail_reps_gated + else: + self.get_trig_reps = self._get_trig_reps_ungated + self.get_curr_avail_reps = self._get_curr_avail_reps_ungated + + + def get_avail_user_reps(self): + return int(np.floor(self.cmd.data_buf.get_avail_user_len_B() / self._seq_size_B)) + + def get_ts_avail_user_reps(self): + return int(self.cmd.ts_buf.get_ts_avail_user_len_B() / self._ts_seq_size_B) + + def toggle_trigger(self, trigger_on): + if trigger_on == self.trigger_enabled: + return + else: + if trigger_on: + self.trigger_enabled = self.cmd.card.enable_trigger() + else: + self.trigger_enabled = self.cmd.card.disable_trigger() + + def wait_new_trig_reps(self, prev_trig_reps): + curr_trig_reps = self.get_trig_reps() + while curr_trig_reps == prev_trig_reps: + curr_trig_reps = self.get_trig_reps() + time.sleep(self.wait_time_interval) + return curr_trig_reps + + def _get_trig_reps_ungated(self): + return int(self.cmd.data_buf.get_trig_counter()) + + def _get_trig_reps_gated(self): + return int(self.cmd.data_buf.get_trig_counter() / self._total_gate) + + def _get_curr_avail_reps_ungated(self): + return self.cmd.data_buf.get_avail_user_reps() + + def _get_curr_avail_reps_gated(self): + curr_data_avail_reps = self.cmd.process.get_avail_user_reps() + curr_ts_avail_reps = self.cmd.process.get_ts_avail_user_reps() + return min(curr_data_avail_reps, curr_ts_avail_reps) + + def wait_avail_data(self): + initial_time = time.time() + curr_avail_reps = self.get_curr_avail_reps() + while curr_avail_reps == 0: + curr_avail_reps = self.get_curr_avail_reps() + current_time = time.time() - initial_time + if current_time > 10: + time_out = True + return time_out + time.sleep(self.wait_time_interval) + time_out = False + return time_out diff --git a/src/qudi/hardware/fast_adc/spectrum/si_main.py b/src/qudi/hardware/fast_adc/spectrum/si_main.py new file mode 100644 index 000000000..38d219359 --- /dev/null +++ b/src/qudi/hardware/fast_adc/spectrum/si_main.py @@ -0,0 +1,418 @@ +# -*- coding: utf-8 -*- +""" +This file contains the Qudi hardware for spectrum instrumentation ADC. + +Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" +import time +import numpy as np +from enum import IntEnum +import pyspcm as spcm +import spcm_tools +from PySide2.QtCore import Signal + +from qudi.core.configoption import ConfigOption +from qudi.util.mutex import Mutex +from qudi.interface.fast_counter_interface import FastCounterInterface + +from qudi.hardware.fast_adc.spectrum.si_utils.si_settings import CardSettings, MeasurementSettings +from qudi.hardware.fast_adc.spectrum.si_commands.si_commands import Commands +from qudi.hardware.fast_adc.spectrum.si_utils.si_dataclass import Data +from qudi.hardware.fast_adc.spectrum.si_utils.si_data_fetcher import DataFetcher +from qudi.hardware.fast_adc.spectrum.si_utils.si_data_processer import DataProcessGated, DataProcessorUngated +from qudi.hardware.fast_adc.spectrum.si_utils.si_commander import Commander +from qudi.hardware.fast_adc.spectrum.si_utils.si_loop_manager import LoopManager + + +class CardStatus(IntEnum): + unconfigured = 0 + idle = 1 + running = 2 + paused = 3 + error = -1 + + +class SpectrumInstrumentation(FastCounterInterface): + + ''' + Hardware class for the spectrum instrumentation card. + pyspcm.py and spcm_tools.py from the spectrum instrumentation need to be stored in your library. + + Implemented mode: + - trigger_mode: + 'EXT' (External trigger), + 'SW' (Software trigger), + 'CH0' (Channel0 trigger) + - acquisition_mode: + 'STD_SINGLE' + 'STD_MULTI' + 'STD_GATE' + 'FIFO_SINGLE', + 'FIFO_GATE', + 'FIFO_MULTI', + 'FIFO_AVERAGE' + + Config example: + si: + module.Class: 'fast_adc.spectrum.si_main.SpectrumInstrumentation' + options: + ai_ch: + - 'CH0' + ai0_range_mV: 2000 + ai0_offset_mV: 0 + ai0_termination: '1MOhm' + ai0_coupling: 'DC' + ai1_range_mV: 2000 + ai1_offset_mV: 0 + ai1_termination: '1MOhm' + ai1_coupling: 'DC' + acq_mode: 'FIFO_GATE' + acq_HW_avg_num: 1 + acq_pre_trigger_samples: 16 + acq_post_trigger_samples: 16 + buf_notify_size_B: 4096 + clk_reference_Hz: 10e6 + trig_mode: 'EXT' + trig_level_mV: 1000 + initial_buffer_size_S: 1e9 + max_reps_per_buf: 1e4 + repetitions: 0 + double_gate_acquisition: False + data_stack_on: True + + Basic class strutcture: + SpectrumInstrumentation + ├ cs (CardSettings) + ├ ms (MeasurementSettings) + ├ cmd (Commands) + │ ├ card (CardCommands) + │ ├ data_buf (DataBufferCommands) + │ ├ ts_buf (TsBufferCommands) + │ ├ cfg (ConfigureCommands) + │ └ process (ProcessActions) + ├ data(Data) + ├ fetcher (DataFetcher) + ├ commander (Commander) + └ loop_manager(LoopManager) + ''' + + _modtype = 'SpectrumCard' + _modclass = 'hardware' + _threaded = True + _threadlock = Mutex() + _signal_measure = Signal() + + # ConfigOptions for card settings. See the manual for details. + _ai_ch = ConfigOption('ai_ch', 'CH0', missing='nothing') + _ai0_range_mV = ConfigOption('ai0_range_mV', 1000, missing='warn') + _ai0_offset_mV = ConfigOption('ai0_offset_mV', 0, missing='warn') + _ai0_term = ConfigOption('ai0_termination', '50Ohm', missing='warn') + _ai0_coupling = ConfigOption('ai0_coupling', 'DC', missing='warn') + _ai1_range_mV = ConfigOption('ai1_range_mV', 1000, missing='warn') + _ai1_offset_mV = ConfigOption('ai1_offset_mV', 0, missing='warn') + _ai1_term = ConfigOption('ai1_termination', '50Ohm', missing='warn') + _ai1_coupling = ConfigOption('ai1_coupling', 'DC', missing='warn') + _acq_mode = ConfigOption('acq_mode', 'FIFO_MULTI', missing='warn') + _acq_HW_avg_num = ConfigOption('acq_HW_avg_num', 1, missing='nothing') + _acq_pre_trigs_S = ConfigOption('acq_pre_trigger_samples', 16, missing='warn') + _acq_post_trigs_S = ConfigOption('acq_post_trigger_samples', 16, missing='nothing') + _buf_notify_size_B = ConfigOption('buf_notify_size_B', 4096, missing='warn') + _clk_ref_Hz = ConfigOption('clk_reference_Hz', 10e6, missing='warn') + _trig_mode = ConfigOption('trig_mode', 'EXT', missing='warn') + _trig_level_mV = ConfigOption('trig_level_mV', '1000', missing='warn') + + #ConfigOptions for measurement settings. + _init_buf_size_S = ConfigOption('initial_buffer_size_S', 1e9, missing='warn') + _max_reps_per_buf = ConfigOption('max_reps_per_buf', 1e4, missing='nothing') + _reps = ConfigOption('repetitions', 0, missing='nothing') + _double_gate_acquisition = ConfigOption('double_gate_acquisition', False, missing='nothing') + _data_stack_on = ConfigOption('data_stack_on', True, missing='nothing') + + _wait_time_interval = ConfigOption('wait_time_interval', 0.01, missing='nothing') + + def __init__(self, config, **kwargs): + super().__init__(config=config, **kwargs) + self.card = None + self._card_on = False + self._internal_status = CardStatus.idle + + self.cs = None + self.ms = None + self.cmd = None + self.fetcher = None + self.data = None + self.commander = None + self.loop_manager = None + + def on_activate(self): + """ + Open the card by activation of the module + """ + + if not self._card_on: + self.card = spcm.spcm_hOpen(spcm_tools.create_string_buffer(b'/dev/spcm0')) + self._card_on = True + self._generate_helpers() + self._load_settings_from_config_file() + + else: + self.log.info('SI card is already on') + + if self.card is None: + self.log.info('No card found') + + def _generate_helpers(self): + self.cs = CardSettings() + self.ms = MeasurementSettings() + self.cmd = Commands(self.card, self.log) + self.data = Data() + self.fetcher = DataFetcher() + self.commander = Commander(self.cmd, self.log) + self.loop_manager = LoopManager(self.commander, self.log) + self._signal_measure.connect(self.loop_manager.start_data_process) + + def _load_settings_from_config_file(self): + """ + Load the settings parameters of the configuration file to the card and measurement settings. + """ + self.cs.ai_ch = self._ai_ch + self.cs.ai0_range_mV = int(self._ai0_range_mV) + self.cs.ai0_offset_mV = int(self._ai0_offset_mV) + self.cs.ai0_term = self._ai0_term + self.cs.ai0_coupling = self._ai0_coupling + self.cs.ai1_range_mV = int(self._ai1_range_mV) + self.cs.ai1_offset_mV = int(self._ai1_offset_mV) + self.cs.ai1_term = self._ai1_term + self.cs.ai1_coupling = self._ai1_coupling + self.cs.acq_mode = self._acq_mode + self.cs.acq_HW_avg_num = int(self._acq_HW_avg_num) + self.cs.acq_pre_trigs_S = int(self._acq_pre_trigs_S) + self.cs.acq_post_trigs_S = int(self._acq_post_trigs_S) + self.cs.buf_notify_size_B = int(self._buf_notify_size_B) + self.cs.clk_ref_Hz = int(self._clk_ref_Hz) + self.cs.trig_mode = self._trig_mode + self.cs.trig_level_mV = int(self._trig_level_mV) + + self.ms.init_buf_size_S = int(self._init_buf_size_S) + self.ms.max_reps_per_buf = int(self._max_reps_per_buf) + self.ms.reps = self._reps + self.ms.data_stack_on = self._data_stack_on + self.ms.double_gate_acquisition = self._double_gate_acquisition + self.ms.assign_data_bit(self.cs.acq_mode) + + self.cmd.process.wait_time_interval = self._wait_time_interval + + def on_deactivate(self): + """ + Close the card. + """ + spcm.spcm_vClose(self.card) + + def get_constraints(self): + + constraints = dict() + + constraints['possible_timebase_list'] = np.array([2**i for i in range(18)]) + constraints['hardware_binwidth_list'] = (constraints['possible_timebase_list']) / 250e6 #maximum sampling rate 250 MHz + + return constraints + + def configure(self, binwidth_s, record_length_s, number_of_gates=1): + """ + Configure the card parameters. + @param float binwidth_s: Length of a single time bin in the time trace + histogram in seconds. + @param float record_length_s: Total length of the timetrace/each single + gate in seconds. + @param int number_of_gates: optional, number of gates in the pulse + sequence. Ignore for not gated counter. + + @return tuple(binwidth_s, gate_length_s, number_of_gates): + binwidth_s: float the actual set binwidth in seconds + gate_length_s: the actual set gate length in seconds + number_of_gates: the number of gated, which are accepted + """ + def set_dynamic_params(binwidth_s, record_length_s, number_of_gates): + self.ms.load_dynamic_params(binwidth_s, record_length_s, number_of_gates) + self.ms.calc_data_size_S(self.cs.acq_pre_trigs_S, self.cs.acq_post_trigs_S) + self.cs.calc_dynamic_cs(self.ms) + self.ms.calc_buf_params() + self.ms.calc_actual_length_s() + self.cs.get_buf_size_B(self.ms.seq_size_B, self.ms.reps_per_buf, self.ms.total_pulse) + + def get_buffer_pointer(): + self.ms.c_buf_ptr = self.cmd.cfg.return_c_buf_ptr() + if self.ms.gated: + self.ms.c_ts_buf_ptr = self.cmd.cfg.return_c_ts_buf_ptr() + + def initialize_all(): + self.cmd.process.input_ms(self.ms) + self.fetcher.init_buffer(self.ms) + self.data.initialize(self.ms, self.cs) + self.processor = DataProcessGated(self.data, self.fetcher) if self.ms.gated \ + else DataProcessorUngated(self.data, self.fetcher) + self.commander.init(self.ms, self.processor) + self.loop_manager.input_ms(self.ms) + + self.cmd.card.card_reset() + self.ms.gated = self.cs.gated + self.ms.num_channels = len(self.cs.ai_ch) + + set_dynamic_params(binwidth_s, record_length_s, number_of_gates) + + self.cmd.cfg.configure_all(self.cs) + get_buffer_pointer() + + initialize_all() + + return self.ms.binwidth_s, self.ms.record_length_s, self.ms.total_pulse + + def get_status(self): + """ + Receives the current status of the Fast Counter and outputs it as + return value. + + 0 = unconfigured + 1 = idle + 2 = running + 3 = paused + -1 = error state + TODO Use error state when an error is caught. + """ + return self._internal_status + + def start_measure(self): + """ + Start the acquisition and data process loop + """ + self._start_acquisition() + self._signal_measure.emit() + self.loop_manager.start() + + return 0 + + def _start_acquisition(self): + """ + Start the data acquisition + """ + self.log.info('Measurement started') + if self._internal_status == CardStatus.idle: +# self.loop_manager.init_measure_params() + if self.ms.gated: + self.cmd.ts_buf.reset_ts_counter() + self.cmd.card.start_all_with_extradma() + else: + self.cmd.card.start_all() + self.log.debug('card started') + + elif self._internal_status == CardStatus.paused: + self.cmd.card.enable_trigger() + + self.cmd.process.trigger_enabled = True + self._internal_status = CardStatus.running + + def get_data_trace(self): + """ Polls the current timetrace data from the fast counter. + + Return value is a numpy array (dtype = int64). + The binning, specified by calling configure() in forehand, must be + taken care of in this hardware class. A possible overflow of the + histogram bins must be caught here and taken care of. + If the counter is NOT GATED it will return a tuple (1D-numpy-array, info_dict) with + returnarray[timebin_index] + If the counter is GATED it will return a tuple (2D-numpy-array, info_dict) with + returnarray[gate_index, timebin_index] + + info_dict is a dictionary with keys : + - 'elapsed_sweeps' : the elapsed number of sweeps + - 'elapsed_time' : the elapsed time in seconds + + If the hardware does not support these features, the values should be None + """ + with self._threadlock: + avg_data = self.data.avg.pulse_array[0:self.data.avg.num_pulse, :] if self.ms.gated else self.data.avg.data + #tentative workaround: + avg_num = self.data.avg.num_rep + info_dict = {'elapsed_sweeps': avg_num, 'elapsed_time': time.time() - self.loop_manager.start_time} + + return avg_data, info_dict + + def stop_measure(self): + """ + Stop the measurement. + """ + if self._internal_status == CardStatus.running: + self.loop_manager.stop_data_process() + self.loop_manager.wait() + self.log.debug('Measurement thread stopped') + + self.cmd.card.disable_trigger() + self.cmd.process.trigger_enabled = False + self.cmd.card.stop_dma() + self.cmd.card.card_stop() + self.log.debug('card stopped') + self._internal_status = CardStatus.idle + self.log.info('Measurement stopped') + return 0 + + elif self._internal_status == CardStatus.idle: + self.log.info('Measurement is not runnning') + return 0 + + + def pause_measure(self): + """ Pauses the current measurement. + + Fast counter must be initially in the run state to make it pause. + """ + self.cmd.card.disable_trigger() + self.cmd.process.trigger_enabled = False + self.loop_manager.stop_data_process() + + self._internal_status = CardStatus.paused + self.log.debug('Measurement paused') + return + + def continue_measure(self): + """ Continues the current measurement. + + If fast counter is in pause state, then fast counter will be continued. + """ + self.log.info('Measurement continued') + self._internal_status = CardStatus.running + self.loop_manager.loop_on = True + self.cmd.card.enable_trigger() + self.cmd.process.trigger_enabled = True + self.loop_manager.start_data_process() + + return 0 + + def is_gated(self): + """ Check the gated counting possibility. + + @return bool: Boolean value indicates if the fast counter is a gated + counter (TRUE) or not (FALSE). + """ + return self.ms.gated + + def get_binwidth(self): + """ Returns the width of a single timebin in the timetrace in seconds. + + @return float: current length of a single bin in seconds (seconds/bin) + """ + return self.ms.binwidth_s + diff --git a/src/qudi/hardware/fast_adc/spectrum/si_utils/si_commander.py b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_commander.py new file mode 100644 index 000000000..6983d0745 --- /dev/null +++ b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_commander.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +""" +This file contains the commander classes for spectrum instrumentation ADC. + +Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" +from qudi.hardware.fast_adc.spectrum.si_utils.si_settings import MeasurementSettings + + +class Commander: + """ + This class commands the process to be done in the measurement loop. + It communicates with the command class and the data process class. + Its main join is to ask the buffer about available data, process that data, and free that buffer. + """ + def __init__(self, cmd, log): + """ + @param Commands cmd: Command instance + @param log: qudi logger from the SpectrumInstrumentation. + """ + self.cmd = cmd + self.processor = None + self._log = log + + self._seq_size_B = 0 + self._reps = 0 + self._gated = False + self.unprocessed_reps_limit = 0 + + def init(self, ms: MeasurementSettings, data_processor): + self._seq_size_B = ms.seq_size_B + self._reps = ms.reps + self._gated = ms.gated + self.unprocessed_reps_limit = ms.reps_per_buf + + self._assign_process(ms.gated, ms.data_stack_on) + + self.processor = data_processor + + def _assign_process(self, gated, stack_on): + + def process_data_ungated_stackoff(): + curr_avail_reps = self.cmd.process.get_curr_avail_reps() + if curr_avail_reps == 0: + return + user_pos_B = self.cmd.data_buf.get_avail_user_pos_B() + self.processor.update_data(curr_avail_reps, user_pos_B) + self.cmd.data_buf.set_avail_card_len_B(curr_avail_reps * self._seq_size_B) + + def process_data_ungated_stackon(): + curr_avail_reps = self.cmd.process.get_curr_avail_reps() + if curr_avail_reps == 0: + return + user_pos_B = self.cmd.data_buf.get_avail_user_pos_B() + self.processor.update_data(curr_avail_reps, user_pos_B) + self.cmd.data_buf.set_avail_card_len_B(curr_avail_reps * self._seq_size_B) + self.processor.stack_new_data() + + def process_data_gated_stackoff(): + curr_avail_reps = self.cmd.process.get_curr_avail_reps() + if curr_avail_reps == 0: + return + user_pos_B = self.cmd.data_buf.get_avail_user_pos_B() + ts_user_pos_B = self.cmd.ts_buf.get_ts_avail_user_pos_B() + self.processor.update_data(curr_avail_reps, user_pos_B, ts_user_pos_B) + self.cmd.data_buf.set_avail_card_len_B(curr_avail_reps * self._seq_size_B) + + def process_data_gated_stackon(): + curr_avail_reps = self.cmd.process.get_curr_avail_reps() + if curr_avail_reps == 0: + return + user_pos_B = self.cmd.data_buf.get_avail_user_pos_B() + ts_user_pos_B = self.cmd.ts_buf.get_ts_avail_user_pos_B() + self.processor.update_data(curr_avail_reps, user_pos_B, ts_user_pos_B) + self.cmd.data_buf.set_avail_card_len_B(curr_avail_reps * self._seq_size_B) + self.processor.stack_new_data() + self.processor.stack_new_ts() + + if gated: + if stack_on: + self._process_curr_avail_data = process_data_gated_stackon + else: + self._process_curr_avail_data = process_data_gated_stackoff + else: + if stack_on: + self._process_curr_avail_data = process_data_ungated_stackon + else: + self._proess_curr_avail_data = process_data_ungated_stackoff + + def do_init_process(self): + curr_avail_reps = self.cmd.process.get_curr_avail_reps() + user_pos_B = self.cmd.data_buf.get_avail_user_pos_B() + ts_user_pos_B = self.cmd.ts_buf.get_ts_avail_user_pos_B() if self._gated else None + self.processor.process_data(curr_avail_reps, user_pos_B, ts_user_pos_B) + self.processor.get_initial_data() + self.processor.get_initial_avg() + self.cmd.data_buf.set_avail_card_len_B(curr_avail_reps * self._seq_size_B) + + def command_process(self): + """ + Command the main process dependent on the repetitions of unprocessed data. + """ + + trig_reps = self.cmd.process.get_trig_reps() + processed_reps = self.processor.data.avg.num_rep + unprocessed_reps = trig_reps - processed_reps + if self._reps != 0 and processed_reps >= self._reps: + return + + if trig_reps == 0 or unprocessed_reps == 0: + self.cmd.process.toggle_trigger(True) + + elif unprocessed_reps < self.unprocessed_reps_limit: + self.cmd.process.toggle_trigger(True) + self._process_curr_avail_data() + + elif unprocessed_reps >= self.unprocessed_reps_limit: + self._log.info('trigger off for too much unprocessed data') + self.cmd.process.toggle_trigger(False) + self._process_curr_avail_data() diff --git a/src/qudi/hardware/fast_adc/spectrum/si_utils/si_data_fetcher.py b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_data_fetcher.py new file mode 100644 index 000000000..775408a7d --- /dev/null +++ b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_data_fetcher.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +""" +This file contains the data fetcher class for spectrum instrumentation ADC. + +Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" +import numpy as np +import ctypes as c + + +class DataFetcher: + """ + This class can fetch the data from the data buffer and the timestamps from the timestamp buffer. + """ + + def __init__(self): + self.hw_dt = None + self.ts_dt = None + + def init_buffer(self, ms): + """ + @param MeasurementSettings ms: MeasurementSettings instance from the main. + """ + self.hw_dt = DataTransfer(ms.c_buf_ptr, ms.data_type, + ms.data_bytes_B, ms.seq_size_S, ms.reps_per_buf) + if ms.gated: + self.ts_dt = DataTransfer(ms.c_ts_buf_ptr, ms.ts_data_type, + ms.ts_data_bytes_B, ms.ts_seq_size_S, ms.reps_per_buf) + + def fetch_data(self, curr_avail_reps, user_pos_B): + data = self.hw_dt.get_new_data(curr_avail_reps, user_pos_B) + return data + + def fetch_ts_data(self, curr_avail_reps, ts_user_pos_B): + ts_row = self.ts_dt.get_new_data(curr_avail_reps, ts_user_pos_B).astype(np.uint64) + ts_r, ts_f = self._get_ts_rf(ts_row) + return ts_r, ts_f + + def _get_ts_rf(self, ts_row): + """ + Get the timestamps for the rising and falling edges. + Refer to 'Timestamps' in the documentation for the timestamp data format. + """ + ts_used = ts_row[::2] # odd data is always filled with zero + ts_r = ts_used[::2] + ts_f = ts_used[1::2] + return ts_r, ts_f + + +class DataTransfer: + ''' + This class can access the data stored in the buffer by specifying the buffer pointer. + Refer to the chapter 'Buffer handling' in the manual. + ''' + + def __init__(self, c_buf_ptr, data_type, data_bytes_B, seq_size_S, reps_per_buf): + self.c_buf_ptr = c_buf_ptr + self.data_type = data_type + self.data_bytes_B = data_bytes_B + self.seq_size_S = seq_size_S + self.seq_size_B = seq_size_S * self.data_bytes_B + self.reps_per_buf = reps_per_buf + + def _cast_buf_ptr(self, user_pos_B): + """ + Return the pointer of the specified data type at the user position + + @params int user_pos_B: user position in bytes + + @return ctypes pointer + """ + c_buffer = c.cast(c.addressof(self.c_buf_ptr) + user_pos_B, c.POINTER(self.data_type)) + return c_buffer + + def _asnparray(self, c_buffer, shape): + """ + Create a numpy array from the ctypes pointer with selected shape. + + @param ctypes pointer for the buffer + + @return numpy array + """ + + np_buffer = np.ctypeslib.as_array(c_buffer, shape=shape) + return np_buffer + + def _fetch_from_buf(self, user_pos_B, sample_S): + """ + Fetch given number of samples at the user position. + + @params int user_pos_B: user position in bytes + @params int sample_S: number of samples to fetch + + @return numpy array: 1D data + """ + shape = (sample_S,) + c_buffer = self._cast_buf_ptr(user_pos_B) + np_buffer = self._asnparray(c_buffer, shape) + return np_buffer + + def get_new_data(self, curr_avail_reps, user_pos_B): + """ + Get the currently available new data at the user position + dependent on the relative position of the user position in the buffer. + + @params int user_pos_B: user position in bytes + @params int curr_avail_reps: the number of currently available repetitions + + @return numpy array + """ + rep_end = int(user_pos_B / self.seq_size_B) + curr_avail_reps + + if 0 < rep_end <= self.reps_per_buf: + np_data = self._fetch_data(curr_avail_reps, user_pos_B) + + elif self.reps_per_buf < rep_end < 2 * self.reps_per_buf: + np_data = self._fetch_data_buf_end(curr_avail_reps, user_pos_B) + else: + print('error: rep_end {} is out of range'.format(rep_end)) + return + + return np_data + + def _fetch_data(self, curr_avail_reps, user_pos_B): + """ + Fetch currently available data at user position in one shot + """ + np_data = self._fetch_from_buf(user_pos_B, curr_avail_reps * self.seq_size_S) + return np_data + + def _fetch_data_buf_end(self, curr_avail_reps, user_pos_B): + """ + Fetch currently available data at user position which exceeds the end of the buffer. + """ + processed_rep = int((user_pos_B / self.seq_size_B)) + reps_tail = self.reps_per_buf - processed_rep + reps_head = curr_avail_reps - reps_tail + + np_data_tail = self._fetch_data(user_pos_B, reps_tail) + np_data_head = self._fetch_data(0, reps_head) + np_data = np.append(np_data_tail, np_data_head) + + return np_data + + diff --git a/src/qudi/hardware/fast_adc/spectrum/si_utils/si_data_processer.py b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_data_processer.py new file mode 100644 index 000000000..8b312ff60 --- /dev/null +++ b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_data_processer.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +""" +This file contains the data processer classes for spectrum instrumentation ADC. + +Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" +import copy + + +class DataProcessorUngated: + """ + This class brings the data through the fetcher class and store it in the data class for ungated measurement. + """ + def __init__(self, data, fetcher): + """ + @param Data data: Data instance from the main. + @param DataFetcher fetcher: DataFetcher instance from the main. + """ + self.data = data + self.fetcher = fetcher + + def process_data(self, curr_avail_reps, user_pos_B, *args): + self._fetch_data(curr_avail_reps, user_pos_B) + self._get_new_avg_data() + + def _fetch_data(self, curr_avail_reps, user_pos_B): + self.data.dc_new.data = self.fetcher.fetch_data(curr_avail_reps, user_pos_B).reshape(curr_avail_reps, -1) + if self.data.data_info.num_channels == 2: + self.data.dc_new.data = self.data.dc_new.dual_ch_data.copy() + self.data.dc_new.num_rep = curr_avail_reps + + def _get_new_avg_data(self): + self.data.avg_new.data = self.data.dc_new.get_average() + self.data.avg_new.num_rep = copy.copy(self.data.dc_new.num_rep) + + def get_initial_data(self): + self.data.dc.data = copy.deepcopy(self.data.dc_new.data) + self.data.dc.num_rep = copy.deepcopy(self.data.dc_new.num_rep) + + def get_initial_avg(self): + self.data.avg.data = copy.deepcopy(self.data.avg_new.data) + self.data.avg.num_rep = copy.copy(self.data.avg_new.num_rep) + + def update_data(self, curr_avail_reps, user_pos_B, *args): + self.process_data(curr_avail_reps, user_pos_B) + self.data.avg.update(self.data.avg_new) + + def stack_new_data(self): + self.data.dc.stack(self.data.dc_new) + +class DataProcessGated(DataProcessorUngated): + """ + This class brings the data through the fetcher class and store it in the data class for gated measurement. + In addition to the data, it also collects the timestamps. + """ + + def process_data(self, curr_avail_reps, user_pos_B, ts_user_pos_B): + self._fetch_data(curr_avail_reps, user_pos_B) + self._fetch_ts(curr_avail_reps, ts_user_pos_B) + self._get_new_avg_data() + + def update_data(self, curr_avail_reps, user_pos_B, ts_user_pos_B): + self.process_data(curr_avail_reps, user_pos_B, ts_user_pos_B) + self.data.avg.update(self.data.avg_new) + + def _fetch_ts(self, curr_avail_reps, ts_user_pos_B): + self.data.ts_new.ts_r, self.data.ts_new.ts_f = self.fetcher.fetch_ts_data(curr_avail_reps, ts_user_pos_B) + + def stack_new_ts(self): + self.data.ts.stack(self.data.ts_new) + + + + + + diff --git a/src/qudi/hardware/fast_adc/spectrum/si_utils/si_dataclass.py b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_dataclass.py new file mode 100644 index 000000000..2195bed2e --- /dev/null +++ b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_dataclass.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +""" +This file contains the data classes for spectrum instrumentation ADC. + +Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" +import numpy as np +from dataclasses import dataclass + + +class Data: + """ + This class contains all the dataclasses of the measurement. + All the new data and average are stores in dc_new and avg_new classes, and + stacked on data and avg, respectively. + """ + + def __init__(self): + self.data_info = DataInfo() + self.dc = DataClass() + self.dc_new = DataClass() + self.avg = AverageDataClass() + self.avg_new = AverageDataClass() + self.ts = None + self.ts_new = None + + def initialize(self, ms, cs): + self.data_info.data_bit = ms.data_bits + self.data_info.num_channels = ms.num_channels + + self.dc.initialize(ms.num_channels, ms.total_pulse, ms.seq_size_S) + self.dc_new.initialize(ms.num_channels, ms.total_pulse, ms.seq_size_S) + self.avg.initialize(ms.num_channels, ms.total_pulse, ms.seq_size_S) + self.avg_new.initialize(ms.num_channels, ms.total_pulse, ms.seq_size_S) + if ms.gated: + self.ts = TimeStamp() + self.ts_new = TimeStamp() + +@dataclass +class DataClass: + ''' + default data structure is (rep_index, all_pulses) + ''' + data: np.ndarray = np.array([]) + num_ch: int = 1 + num_pulse: int = 0 + num_rep: int = 0 + + def initialize(self, num_ch, num_pulse, seq_size_S): + self.num_ch = num_ch + self.num_pulse = num_pulse + self.data = np.zeros((0, seq_size_S), int) + self.num_rep = 0 + + def stack(self, new_data): + self.data = np.vstack((self.data, new_data.data)) + self.num_rep += new_data.num_rep + + def get_average(self): + return self.data.mean(axis=0) + + @property + def pulse_array(self): + """ + data reshaped from single multi pulse data into 2d array separated by pulse numbers. + """ + return self.data.reshape((self.num_pulse * self.num_ch, -1)) + + @property + def dual_ch_data(self): + """" + return data reshaped from the pairwise combined data. See 'Data organization' in manual. + """ + return np.hstack((self.data[:, 0::2], self.data[:, 1::2])) + +@dataclass +class AverageDataClass(DataClass): + """ + Dataclass for averaged sequence data consisting of multiple pulses. + Use pulse_array to get 2d array segmented by reps. + """ + + def initialize(self, num_ch, num_pulse, seq_size_S): + self.num_ch = num_ch + self.num_pulse = num_pulse + self.data = np.empty((0, seq_size_S)) + + self.num_rep = 0 + + def update(self, new_avgdata): + ''' + @param AverageDataClass new_avgdata: : + ''' + avg_data_set = np.vstack((self.data, new_avgdata.data)) + avg_weights = np.array([self.num_rep, new_avgdata.num_rep], dtype=int) + try: + self.data = np.average(avg_data_set, axis=0, weights=avg_weights) + self.num_rep += new_avgdata.num_rep + except: + print(avg_weights, avg_data_set) + return self.num_rep, self.data + + +@dataclass +class TimeStamp: + ts_r: np.ndarray = np.array([], dtype=np.uint64) + ts_f: np.ndarray = np.array([], dtype=np.uint64) + + def __init__(self): + self.initialize() + + def initialize(self): + self.ts_r = np.empty(0, np.uint64) + self.ts_f = np.empty(0, np.uint64) + + def stack(self, ts): + """ + @param TimeStamp ts: + """ + self.ts_r = np.hstack((self.ts_r, ts.ts_r)) + self.ts_f = np.hstack((self.ts_f, ts.ts_f)) + +@dataclass +class DataInfo: + data_range_mV: int = 1000 + data_bit: int = 16 + diff --git a/src/qudi/hardware/fast_adc/spectrum/si_utils/si_loop_manager.py b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_loop_manager.py new file mode 100644 index 000000000..bd590100e --- /dev/null +++ b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_loop_manager.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +""" +This file contains the data classes for spectrum instrumentation ADC. + +Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" +import time +from qudi.util.mutex import Mutex +from PySide2.QtCore import QThread + + +class LoopManager(QThread): + """ + This is the main data process loop class. + The loop keeps on asking the commander to do the measurement process. + """ + threadlock = Mutex() + + def __init__(self, commander, log): + """ + @param Commander commander: Commander instance from the main. + @param log: qudi logger from the SpectrumInstrumentation. + """ + super().__init__() + self.commander = commander + self._log = log + + self.loop_on = False + self._reps = 0 + self.start_time = 0 + self.data_proc_th = None + + def input_ms(self, ms): + self._reps = ms.reps + + def init_measure_params(self): + self.start_time = time.time() + self.loop_on = False + + def start_data_process(self): + """ + Start the data process with creating a new thread. + """ + self.start_time = time.time() + self.loop_on = True + return + + def run(self): + self._log.debug('loop running') + time_out = self.commander.cmd.process.wait_avail_data() + if time_out: + return + + self.commander.do_init_process() + + if self._reps == 0: + self._start_inifinite_loop() + else: + self._start_finite_loop() + self._log.debug('loop finished') + + def _start_inifinite_loop(self): + self._log.debug('data process started') + while self.loop_on: + self.commander.command_process() + else: + self._log.debug('data process stopped') + return + + def _start_finite_loop(self): + self._log.debug('data process started') + num_rep = self.commander.processor.data.dc.num_rep + while self.loop_on and num_rep < self._reps: + self.commander.command_process() + else: + self._log.debug('data process stopped') + return + + def stop_data_process(self): + with self.threadlock: + self.loop_on = False \ No newline at end of file diff --git a/src/qudi/hardware/fast_adc/spectrum/si_utils/si_settings.py b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_settings.py new file mode 100644 index 000000000..255bfa6ed --- /dev/null +++ b/src/qudi/hardware/fast_adc/spectrum/si_utils/si_settings.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +""" +This file contains the settings classes for spectrum instrumentation ADC. + +Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this +distribution and on + +This file is part of qudi. + +Qudi is free software: you can redistribute it and/or modify it under the terms of +the GNU Lesser General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later version. + +Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along with qudi. +If not, see . +""" +import numpy as np +from dataclasses import dataclass +import typing +from ctypes import c_void_p, c_int16, c_int32, c_uint64 + + +@dataclass +class CardSettings: + """ + This dataclass contains parameters input to the card. + """ + ai_ch: tuple = 'CH0' + ai0_range_mV: int = 1000 + ai0_offset_mV: int = 0 + ai0_term: str = '' + ai0_coupling: str = '' + ai1_range_mV: int = 1000 + ai1_offset_mV: int = 0 + ai1_term: str = '' + ai1_coupling: str = '' + acq_mode: str = '' + acq_HW_avg_num: int = 1 + acq_pre_trigs_S: int = 16 + acq_post_trigs_S: int = 0 #Dynamic + acq_seg_size_S:int = 0 #Dynamic + acq_mem_size_S: int = 0 #Dynamic + acq_loops: int = 0 + buf_size_B: int = 0 #Dynamic + buf_notify_size_B: int = 4096 + clk_samplerate_Hz: int = 250e6 #Dynamic + clk_ref_Hz: int = 10e6 + trig_mode: str = '' + trig_level_mV: int = 1000 + ts_buf_size_B: int = 0 + ts_buf_notify_size_B: int = 2048 + + def calc_dynamic_cs(self, ms): + """ + Calculate the card settings parameter which changes according to the measurement settings. + + @param ms: measurement settings class instance + """ + self.clk_samplerate_Hz = int(np.ceil(1 / ms.binwidth_s)) + + self._calc_acq_params(ms.seg_size_S, ms.seq_size_S, ms.reps, ms.total_gate) + + def _calc_acq_params(self, seg_size_S, seq_size_S, reps, total_gate): + if 'STD' in self.acq_mode: + self.acq_mem_size_S = int(seq_size_S * reps) + if 'GATE' not in self.acq_mode: + self.acq_seg_size_S = int(seg_size_S) + self.acq_post_trigs_S = int(seg_size_S - self.acq_pre_trigs_S) + + elif 'FIFO' in self.acq_mode: + if 'GATE' in self.acq_mode: + self.acq_loops = int(reps * total_gate) + else: + self.acq_seg_size_S = int(seg_size_S) + self.acq_post_trigs_S = int(self.acq_seg_size_S - self.acq_pre_trigs_S) + self.acq_loops = int(reps) + + def get_buf_size_B(self, seq_size_B, reps_per_buf, total_pulse=1): + """ + Calculate the buffer size in bytes. + + @param int seq_size_B: sequence size in bytes + @param int reps_per_buf: Total number of repetitions which can be stored in the given memory + """ + self.buf_size_B = int(seq_size_B * reps_per_buf) + self.ts_buf_size_B = int(2 * 16 * total_pulse * reps_per_buf) + + @property + def gated(self): + return 'GATE' in self.acq_mode + +@dataclass +class MeasurementSettings: + """ + This dataclass contains paremeters given by the logic and used for data process. + """ + #Fixed + c_buf_ptr: typing.Any = c_void_p() + #Given by the config + num_channels: int = 1 + gated: bool = False + init_buf_size_S: int = 0 + reps: int = 0 + max_reps_per_buf = 1e4 + data_stack_on: bool = True + #Given by the measurement + binwidth_s: float = 0 + record_length_s: float = 0 + total_gate: int = 1 + total_pulse: int = 1 + #Calculated + seg_size_S: int = 0 + seq_size_S: int = 0 + seq_size_B: int = 0 + reps_per_buf: int = 0 + actual_length_s: float = 0 + data_bits: int = 0 + #Gate + c_ts_buf_ptr: typing.Any = c_void_p() + ts_data_bits: int = 64 + ts_data_bytes_B: int = 8 + ts_seg_size_S: int = 4 #rise + null + fall + null + ts_seg_size_B: int = 32 + ts_seq_size_S: int = 8 + ts_seq_size_B: int = 32 + gate_length_S: int = 0 + gate_length_rounded_S: int = 0 + gate_end_alignment_S: int = 16 + double_gate_acquisition: bool = False + + def load_dynamic_params(self, binwidth_s, record_length_s, number_of_gates): + self.binwidth_s = binwidth_s + self.record_length_s = record_length_s + self.total_pulse = number_of_gates + if self.double_gate_acquisition: + self.total_gate = 2 * self.total_pulse + else: + self.total_gate = self.total_pulse + + def calc_data_size_S(self, pre_trigs_S, post_trigs_S): + if not self.gated: + self._calc_triggered_data_size_S() + else: + self._calc_gated_data_size_S(pre_trigs_S, post_trigs_S) + + def _calc_triggered_data_size_S(self): + """ + defines the data size parameters for the trigger mode. + record_length_s is the length of data to be recorded at one trigger in seconds. + The sequence size is the length of data to be recorded in samples. + The segment size used for the acquisition setting corresponds to the sequence size. + """ + seq_size_S_per_ch = int(np.ceil((self.record_length_s / self.binwidth_s) / 16) * 16) # necessary to be multuples of 16 + self.seq_size_S = self.num_channels * seq_size_S_per_ch + self.seg_size_S = self.seq_size_S + + def _calc_gated_data_size_S(self, pre_trigs_S, post_trigs_S): + """ + defines the data size parameters for the gate mode. + The gate length is given by the input gate length, which is calculated from record_length_s. + Note that the actual gate length is rounded by the card specific alignment. + seg_size_S is the segment size recorded per gate. + seq_size_S is the sequence size per repetition. + """ + self.gate_length_S = int(np.ceil(self.record_length_s / self.binwidth_s)) + self.gate_length_rounded_S = int(np.ceil(self.gate_length_S / self.gate_end_alignment_S) * self.gate_end_alignment_S) + seg_size_S_per_ch = self.gate_length_rounded_S + pre_trigs_S + post_trigs_S + self.seg_size_S = self.num_channels * seg_size_S_per_ch + self.seq_size_S = self.seg_size_S * self.total_gate + self.ts_seq_size_S = self.ts_seg_size_S * self.total_gate + self.ts_seq_size_B = self.ts_seg_size_B * self.total_gate + + def calc_actual_length_s(self): + if not self.gated: + self.actual_length_s = self.seq_size_S * self.binwidth_s + else: + self.actual_length_s = self.seg_size_S * self.binwidth_s + + def assign_data_bit(self, acq_mode): + if acq_mode == 'FIFO_AVERAGE': + self.data_bits = 32 + else: + self.data_bits = 16 + + @property + def data_type(self): + if self.data_bits == 16: + return c_int16 + elif self.data_bits == 32: + return c_int32 + else: + pass + + @property + def data_bytes_B(self): + if self.data_bits == 16: + return 2 + elif self.data_bits == 32: + return 4 + else: + pass + + def calc_buf_params(self): + """ + Calculate the parameters related to the buffer size. + The number of repetitions which can be recorded in the buffer is calculated + based on the given initial buffer size and the calculated sequence size. + Maximum repetitions per buffer is used because memory allocation error was observed + when too big a buffer was defined for lower sampling rate. + """ + try: + reps_per_buf = int(self.init_buf_size_S / self.seq_size_S) + except ZeroDivisionError: + reps_per_buf = self.max_reps_per_buf + self.reps_per_buf = int(min(reps_per_buf, self.max_reps_per_buf)) + self.seq_size_B = int(self.seq_size_S * self.data_bytes_B) + + #Gate + @property + def ts_data_type(self): + return c_uint64 + +