Repository: microsoft/Qcodes

Issue:
Missing order of setpoints in the new dataset
Currently the retrieval of setpoint data relies on the order of the registered dependent paramenters. So for example the code below will fail for `depends_on_wrong` and pass for `depends_on_right`

```
import qcodes as qc
from qcodes import ParamSpec, new_data_set, new_experiment
from qcodes.dataset.database import initialise_database
from qcodes.dataset.data_export import get_data_by_id

initialise_database()
new_experiment('issue1239', 'test2')

indep1 = ParamSpec('indep1', "numeric")
indep2 = ParamSpec('indep2', "numeric")
depends_on_right = [indep1, indep2]
depends_on_wrong = [indep2, indep1]
dep = ParamSpec('response', "numeric", depends_on=depends_on_wrong)

dataset = new_data_set("results", specs=[indep1, indep2, dep])
dataset.add_result({'response': 1234,
                    'indep2': 2,
                    'indep1': 1 })
dataset.mark_complete()

d = get_data_by_id(dataset.run_id)
# test if data is correctly saved
for el in d[0]:
    if el['name'] == 'indep2':
        val = el['data']
        break
# val[0] is actually 1, i.e indep2 and indep1 are interchanged
assert val[0] == 2
```

In the failing case the values for the setpoint axes get interchanged, because the array of dependent ParamSpecs is reversed. 

This behavior is of course undesired and has to be resolved. 
In my opinion this problem roots all the way back to the `get_setpoints` method of `sqlite_base.py`, which returns the setpoints as a `List[List[List[Any]]]`, for a parameter specified by its name. I don't think that we should establish some standard order of the setpoints to resolve this problem. We should possibly rather return a dictionary with the setpoint parameter names as keys instead of one level of the lists.

The named function gets only used in this presented use case (i.e. `get_data_by_id`).

I will start implementing a fix that involves a dictionary return value for this purpose. 


Discussion:
fully agree. Beware though that @WilliamHPNielsen has started working on a new format for storing dependencies information in relation to the data model. Perhaps, you can align with him.
@astafan8, good point. See https://github.com/QCoDeS/Qcodes/pull/1227 for a description of what is about to change. Unfortunately, https://github.com/QCoDeS/Qcodes/pull/1227 requires https://github.com/QCoDeS/Qcodes/pull/1002 to be merged.
Thanks for pointing that out. It is good that we are all aware of those correlations. It seems to me that #1227 will require a reimplementation of `get_data_by_id` . This reimplementation should include the changes proposed here. I think implementing these changes already in the current version will not impose extra work.

Resolving patch:
diff --git a/qcodes/dataset/data_export.py b/qcodes/dataset/data_export.py
--- a/qcodes/dataset/data_export.py
+++ b/qcodes/dataset/data_export.py
@@ -78,24 +78,23 @@ def get_data_by_id(run_id: int) -> List:
 
         raw_setpoint_data = data.get_setpoints(data_axis['name'])
 
-        my_output = []
+        output_axes = []
 
         max_size = 0
-        for i, dependency in enumerate(dependencies):
+        for dependency in dependencies:
             axis: Dict[str, Union[str, np.ndarray]] = get_layout(conn,
                                                                  dependency[0])
 
-            mydata = flatten_1D_data_for_plot(raw_setpoint_data[i])
+            mydata = flatten_1D_data_for_plot(raw_setpoint_data[axis['name']])
             axis['data'] = mydata
 
             size = mydata.size
             if size > max_size:
                 max_size = size
 
-            my_output.append(axis)
+            output_axes.append(axis)
 
-        for i, dependency in enumerate(dependencies):
-            axis = my_output[i]
+        for axis in output_axes:
             size = axis['data'].size  # type: ignore
             if size < max_size:
                 if max_size % size != 0:
