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
+
+