Repository: microsoft/Qcodes

Issue:
Performing multiple tasks/measurements with Combined parameter breaks setpoints
### Steps to reproduce
```python
import numpy as np

import qcodes as qc
from qcodes.tests.instrument_mocks import DummyInstrument
dac = DummyInstrument(name="dac", gates=['ch1', 'ch2'])  # The DAC voltage source

cp = qc.combine(dac.ch1, dac.ch2, name='combined_dac')

def printsomething():
    print("something")

def printelse():
    print("else")

printtask = qc.Task(printsomething)
printelse = qc.Task(printelse)

chan1_set = np.linspace(0, 10, 11).reshape(11,1)
chan2_set = np.linspace(0, 5, 11).reshape(11,1)
combine_setpoints = np.hstack((chan1_set, chan2_set))
loop = qc.Loop(cp.sweep(combine_setpoints), 0.1).each(printtask , printelse)
print(data.dac_ch1)
print(data.dac_ch2)
```
or
```python
import numpy as np

import qcodes as qc
from qcodes.tests.instrument_mocks import DummyInstrument

dac = DummyInstrument(name="dac", gates=['ch1', 'ch2'])  # The DAC voltage source
dmm = DummyInstrument(name="dmm", gates=['voltage'])  # The DMM voltage reader
import random
dmm.voltage.get =  lambda: random.randint(0, 100)

cp = qc.combine(dac.ch1, dac.ch2, name='combined_dac')

chan1_set = np.linspace(0, 10, 11).reshape(11,1)
chan2_set = np.linspace(0, 5, 11).reshape(11,1)
combine_setpoints = np.hstack((chan1_set, chan2_set))
loop = qc.Loop(cp.sweep(combine_setpoints), 0.1).each(dmm.voltage, dmm.voltage)

print(data.dac_ch1)
print(data.dac_ch2)
```
### Expected behaviour

measurements should work

### Actual behaviour
In the first case an error is raised
```
Traceback (most recent call last):
  File "C:/Users/Jens/Majorana/combined_example.py", line 31, in <module>
    data = loop.run()
  File "C:\Users\Jens\Qcodes\qcodes\loops.py", line 735, in run
    self._run_wrapper()
  File "C:\Users\Jens\Qcodes\qcodes\loops.py", line 784, in _run_wrapper
    self._run_loop(*args, **kwargs)
  File "C:\Users\Jens\Qcodes\qcodes\loops.py", line 840, in _run_loop
    set_name = (self.data_set.action_id_map[set_index])
KeyError: (1,)
```
Furthermore the setpoints stored in dac_ch1 and dac_ch2 are wrong
(first array contains values from 2nd and 2en empty)

### System
Windows 
QCodes master



Discussion:
@jenshnielsen we ran into the same...
in loops.py this:

``` Python
            if hasattr(self.sweep_values, "parameters"):
                set_name = self.data_set.action_id_map[action_indices]
                if hasattr(self.sweep_values, 'aggregate'):
                    value = self.sweep_values.aggregate(*set_val)
                self.data_set.store(new_indices, {set_name: value})
                # for j, val in enumerate(set_val):
                #     set_index = action_indices + (j+1, )
                #     set_name = (self.data_set.action_id_map[set_index])
                #     data_to_store[set_name] = val
            else:
                set_name = self.data_set.action_id_map[action_indices]
                data_to_store[set_name] = value
```

made me forget the issue 😮 
there is something wrong with that indexing, that `j` is just randomly (actually its not random...) counting and has nothing to do with the actual action index that parameter got.
Yes that also made me want to forget it ;) 
@MerlinSmiles noticed that this leads to unsaved data. This happens because this bug results in lines that contains nan's and the match_save_range check  https://github.com/QCoDeS/Qcodes/blob/master/qcodes/data/gnuplot_format.py#L269 only writes complete lines 
lol great that you found the reason, I didnt figure that part out

Resolving patch:
diff --git a/qcodes/loops.py b/qcodes/loops.py
--- a/qcodes/loops.py
+++ b/qcodes/loops.py
@@ -441,7 +441,7 @@ def containers(self):
         data_arrays = [loop_array]
         # hack set_data into actions
         new_actions = self.actions[:]