@@ -104,9 +103,9 @@ def get_data_by_id(run_id: int) -> List:
                                        f"of {max_size}")
                 axis['data'] = np.repeat(axis['data'], max_size//size)
 
-        my_output.append(data_axis)
+        output_axes.append(data_axis)
 
-        output.append(my_output)
+        output.append(output_axes)
     return output
 
 
diff --git a/qcodes/dataset/data_set.py b/qcodes/dataset/data_set.py
--- a/qcodes/dataset/data_set.py
+++ b/qcodes/dataset/data_set.py
@@ -622,7 +622,7 @@ def get_values(self, param_name: str) -> List[List[Any]]:
 
         return values
 
-    def get_setpoints(self, param_name: str) -> List[List[Any]]:
+    def get_setpoints(self, param_name: str) -> Dict[str, List[List[Any]]]:
         """
         Get the setpoints for the specified parameter
 
diff --git a/qcodes/dataset/sqlite_base.py b/qcodes/dataset/sqlite_base.py
--- a/qcodes/dataset/sqlite_base.py
+++ b/qcodes/dataset/sqlite_base.py
@@ -625,7 +625,7 @@ def get_values(conn: sqlite3.Connection,
 
 def get_setpoints(conn: sqlite3.Connection,
                   table_name: str,
-                  param_name: str) -> List[List[List[Any]]]:
+                  param_name: str) -> Dict[str, List[List[Any]]]:
     """
     Get the setpoints for a given dependent parameter
 
@@ -678,7 +678,7 @@ def get_setpoints(conn: sqlite3.Connection,
     setpoint_names = cast(List[str], setpoint_names)
 
     # get the actual setpoint data
-    output = []
+    output: Dict[str, List[List[Any]]] = {}
     for sp_name in setpoint_names:
         sql = f"""
         SELECT {sp_name}
@@ -687,7 +687,7 @@ def get_setpoints(conn: sqlite3.Connection,
         """
         c = atomic_transaction(conn, sql)
         sps = many_many(c, sp_name)
-        output.append(sps)
+        output[sp_name] = sps
 
     return output
 


Test patch:
diff --git a/qcodes/tests/dataset/test_dataset_basic.py b/qcodes/tests/dataset/test_dataset_basic.py
--- a/qcodes/tests/dataset/test_dataset_basic.py
+++ b/qcodes/tests/dataset/test_dataset_basic.py
@@ -451,9 +451,11 @@ def fb(xv, yv):
     assert dataset.get_values("a") == [[r["a"]] for r in results if "a" in r]
     assert dataset.get_values("b") == [[r["b"]] for r in results if "b" in r]
 
-    assert dataset.get_setpoints("a") == [[[xv] for xv in xvals]]
+    assert dataset.get_setpoints("a")['x'] == [[xv] for xv in xvals]
 
     tmp = [list(t) for t in zip(*(itertools.product(xvals, yvals)))]
     expected_setpoints = [[[v] for v in vals] for vals in tmp]
 
-    assert dataset.get_setpoints("b") == expected_setpoints
+    assert dataset.get_setpoints("b")['x'] == expected_setpoints[0]
+    assert dataset.get_setpoints("b")['y'] == expected_setpoints[1]
+
diff --git a/qcodes/tests/dataset/test_dataset_loading.py b/qcodes/tests/dataset/test_dataset_loading.py
--- a/qcodes/tests/dataset/test_dataset_loading.py
+++ b/qcodes/tests/dataset/test_dataset_loading.py
@@ -6,8 +6,10 @@
 import pytest
 
 import qcodes as qc
-from qcodes.dataset.data_set import new_data_set, load_by_id, load_by_counter
+from qcodes.dataset.data_set import (new_data_set, load_by_id, load_by_counter,
+                                     ParamSpec)
 from qcodes.dataset.database import initialise_database
+from qcodes.dataset.data_export import get_data_by_id
 from qcodes.dataset.experiment_container import new_experiment
 
 
@@ -27,6 +29,11 @@ def experiment(empty_temp_db):
     yield e
     e.conn.close()
 
+@pytest.fixture(scope='function')
+def dataset(experiment):
+    dataset = new_data_set("test-dataset")
+    yield dataset
+    dataset.conn.close()
 
 def test_load_by_id(experiment):
     ds = new_data_set("test-dataset")
@@ -145,3 +152,33 @@ def test_completed_timestamp_with_default_format(empty_temp_db):
     assert t_before_complete_secs \
            <= actual_completed_timestamp_raw \
            <= t_after_complete_secs + 1
+
+def test_get_data_by_id_order(dataset):
+    """
+    Test if the values of the setpoints/dependent parameters is dependent on
+    the order of the `depends_on` value. This sounds far fetch but was
+    actually the case before #1250.
+    """
+    indepA = ParamSpec('indep1', "numeric")
+    indepB = ParamSpec('indep2', "numeric")
+    depAB = ParamSpec('depAB', "numeric", depends_on=[indepA, indepB])
+    depBA = ParamSpec('depBA', "numeric", depends_on=[indepB, indepA])
+    dataset.add_parameters([indepA, indepB, depAB, depBA])
+
+    dataset.add_result({'depAB': 12,
+                        'indep2': 2,
+                        'indep1': 1})
+
+    dataset.add_result({'depBA': 21,
+                        'indep2': 2,
+                        'indep1': 1})
+    dataset.mark_complete()
+
+    data = get_data_by_id(dataset.run_id)
+    data_dict = {el['name']: el['data'] for el in data[0]}
+    assert data_dict['indep1'] == 1
+    assert data_dict['indep2'] == 2
+
+    data_dict = {el['name']: el['data'] for el in data[1]}
+    assert data_dict['indep1'] == 1
+    assert data_dict['indep2'] == 2