From 6c4c5488c63f88e85bc7fbee07fb6e0a5c6f0d16 Mon Sep 17 00:00:00 2001 From: sldesnoo-Delft <s.l.desnoo@tudelft.nl> Date: Wed, 18 Jan 2023 17:52:46 +0100 Subject: [PATCH] Added iq_mode to measurement parameter and Qblox fast_scan --- pulse_lib/acquisition/iq_modes.py | 21 ++++ .../acquisition/measurement_converter.py | 102 +++++++++--------- pulse_lib/fast_scan/qblox_fast_scans.py | 45 ++++++-- pulse_lib/sequencer.py | 54 ++++++---- pulse_lib/tests/acquire/test_downsampling.py | 3 +- pulse_lib/tests/acquire/test_fast_scan.py | 30 +++++- 6 files changed, 169 insertions(+), 86 deletions(-) create mode 100644 pulse_lib/acquisition/iq_modes.py diff --git a/pulse_lib/acquisition/iq_modes.py b/pulse_lib/acquisition/iq_modes.py new file mode 100644 index 00000000..224af7a8 --- /dev/null +++ b/pulse_lib/acquisition/iq_modes.py @@ -0,0 +1,21 @@ +import numpy as np + +def iq_mode2func(iq_mode): + ''' + Returns: + func[np.array]->np.array, or + list[Tuple[str, func[np.array]->np.array]] + ''' + func_map = { + 'Complex': lambda x:x, + 'I': np.real, + 'Q': np.imag, + 'abs': np.abs, + 'angle': np.angle, + 'I+Q': [('_I', np.real), ('_Q', np.imag)], + 'abs+angle': [('_abs', np.abs), ('_angle', np.angle)], + } + try: + return func_map[iq_mode] + except KeyError: + raise Exception(f'Unknown iq_mode f{iq_mode}') diff --git a/pulse_lib/acquisition/measurement_converter.py b/pulse_lib/acquisition/measurement_converter.py index 1d1f26de..d1d905cb 100644 --- a/pulse_lib/acquisition/measurement_converter.py +++ b/pulse_lib/acquisition/measurement_converter.py @@ -3,7 +3,7 @@ from typing import Tuple, List import logging import numpy as np from pulse_lib.segments.segment_measurements import measurement_acquisition, measurement_expression - +from pulse_lib.acquisition.iq_modes import iq_mode2func from qcodes import MultiParameter @@ -32,6 +32,23 @@ class SetpointsSingle: self.setpoint_labels += (setpoint_label, ) self.setpoint_units += (setpoint_unit, ) + def with_attributes(self, name=None, unit=None): + label = self.label + if name is None: + name = self.name + else: + if label.startswith(self.name): + label = name + label[len(self.name):] + if unit is None: + unit = self.unit + return SetpointsSingle(name, label, unit, + self.shape, + self.setpoints, + self.setpoint_names, + self.setpoint_labels, + self.setpoint_units) + + class SetpointsMulti: ''' Pass to MultiParameter using __dict__ attribute. Example: @@ -57,7 +74,7 @@ class DataSelection: selectors: bool = False total_selected: bool = False accept_mask: bool = False - iq_complex: bool =True + iq_mode: str = 'Complex' class MeasurementParameter(MultiParameter): @@ -176,7 +193,7 @@ class MeasurementConverter: self._channel_raw = {} self._raw = [] - self._raw_split = [] + self._raw_is_iq = [] self._states = [] self._selectors = [] self._values = [] @@ -184,7 +201,6 @@ class MeasurementConverter: self._accepted = [] self.sp_raw = [] - self.sp_raw_split = [] self.sp_states = [] self.sp_selectors = [] self.sp_values = [] @@ -209,23 +225,10 @@ class MeasurementConverter: if m.interval is not None: time = tuple(np.arange(m.n_samples) * m.interval) sp_raw.append(time, 'time', 'time', 'ns') - channel = digitizer_channels[channel_name] self.sp_raw.append(sp_raw) - - if channel.iq_out: - for suffix in ['_I', '_Q']: - name = f'{m.name}{suffix}' - label = f'{m.name}{suffix} ({channel_name}{suffix}:{m.index})' - sp_raw = SetpointsSingle(name, label, 'mV', - sp_raw.shape, - sp_raw.setpoints, - sp_raw.setpoint_names, - sp_raw.setpoint_labels, - sp_raw.setpoint_units) - self.sp_raw_split.append(sp_raw) - else: - self.sp_raw_split.append(sp_raw) + channel = digitizer_channels[channel_name] + self._raw_is_iq.append(channel.iq_out) def _generate_setpoints(self): @@ -261,44 +264,21 @@ class MeasurementConverter: self.sp_total.append(SetpointsSingle('total_selected', 'total_selected', '#')) def _get_names(self, selection): - sp_list = [] - if selection.raw: - if selection.iq_complex: - sp_list += self.sp_raw - else: - sp_list += self.sp_raw_split - if selection.states: - sp_list += self.sp_states - if selection.values: - sp_list += self.sp_values - if selection.selectors: - sp_list += self.sp_selectors - if selection.total_selected: - sp_list += self.sp_total - if selection.accept_mask: - sp_list += self.sp_mask - names = [sp.name for sp in sp_list] - return names + setpoints = self.get_setpoints(selection) + return setpoints.names def _set_data_raw(self): - digitizer_channels = self._description.digitizer_channels self._raw = [] self._raw_split = [] for m in self._description.measurements: if isinstance(m, measurement_acquisition): channel_name = m.acquisition_channel - channel = digitizer_channels[channel_name] if m.n_samples is None: channel_raw = self._channel_raw[channel_name][...,m.data_offset] else: channel_raw = self._channel_raw[channel_name][...,m.data_offset:m.data_offset+m.n_samples] self._raw.append(channel_raw) - if channel.iq_out: - self._raw_split.append(channel_raw.real) - self._raw_split.append(channel_raw.imag) - else: - self._raw_split.append(channel_raw.real) def _set_states(self): # iterate through measurements and keep last named values in dictionary @@ -351,10 +331,22 @@ class MeasurementConverter: def get_setpoints(self, selection): sp_list = [] if selection.raw: - if selection.iq_complex: - sp_list += self.sp_raw - else: - sp_list += self.sp_raw_split + for sp,is_iq in zip(self.sp_raw, self._raw_is_iq): + if not is_iq: + sp_list.append(sp) + else: + funcs = iq_mode2func(selection.iq_mode) + if isinstance(funcs, list): + for postfix,_ in funcs: + unit = 'rad' if postfix == '_angle' else 'mV' + sp_new = sp.with_attributes(name=sp.name+postfix, unit=unit) + sp_list.append(sp_new) + else: + if selection.iq_mode == 'angle': + sp_new = sp.with_attributes(unit='rad') + sp_list.append(sp_new) + else: + sp_list.append(sp) if selection.states: sp_list += self.sp_states if selection.values: @@ -370,10 +362,16 @@ class MeasurementConverter: def get_measurement_data(self, selection): data = [] if selection.raw: - if selection.iq_complex: - data += self._raw - else: - data += self._raw_split + for raw,is_iq in zip(self._raw, self._raw_is_iq): + if not is_iq: + data.append(raw) + else: + funcs = iq_mode2func(selection.iq_mode) + if isinstance(funcs, list): + for _,func in funcs: + data.append(func(raw)) + else: + data.append(funcs(raw)) if selection.states: data += self._states if selection.values: diff --git a/pulse_lib/fast_scan/qblox_fast_scans.py b/pulse_lib/fast_scan/qblox_fast_scans.py index 6cde74e1..c792c3e6 100644 --- a/pulse_lib/fast_scan/qblox_fast_scans.py +++ b/pulse_lib/fast_scan/qblox_fast_scans.py @@ -3,6 +3,7 @@ from qcodes import MultiParameter import numpy as np import logging +from pulse_lib.acquisition.iq_modes import iq_mode2func def fast_scan1D_param(pulse_lib, gate, swing, n_pt, t_step, biasT_corr=False, @@ -13,7 +14,8 @@ def fast_scan1D_param(pulse_lib, gate, swing, n_pt, t_step, enabled_markers=[], pulse_gates={}, n_avg=1, - iq_complex=True, + iq_mode='Complex', + iq_complex=None, ): """ Creates a parameter to do a 1D fast scan. @@ -38,6 +40,16 @@ def fast_scan1D_param(pulse_lib, gate, swing, n_pt, t_step, Gates to pulse during scan with pulse voltage in mV. E.g. {'vP1': 10.0, 'vB2': -29.1} n_avg (int): number of times to scan and average data. + iq_mode (str): + when channel contains IQ data, i.e. iq_input=True or frequency is not None, + then this parameter specifies how the complex I/Q value should be returned: + 'Complex': return IQ data as complex value. + 'I': return only I value. + 'Q': return only Q value. + 'abs': return absolute value (amplitude). + 'angle:' return angle (phase) in radians, + 'I+Q', return I and Q using channel name postfixes '_I', '_Q'. + 'abs+angle'. return absolute value and angle using channel name postfixes '_abs', '_angle'. iq_complex (bool): If True return IQ data as complex value in 1 value, otherwise return IQ data in two values with suffixes '_I' and '_Q'. @@ -56,7 +68,7 @@ def fast_scan1D_param(pulse_lib, gate, swing, n_pt, t_step, logging.error(msg) raise Exception(msg) - acq_channels,channel_map = _get_channels(pulse_lib, channel_map, channels, iq_complex) + acq_channels,channel_map = _get_channels(pulse_lib, channel_map, channels, iq_mode, iq_complex) vp = swing/2 line_margin = int(line_margin) @@ -124,7 +136,8 @@ def fast_scan2D_param(pulse_lib, gate1, swing1, n_pt1, gate2, swing2, n_pt2, t_s enabled_markers=[], pulse_gates={}, n_avg=1, - iq_complex=True, + iq_mode='Complex', + iq_complex=None, ): """ Creates a parameter to do a 2D fast scan. @@ -153,6 +166,16 @@ def fast_scan2D_param(pulse_lib, gate1, swing1, n_pt1, gate2, swing2, n_pt2, t_s Gates to pulse during scan with pulse voltage in mV. E.g. {'vP1': 10.0, 'vB2': -29.1} n_avg (int): number of times to scan and average data. + iq_mode (str): + when channel contains IQ data, i.e. iq_input=True or frequency is not None, + then this parameter specifies how the complex I/Q value should be returned: + 'Complex': return IQ data as complex value. + 'I': return only I value. + 'Q': return only Q value. + 'abs': return absolute value (amplitude). + 'angle:' return angle (phase) in radians, + 'I+Q', return I and Q using channel name postfixes '_I', '_Q'. + 'abs+angle'. return absolute value and angle using channel name postfixes '_abs', '_angle'. iq_complex (bool): If True return IQ data as complex value in 1 value, otherwise return IQ data in two values with suffixes '_I' and '_Q'. @@ -171,7 +194,7 @@ def fast_scan2D_param(pulse_lib, gate1, swing1, n_pt1, gate2, swing2, n_pt2, t_s logging.error(msg) raise Exception(msg) - acq_channels,channel_map = _get_channels(pulse_lib, channel_map, channels, iq_complex) + acq_channels,channel_map = _get_channels(pulse_lib, channel_map, channels, iq_mode, iq_complex) line_margin = int(line_margin) add_pulse_gate_correction = biasT_corr and len(pulse_gates) > 0 @@ -262,7 +285,9 @@ def fast_scan2D_param(pulse_lib, gate1, swing1, n_pt1, gate2, swing2, n_pt2, t_s biasT_corr, channel_map) -def _get_channels(pulse_lib, channel_map, channels, iq_complex): +def _get_channels(pulse_lib, channel_map, channels, iq_mode, iq_complex): + if iq_complex == False: + iq_mode = 'I+Q' if channel_map is not None: acq_channels = set(v[0] for v in channel_map.values()) else: @@ -275,9 +300,13 @@ def _get_channels(pulse_lib, channel_map, channels, iq_complex): channel_map = {} for name in acq_channels: dig_ch = dig_channels[name] - if dig_ch.iq_out and not iq_complex: - channel_map[name+'_I'] = (name, np.real) - channel_map[name+'_Q'] = (name, np.imag) + if dig_ch.iq_out and not iq_mode != 'Complex': + ch_funcs = iq_mode2func(iq_mode) + if isinstance(ch_funcs, list): + for postfix,func in ch_funcs: + channel_map[name+postfix] = (name, func) + else: + channel_map[name] = (name, ch_funcs) else: channel_map[name] = (name, lambda x:x) diff --git a/pulse_lib/sequencer.py b/pulse_lib/sequencer.py index 19e40004..98b10a0f 100644 --- a/pulse_lib/sequencer.py +++ b/pulse_lib/sequencer.py @@ -373,7 +373,7 @@ class sequencer(): def get_measurement_param(self, name='seq_measurements', upload=None, states=True, values=True, selectors=True, total_selected=True, accept_mask=True, - iq_complex=True): + iq_mode='Complex', iq_complex=None): ''' Returns a qcodes MultiParameter with an entry per measurement, i.e. per acquire call. The data consists of raw data and derived data. @@ -389,14 +389,20 @@ class sequencer(): When sample_rate is set with set_acquisition(sample_rate=sr), then the data contains time traces in a 2D array indexed [index_repetition][time_step]. - Only present when `iq_complex=True` or when - channel contains no IQ data. + Only present when channel contains no IQ data or + when `iq_complex=True` or `iq_mode in['Complex','I','Q','abs','angle']`. "{name}_I": Similar to "{name}", but contains I component of IQ. - Only present when channel contains IQ data, and `iq_complex=False`. + Only present when channel contains IQ data and `iq_mode='I+Q'`. "{name}_Q": Similar to "{name}", but contains Q component of IQ. - Only present when channel contains IQ data, and `iq_complex=False`. + Only present when channel contains IQ data and `iq_mode='I+Q'`. + "{name}_abs": + Similar to "{name}", but contains absolute value (amplitude) of IQ. + Only present when channel contains IQ data and `iq_mode='abs+angle'`. + "{name}_angle": + Similar to "{name}", but contains angle (phase) of IQ. + Only present when channel contains IQ data and `iq_mode='abs+angle'`. "{name}_state": Qubit states in 1 D array. Only present when `states=True`, threshold is set, @@ -451,10 +457,12 @@ class sequencer(): else: reader = self mc = self._get_measurement_converter() + if iq_complex == False: + iq_mode = 'I+Q' selection = DataSelection(raw=True, states=states, values=values, selectors=selectors, total_selected=total_selected, accept_mask=accept_mask, - iq_complex=iq_complex) + iq_mode=iq_mode) param = MeasurementParameter(name, reader, mc, selection) return param @@ -547,7 +555,8 @@ class sequencer(): def get_measurement_results(self, index=None, raw=True, states=True, values=True, selectors=True, total_selected=True, - accept_mask=True, iq_complex=True): + accept_mask=True, iq_mode='Complex', + iq_complex=None): ''' Returns data per measurement, i.e. per acquire call. The data consists of raw data and derived data. @@ -563,16 +572,24 @@ class sequencer(): When sample_rate is set with set_acquisition(sample_rate=sr), then the data contains time traces in a 2D array indexed [index_repetition][time_step]. - Only present when `raw=True` and `iq_complex=True` or - channel contains IQ data. + Only present when channel contains no IQ data or + when `iq_complex=True` or `iq_mode in['Complex','I','Q','abs','angle']`. "{name}_I": Similar to "{name}", but contains I component of IQ. Only present when channel contains IQ data, - `raw=True`, and `iq_complex=False`. + `raw=True`, and `iq_mode='I+Q'`. "{name}_Q": Similar to "{name}", but contains Q component of IQ. Only present when channel contains IQ data, - `raw=True`, and `iq_complex=False`. + `raw=True`, and `iq_mode='I+Q'`. + "{name}_abs": + Similar to "{name}", but contains absolute value (amplitude) of IQ. + Only present when channel contains IQ data, + `raw=True`, and `iq_mode='abs+angle'`. + "{name}_angle": + Similar to "{name}", but contains angle (phase) of IQ. + Only present when channel contains IQ data, + `raw=True`, and `iq_mode='abs+angle'`. "{name}_state": Qubit states in 1 D array. Only present when `states=True`, threshold is set, @@ -624,23 +641,14 @@ class sequencer(): index = self.sweep_index[::-1] mc = self._get_measurement_converter() mc.set_channel_data(self.get_channel_data(index)) + if iq_complex == False: + iq_mode = 'I+Q' selection = DataSelection(raw=raw, states=states, values=values, selectors=selectors, total_selected=total_selected, accept_mask=accept_mask, - iq_complex=iq_complex) + iq_mode=iq_mode) return mc.get_measurements(selection) - def get_measurement_data(self, index=None): - ''' - Deprecated - Returns channel data in V (!) - ''' - logging.warning('get_measurement_data is deprecated. Use get_channel_data') - return { - name:value/1000.0 - for name, value in self.get_channel_data(index).items() - } - def get_channel_data(self, index=None): ''' diff --git a/pulse_lib/tests/acquire/test_downsampling.py b/pulse_lib/tests/acquire/test_downsampling.py index 62ac25c0..018f63fb 100644 --- a/pulse_lib/tests/acquire/test_downsampling.py +++ b/pulse_lib/tests/acquire/test_downsampling.py @@ -1,6 +1,7 @@ from pulse_lib.tests.configurations.test_configuration import context +#%% def test1(): pulse = context.init_pulselib(n_gates=2, n_sensors=2) @@ -40,7 +41,7 @@ def test2(): s.wait(10000) sequence = pulse.mk_sequence([s]) - sequence.n_rep = None + sequence.n_rep = 2 sequence.set_acquisition(sample_rate=500e3) m_param = sequence.get_measurement_param() context.add_hw_schedule(sequence) diff --git a/pulse_lib/tests/acquire/test_fast_scan.py b/pulse_lib/tests/acquire/test_fast_scan.py index df0ca39d..e1548d3c 100644 --- a/pulse_lib/tests/acquire/test_fast_scan.py +++ b/pulse_lib/tests/acquire/test_fast_scan.py @@ -2,6 +2,7 @@ from pulse_lib.tests.configurations.test_configuration import context from pulse_lib.fast_scan.qblox_fast_scans import fast_scan1D_param, fast_scan2D_param +#%% def test1(): pulse = context.init_pulselib(n_gates=2, n_sensors=2, rf_sources=True) @@ -15,7 +16,7 @@ def test1(): 'P1', 100, 51, 'P2', 20, 21, 2000, - iq_complex=True) + iq_mode='Complex') data1 = m_param1D() print(m_param1D.names) @@ -40,8 +41,33 @@ def test2(): 'P1', 100, 51, 'P2', 20, 21, 2000, + iq_mode='I') + + data1 = m_param1D() + print(m_param1D.names) + print(data1) + data2 = m_param2D() + print(m_param2D.names) + print(data2) + print(flush=True) + + return context.run('fast_scan_IQ', m_param1D, m_param2D) + +def test3(): + pulse = context.init_pulselib(n_gates=2, n_sensors=2, rf_sources=True) + + m_param1D = fast_scan1D_param( + pulse, + 'P1', 100, 51, 2000, iq_complex=False) + m_param2D = fast_scan2D_param( + pulse, + 'P1', 100, 51, + 'P2', 20, 21, + 2000, + iq_mode='abs+angle') + data1 = m_param1D() print(m_param1D.names) print(data1) @@ -50,7 +76,7 @@ def test2(): print(data2) print(flush=True) - return context.run('fas_scan_IQ', m_param1D, m_param2D) + return context.run('fast_scan_IQ', m_param1D, m_param2D) if __name__ == '__main__': -- GitLab