-        if hasattr(self.sweep_values, "parameters"):
+        if hasattr(self.sweep_values, "parameters"): # combined parameter
             for parameter in self.sweep_values.parameters:
                 new_actions.append(parameter)
 
@@ -731,7 +731,7 @@ def run(self, use_threads=False, quiet=False, station=None,
 
         try:
             if not quiet:
-            	print(datetime.now().strftime('Started at %Y-%m-%d %H:%M:%S'))
+                print(datetime.now().strftime('Started at %Y-%m-%d %H:%M:%S'))
             self._run_wrapper()
             ds = self.data_set
         finally:
@@ -811,7 +811,12 @@ def _run_loop(self, first_delay=0, action_indices=(),
         delay = max(self.delay, first_delay)
 
         callables = self._compile_actions(self.actions, action_indices)
-
+        n_callables = 0
+        for item in callables:
+            if hasattr(item, 'param_ids'):
+                n_callables += len(item.param_ids)
+            else:
+                n_callables += 1
         t0 = time.time()
         last_task = t0
         imax = len(self.sweep_values)
@@ -830,13 +835,13 @@ def _run_loop(self, first_delay=0, action_indices=(),
             new_values = current_values + (value,)
             data_to_store = {}
 
-            if hasattr(self.sweep_values, "parameters"):
+            if hasattr(self.sweep_values, "parameters"): # combined parameter
                 set_name = self.data_set.action_id_map[action_indices]
                 if hasattr(self.sweep_values, 'aggregate'):
                     value = self.sweep_values.aggregate(*set_val)
                 self.data_set.store(new_indices, {set_name: value})
-                for j, val in enumerate(set_val):
-                    set_index = action_indices + (j+1, )
+                for j, val in enumerate(set_val): # set_val list of values to set [param1_setpoint, param2_setpoint ..]
+                    set_index = action_indices + (j+n_callables, )
                     set_name = (self.data_set.action_id_map[set_index])
                     data_to_store[set_name] = val
             else:


Test patch:
diff --git a/qcodes/tests/test_combined_loop.py b/qcodes/tests/test_combined_loop.py
new file mode 100644
--- /dev/null
+++ b/qcodes/tests/test_combined_loop.py
@@ -0,0 +1,197 @@
+from unittest import TestCase
+import numpy as np
+from hypothesis import given, settings
+import hypothesis.strategies as hst
+
+
+from .instrument_mocks import DummyInstrument
+from qcodes.instrument.parameter import combine
+from qcodes import Task, Loop
+from qcodes.instrument.parameter import ManualParameter
+
+
+class TestLoopCombined(TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.dmm = DummyInstrument(name="dmm", gates=['voltage', 'somethingelse'])
+        cls.dmm.somethingelse.get = lambda: 1
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.dmm.close()
+        del cls.dmm
+
+    @given(npoints=hst.integers(2, 100),
+           x_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted),
+           y_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted),
+           z_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted))
+    @settings(max_examples=10)
+    def testLoopCombinedParameterPrintTask(self, npoints, x_start_stop, y_start_stop, z_start_stop):
+
+        x_set = np.linspace(x_start_stop[0], x_start_stop[1], npoints)
+        y_set = np.linspace(y_start_stop[0], y_start_stop[1], npoints)
+        z_set = np.linspace(z_start_stop[0], z_start_stop[1], npoints)
+        setpoints = np.hstack((x_set.reshape(npoints, 1),
+                               y_set.reshape(npoints, 1),
+                               z_set.reshape(npoints, 1)))
+
+        parameters = [ManualParameter(name) for name in ["X", "Y", "Z"]]
+
+        sweep_values = combine(*parameters,
+                               name="combined").sweep(setpoints)
+        def ataskfunc():
+            a = 1+1
+
+        def btaskfunc():
+            b = 1+2
+
+        atask = Task(ataskfunc)
+        btask = Task(btaskfunc)
+
+        loop = Loop(sweep_values).each(atask, btask)
+        data = loop.run(quiet=True)
+        np.testing.assert_array_equal(data.arrays['X'].ndarray, x_set)
+        np.testing.assert_array_equal(data.arrays['Y'].ndarray, y_set)
+        np.testing.assert_array_equal(data.arrays['Z'].ndarray, z_set)
+
+    @given(npoints=hst.integers(2, 100),
+           x_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted),
+           y_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted),
+           z_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted))
+    @settings(max_examples=10)
+    def testLoopCombinedParameterTwice(self, npoints, x_start_stop, y_start_stop, z_start_stop):
+        x_set = np.linspace(x_start_stop[0], x_start_stop[1], npoints)
+        y_set = np.linspace(y_start_stop[0], y_start_stop[1], npoints)
+        z_set = np.linspace(z_start_stop[0], z_start_stop[1], npoints)
+        setpoints = np.hstack((x_set.reshape(npoints, 1),
+                               y_set.reshape(npoints, 1),
+                               z_set.reshape(npoints, 1)))
+        parameters = [ManualParameter(name) for name in ["X", "Y", "Z"]]
+        sweep_values = combine(*parameters,
+                               name="combined").sweep(setpoints)
+
+        def wrapper():
+            counter = 0
+
+            def inner():
+                nonlocal counter
+                counter += 1
+                return counter
+
+            return inner
+
+        self.dmm.voltage.get = wrapper()
+        loop = Loop(sweep_values).each(self.dmm.voltage, self.dmm.voltage)
+        data = loop.run(quiet=True)
+        np.testing.assert_array_equal(data.arrays['X'].ndarray, x_set)
+        np.testing.assert_array_equal(data.arrays['Y'].ndarray, y_set)
+        np.testing.assert_array_equal(data.arrays['Z'].ndarray, z_set)
+        np.testing.assert_array_equal(data.arrays['dmm_voltage_0'].ndarray, np.arange(1, npoints*2, 2))
+        np.testing.assert_array_equal(data.arrays['dmm_voltage_1'].ndarray, np.arange(2, npoints*2+1, 2))
+
+    @given(npoints=hst.integers(2, 100),
+           x_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted),
+           y_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted),
+           z_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted))
+    @settings(max_examples=10)
+    def testLoopCombinedParameterAndMore(self, npoints, x_start_stop, y_start_stop, z_start_stop):
+        x_set = np.linspace(x_start_stop[0], x_start_stop[1], npoints)
+        y_set = np.linspace(y_start_stop[0], y_start_stop[1], npoints)
+        z_set = np.linspace(z_start_stop[0], z_start_stop[1], npoints)
+        setpoints = np.hstack((x_set.reshape(npoints, 1),
+                               y_set.reshape(npoints, 1),
+                               z_set.reshape(npoints, 1)))
+        parameters = [ManualParameter(name) for name in ["X", "Y", "Z"]]
+        sweep_values = combine(*parameters,
+                               name="combined").sweep(setpoints)
+
+        def wrapper():
+            counter = 0
+
+            def inner():
+                nonlocal counter
+                counter += 1
+                return counter
+
+            return inner
+
+        self.dmm.voltage.get = wrapper()
+        loop = Loop(sweep_values).each(self.dmm.voltage, self.dmm.somethingelse, self.dmm.voltage)
+        data = loop.run(quiet=True)
+        np.testing.assert_array_equal(data.arrays['X'].ndarray, x_set)
+        np.testing.assert_array_equal(data.arrays['Y'].ndarray, y_set)
+        np.testing.assert_array_equal(data.arrays['Z'].ndarray, z_set)
+        np.testing.assert_array_equal(data.arrays['dmm_voltage_0'].ndarray, np.arange(1, npoints * 2, 2))
+        np.testing.assert_array_equal(data.arrays['dmm_somethingelse'].ndarray, np.ones(npoints))
+        np.testing.assert_array_equal(data.arrays['dmm_voltage_2'].ndarray, np.arange(2, npoints * 2 + 1, 2))
+
+    @given(npoints=hst.integers(2, 100),
+           npoints_outer=hst.integers(2,100),
+           x_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted),
+           y_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted),
+           z_start_stop=hst.lists(hst.integers(min_value=-800, max_value=400),
+                                  min_size=2, max_size=2, unique=True).map(sorted))
+    @settings(max_examples=10)
+    def testLoopCombinedParameterInside(self, npoints, npoints_outer, x_start_stop, y_start_stop, z_start_stop):
+        x_set = np.linspace(x_start_stop[0], x_start_stop[1], npoints_outer)
+        y_set = np.linspace(y_start_stop[0], y_start_stop[1], npoints)
+        z_set = np.linspace(z_start_stop[0], z_start_stop[1], npoints)
+
+        setpoints = np.hstack((y_set.reshape(npoints, 1),
+                               z_set.reshape(npoints, 1)))
+
+        parameters = [ManualParameter(name) for name in ["X", "Y", "Z"]]
+        sweep_values = combine(parameters[1], parameters[2],
+                               name="combined").sweep(setpoints)
+
+        def ataskfunc():
+            a = 1+1
+
+        def btaskfunc():
+            b = 1+2
+
+        atask = Task(ataskfunc)
+        btask = Task(btaskfunc)
+
+
+        def wrapper():
+            counter = 0
+
+            def inner():
+                nonlocal counter
+                counter += 1
+                return counter
+
+            return inner
+
+        self.dmm.voltage.get = wrapper()
+        loop = Loop(parameters[0].sweep(x_start_stop[0],
+                                     x_start_stop[1],
+                                     num=npoints_outer)).loop(sweep_values).each(self.dmm.voltage,
+                                                                                 atask,
+                                                                                 self.dmm.somethingelse,
+                                                                                 self.dmm.voltage,
+                                                                                 btask)
+        data = loop.run(quiet=True)
+        np.testing.assert_array_equal(data.arrays['X_set'].ndarray, x_set)
+        np.testing.assert_array_equal(data.arrays['Y'].ndarray,
+                                      np.repeat(y_set.reshape(1,npoints), npoints_outer, axis=0))
+        np.testing.assert_array_equal(data.arrays['Z'].ndarray,
+                                      np.repeat(z_set.reshape(1, npoints), npoints_outer, axis=0))
+
+        np.testing.assert_array_equal(data.arrays['dmm_voltage_0'].ndarray,
+                                      np.arange(1, npoints * npoints_outer* 2, 2).reshape(npoints_outer, npoints))
+        np.testing.assert_array_equal(data.arrays['dmm_voltage_3'].ndarray,
+                                      np.arange(2, npoints * npoints_outer* 2 + 1, 2).reshape(npoints_outer, npoints))
+        np.testing.assert_array_equal(data.arrays['dmm_somethingelse'].ndarray, np.ones((npoints_outer, npoints)))
diff --git a/qcodes/tests/test_combined_par.py b/qcodes/tests/test_combined_par.py
--- a/qcodes/tests/test_combined_par.py
+++ b/qcodes/tests/test_combined_par.py
@@ -4,6 +4,9 @@
 from unittest.mock import patch
 from unittest.mock import call
 
