Repository: microsoft/Qcodes

Issue:
DataSaver.add_result should fail with an error if unknown type is passed
Currently, if as part of a sweep, the result of an `ArrayParameter` is passed to `DataSaver.add_result` as a python list or tuple, as opposed to an `ndarray`, the data will silently be truncated to the first element. This is not made clear to the user and hence data is easily lost.

### Expected behaviour
Either an error should be thrown for an unknown type, or python sequence types should be handled.

### Actual behaviour
Data is silently truncated to the first item in the array.

### System
Windows on QCodes Master (940b46f6)



Discussion:

Resolving patch:
diff --git a/qcodes/dataset/measurements.py b/qcodes/dataset/measurements.py
--- a/qcodes/dataset/measurements.py
+++ b/qcodes/dataset/measurements.py
@@ -18,6 +18,9 @@
 
 log = logging.getLogger(__name__)
 
+array_like_types = (tuple, list, np.ndarray)
+non_array_like_types = (int, float, str)
+
 
 class ParameterTypeError(Exception):
     pass
@@ -101,7 +104,7 @@ def add_result(self,
                 raise ValueError(f'Can not add a result for {paramstr}, no '
                                  'such parameter registered in this '
                                  'measurement.')
-            if isinstance(value, np.ndarray):
+            if any(isinstance(value, typ) for typ in array_like_types):
                 value = cast(np.ndarray, partial_result[1])
                 value = np.atleast_1d(value)
                 array_size = len(value)
@@ -111,6 +114,14 @@ def add_result(self,
                                      f'and {array_size}')
                 else:
                     input_size = array_size
+            elif any(isinstance(value, t) for t in non_array_like_types):
+                pass
+            else:
+                raise ValueError('Wrong value type received. '
+                                 f'Got {type(value)}, but only int, float, '
+                                 'str, tuple, list, and np.ndarray is '
+                                 'allowed.')
+
             # TODO (WilliamHPNielsen): The following code block is ugly and
             # brittle and should be enough to convince us to abandon the
             # design of ArrayParameters (possibly) containing (some of) their


Test patch:
diff --git a/qcodes/tests/dataset/test_measurement_context_manager.py b/qcodes/tests/dataset/test_measurement_context_manager.py
--- a/qcodes/tests/dataset/test_measurement_context_manager.py
+++ b/qcodes/tests/dataset/test_measurement_context_manager.py
@@ -54,7 +54,8 @@ def DMM():
 @pytest.fixture
 def SpectrumAnalyzer():
     """
-    Yields a DummyInstrument that holds an ArrayParameter
+    Yields a DummyInstrument that holds ArrayParameters returning
+    different types
     """
 
     class Spectrum(ArrayParameter):
@@ -80,8 +81,22 @@ def get_raw(self):
             # not the best SA on the market; it just returns noise...
             return np.random.randn(self.npts)
 
+    class ListSpectrum(Spectrum):
+
+        def get_raw(self):
+            output = super().get_raw()
+            return list(output)
+
+    class TupleSpectrum(Spectrum):
+
+        def get_raw(self):
+            output = super().get_raw()
+            return tuple(output)
+
     SA = DummyInstrument('dummy_SA')
     SA.add_parameter('spectrum', parameter_class=Spectrum)
+    SA.add_parameter('listspectrum', parameter_class=ListSpectrum)
+    SA.add_parameter('tuplespectrum', parameter_class=TupleSpectrum)
 
     yield SA
 
@@ -462,7 +477,7 @@ def test_datasaver_scalars(experiment, DAC, DMM, set_values, get_values,
 
 @settings(max_examples=10, deadline=None)
 @given(N=hst.integers(min_value=2, max_value=500))
-def test_datasaver_arrays(empty_temp_db, N):
+def test_datasaver_arrays_lists_tuples(empty_temp_db, N):
     new_experiment('firstexp', sample_name='no sample')
 
     meas = Measurement()
@@ -498,6 +513,7 @@ def test_datasaver_arrays(empty_temp_db, N):
                                    unit='Majorana flux',
                                    setpoints=('freqax', 'gate_voltage'))
 
+    # save arrays
     with meas.run() as datasaver:
         freqax = np.linspace(1e6, 2e6, N)
         signal = np.random.randn(N)
@@ -508,6 +524,44 @@ def test_datasaver_arrays(empty_temp_db, N):
 
     assert datasaver.points_written == N
 
+    # save lists
+    with meas.run() as datasaver:
+        freqax = list(np.linspace(1e6, 2e6, N))
+        signal = list(np.random.randn(N))
+
+        datasaver.add_result(('freqax', freqax),
+                             ('signal', signal),
+                             ('gate_voltage', 0))
+
+    assert datasaver.points_written == N
+
+    # save tuples
+    with meas.run() as datasaver:
+        freqax = tuple(np.linspace(1e6, 2e6, N))
+        signal = tuple(np.random.randn(N))
+
+        datasaver.add_result(('freqax', freqax),
+                             ('signal', signal),
+                             ('gate_voltage', 0))
+
+    assert datasaver.points_written == N
+
+
+def test_datasaver_foul_input(experiment):
+
+    meas = Measurement()
+
+    meas.register_custom_parameter('foul',
+                                   label='something unnatural',
+                                   unit='Fahrenheit')
+
+    foul_stuff = [qc.Parameter('foul'), set((1, 2, 3))]
+
+    with meas.run() as datasaver:
+        for ft in foul_stuff:
+            with pytest.raises(ValueError):
+                datasaver.add_result(('foul', ft))
+
 
 @settings(max_examples=10, deadline=None)
 @given(N=hst.integers(min_value=2, max_value=500))
@@ -572,6 +626,74 @@ def test_datasaver_array_parameters(experiment, SpectrumAnalyzer, DAC, N, M):
     assert datasaver.points_written == N*M
 
 
+@settings(max_examples=5, deadline=None)
+@given(N=hst.integers(min_value=5, max_value=500),
+       M=hst.integers(min_value=4, max_value=250))
+def test_datasaver_arrayparams_lists(experiment, SpectrumAnalyzer, DAC, N, M):
+
+    lspec = SpectrumAnalyzer.listspectrum
+
+    meas = Measurement()
+
+    meas.register_parameter(lspec)
+    assert len(meas.parameters) == 2
+    assert meas.parameters[str(lspec)].depends_on == 'dummy_SA_Frequency'
+    assert meas.parameters[str(lspec)].type == 'numeric'
+    assert meas.parameters['dummy_SA_Frequency'].type == 'numeric'
+
+    # Now for a real measurement
+
+    meas = Measurement()
+
+    meas.register_parameter(DAC.ch1)
+    meas.register_parameter(lspec, setpoints=[DAC.ch1])
+
+    assert len(meas.parameters) == 3
+
+    lspec.npts = M
+
+    with meas.run() as datasaver:
+        for set_v in np.linspace(0, 0.01, N):
+            datasaver.add_result((DAC.ch1, set_v),
+                                 (lspec, lspec.get()))
+
+    assert datasaver.points_written == N*M
+
+
+@settings(max_examples=5, deadline=None)
+@given(N=hst.integers(min_value=5, max_value=500),
+       M=hst.integers(min_value=4, max_value=250))
+def test_datasaver_arrayparams_tuples(experiment, SpectrumAnalyzer, DAC, N, M):
+
+    tspec = SpectrumAnalyzer.tuplespectrum
+
+    meas = Measurement()
+
+    meas.register_parameter(tspec)
+    assert len(meas.parameters) == 2
+    assert meas.parameters[str(tspec)].depends_on == 'dummy_SA_Frequency'
+    assert meas.parameters[str(tspec)].type == 'numeric'
+    assert meas.parameters['dummy_SA_Frequency'].type == 'numeric'
+
+    # Now for a real measurement
+
+    meas = Measurement()
+
+    meas.register_parameter(DAC.ch1)
+    meas.register_parameter(tspec, setpoints=[DAC.ch1])
+
+    assert len(meas.parameters) == 3
+
+    tspec.npts = M
+
+    with meas.run() as datasaver:
+        for set_v in np.linspace(0, 0.01, N):
+            datasaver.add_result((DAC.ch1, set_v),
+                                 (tspec, tspec.get()))
+
+    assert datasaver.points_written == N*M
+
+
 def test_load_legacy_files_2D(experiment):
     location = 'fixtures/2018-01-17/#002_2D_test_15-43-14'
     dir = os.path.dirname(__file__)