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