+from hypothesis import given
+import hypothesis.strategies as hst
+
 import numpy as np
 
 from qcodes.instrument.parameter import combine
@@ -72,8 +75,17 @@ def testSet(self):
                 ]
             )
 
-    def testAggregator(self):
-        setpoints = np.array([[1, 1, 1], [1, 1, 1]])
+
+    @given(npoints=hst.integers(1, 100),
+           x_start_stop=hst.lists(hst.integers(), min_size=2, max_size=2).map(sorted),
+           y_start_stop=hst.lists(hst.integers(), min_size=2, max_size=2).map(sorted),
+           z_start_stop=hst.lists(hst.integers(), min_size=2, max_size=2).map(sorted))
+    def testAggregator(self, npoints, x_start_stop, y_start_stop, z_start_stop):
+
+        x_set = np.linspace(x_start_stop[0], x_start_stop[1], npoints).reshape(npoints, 1)
+        y_set = np.linspace(y_start_stop[0], y_start_stop[1], npoints).reshape(npoints, 1)
+        z_set = np.linspace(z_start_stop[0], z_start_stop[1], npoints).reshape(npoints, 1)
+        setpoints = np.hstack((x_set, y_set, z_set))
         expected_results = [linear(*set) for set in setpoints]
         sweep_values = combine(*self.parameters,
                                name="combined",