Repository: DiCarloLab-Delft/PycQED_py3

Issue:
Instrument monitor folds on update
First reported by @fluthi , Makes it quite annoying to effectively use this in a real life scenario. 
Instrument monitor folds on every update. 

Comments:

Patch:
diff --git a/examples/Setting up a basic measurement using the MC.ipynb b/examples/Setting up a basic measurement using the MC.ipynb
index 95f8b96a..69639e6f 100644
--- a/examples/Setting up a basic measurement using the MC.ipynb
+++ b/examples/Setting up a basic measurement using the MC.ipynb
@@ -7,18 +7,12 @@
     "collapsed": false
    },
    "outputs": [
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "WARNING:root:Setting datadir to default location\n"
-     ]
-    },
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Data directory set to: None\n"
+      "Data directory set to: /Users/Adriaan/Documents/Testing/Data\n",
+      "Could not import msvcrt (used for detecting keystrokes)\n"
      ]
     }
    ],
@@ -71,15 +65,7 @@
    "metadata": {
     "collapsed": false
    },
-   "outputs": [
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "WARNING:root:Error getting or interpreting *IDN?: ''\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "from pycqed.instrument_drivers.virtual_instruments import instrument_monitor as im \n",
     "IM = im.InstrumentMonitor('IM', station)\n",
@@ -88,6 +74,17 @@
     "MC.instrument_monitor('IM')"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [],
+   "source": [
+    "IM.update()"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -98,9 +95,10 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 5,
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "scrolled": true
    },
    "outputs": [
     {
@@ -109,7 +107,7 @@
        "'dummy_instrument'"
       ]
      },
-     "execution_count": 4,
+     "execution_count": 5,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -130,7 +128,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 6,
    "metadata": {
     "collapsed": false,
     "scrolled": false
@@ -166,7 +164,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 7,
    "metadata": {
     "collapsed": false
    },
@@ -178,37 +176,7 @@
       "Starting measurement: dummy_hard persistent\n",
       "Sweep function: None_Sweep\n",
       "Detector function: Dummy_Detector\n",
-      " 100% completed \telapsed time: 1.4s \ttime left: 0.0s\n",
-      "[[  0.00000000e+00  -8.41805618e-03   9.52221751e-01]\n",
-      " [  3.44827592e-01   5.64679876e-02   9.63823080e-01]\n",
-      " [  6.89655185e-01   2.71647215e-01   9.90496159e-01]\n",
-      " [  1.03448272e+00   3.58638942e-01   9.83597755e-01]\n",
-      " [  1.37931037e+00   4.11285490e-01   8.58026862e-01]\n",
-      " [  1.72413790e+00   4.77858633e-01   7.64453173e-01]\n",
-      " [  2.06896544e+00   6.31779075e-01   7.80045271e-01]\n",
-      " [  2.41379309e+00   6.28673434e-01   7.12422073e-01]\n",
-      " [  2.75862074e+00   8.28549922e-01   6.89963996e-01]\n",
-      " [  3.10344839e+00   8.08600903e-01   5.54980338e-01]\n",
-      " [  3.44827580e+00   8.77372682e-01   4.91556764e-01]\n",
-      " [  3.79310346e+00   9.23762381e-01   4.19212967e-01]\n",
-      " [  4.13793087e+00   9.73725736e-01   2.85644293e-01]\n",
-      " [  4.48275852e+00   9.79517102e-01   7.03447387e-02]\n",
-      " [  4.82758617e+00   1.05004179e+00   5.40984496e-02]\n",
-      " [  5.17241383e+00   1.08093548e+00  -8.87244269e-02]\n",
-      " [  5.51724148e+00   1.00360012e+00  -1.67943716e-01]\n",
-      " [  5.86206913e+00   9.49792027e-01  -3.16440761e-01]\n",
-      " [  6.20689678e+00   9.28232491e-01  -4.36341375e-01]\n",
-      " [  6.55172396e+00   9.33600187e-01  -4.78987128e-01]\n",
-      " [  6.89655161e+00   7.27082551e-01  -6.07001722e-01]\n",
-      " [  7.24137926e+00   7.72163808e-01  -6.35885179e-01]\n",
-      " [  7.58620691e+00   6.45901322e-01  -7.35407114e-01]\n",
-      " [  7.93103456e+00   5.10887504e-01  -9.08347905e-01]\n",
-      " [  8.27586174e+00   4.88061309e-01  -7.98481047e-01]\n",
-      " [  8.62068939e+00   4.13446218e-01  -9.27012920e-01]\n",
-      " [  8.96551704e+00   3.15337449e-01  -1.00892162e+00]\n",
-      " [  9.31034470e+00   1.65451705e-01  -1.01685059e+00]\n",
-      " [  9.65517235e+00   1.13537371e-01  -1.01554918e+00]\n",
-      " [  1.00000000e+01  -4.30937521e-02  -9.94131565e-01]]\n"
+      " 100% completed \telapsed time: 1.5s \ttime left: 0.0s\n"
      ]
     }
    ],
@@ -218,8 +186,7 @@
     "MC.set_sweep_points(np.linspace(0, 10, 30))\n",
     "MC.set_detector_function(det.Dummy_Detector_Hard(noise=0.5, delay=.02))\n",
     "dat2 = MC.run('dummy_hard persistent')\n",
-    "data_set2 = dat2['dset']\n",
-    "print(data_set2)"
+    "data_set2 = dat2['dset']"
    ]
   },
   {
@@ -230,24 +197,6 @@
     "A soft measurement is a a measurement where the data acquisition loop occurs in the **soft**ware"
    ]
   },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
-   "outputs": [],
-   "source": []
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
-   "outputs": [],
-   "source": []
-  },
   {
    "cell_type": "code",
    "execution_count": 8,
@@ -274,11 +223,11 @@
       "Starting measurement: 1D test\n",
       "Sweep function: x\n",
       "Detector function: parabola\n",
-      " 100% completed \telapsed time: 6.6s \ttime left: 0.0s\n",
+      " 100% completed \telapsed time: 6.3s \ttime left: 0.0s\n",
       "Starting measurement: 1D test-persist\n",
       "Sweep function: x\n",
       "Detector function: parabola\n",
-      " 100% completed \telapsed time: 7.5s \ttime left: 0.0s\n"
+      " 100% completed \telapsed time: 6.5s \ttime left: 0.0s\n"
      ]
     }
    ],
@@ -331,7 +280,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 11,
    "metadata": {
     "collapsed": false
    },
@@ -344,7 +293,7 @@
       "Sweep function 0: x\n",
       "Sweep function 1: Sweep_function\n",
       "Detector function: parabola\n",
-      " 100% completed \telapsed time: 5.2s \ttime left: 0.0ss\n"
+      " 100% completed \telapsed time: 3.9s \ttime left: 0.0s\n"
      ]
     }
    ],
@@ -374,7 +323,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 12,
    "metadata": {
     "collapsed": false
    },
@@ -387,7 +336,7 @@
       "Sweep function 0: None_Sweep\n",
       "Sweep function 1: None_Sweep\n",
       "Detector function: Dummy_Detector\n",
-      " 100% completed \telapsed time: 14.0s \ttime left: 0.0s\n"
+      " 100% completed \telapsed time: 11.4s \ttime left: 0.0s\n"
      ]
     }
    ],
@@ -424,7 +373,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 13,
    "metadata": {
     "collapsed": false
    },
@@ -436,7 +385,7 @@
       "Starting measurement: dummy_hard\n",
       "Sweep function: None_Sweep\n",
       "Detector function: Dummy_Detector\n",
-      " 100% completed \telapsed time: 1.1s \ttime left: 0.0s\n"
+      " 100% completed \telapsed time: 0.9s \ttime left: 0.0s\n"
      ]
     }
    ],
@@ -459,7 +408,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 16,
    "metadata": {
     "collapsed": false
    },
@@ -472,7 +421,7 @@
       "Sweep function 0: None_Sweep\n",
       "Sweep function 1: None_Sweep\n",
       "Detector function: Dummy_Detector\n",
-      " 100% completed \telapsed time: 16.7s \ttime left: 0.0s\n"
+      " 100% completed \telapsed time: 13.6s \ttime left: 0.0s\n"
      ]
     }
    ],
@@ -501,7 +450,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 15,
    "metadata": {
     "collapsed": false
    },
@@ -514,7 +463,7 @@
       "Sweep function 0: module\n",
       "Sweep function 1: module\n",
       "Detector function: parabola\n",
-      "Optimization completed in 1.564s\n"
+      "Optimization completed in 1.357s\n"
      ]
     }
    ],
@@ -530,15 +479,6 @@
     "dat = MC.run('1D test', mode='adaptive')\n",
     "data_set = dat['dset']"
    ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "collapsed": true
-   },
-   "outputs": [],
-   "source": []
   }
  ],
  "metadata": {
diff --git a/pycqed/analysis/tools/plotting.py b/pycqed/analysis/tools/plotting.py
index 72814d22..35b3b62f 100644
--- a/pycqed/analysis/tools/plotting.py
+++ b/pycqed/analysis/tools/plotting.py
@@ -85,6 +85,30 @@ def SI_prefix_and_scale_factor(val, unit=None):
     return scale_factor, unit
 
 
+def SI_val_to_msg_str(val: float, unit: str=None):
+    """
+    Takes in a value  with optional unit and returns a string tuple consisting
+    of (value_str, unit) where the value and unit are rescaled according to
+    SI prefixes.
+    """
+    validtypes = (float, int, np.integer, np.floating)
+    if unit in SI_UNITS and isinstance(val, validtypes):
+        if val == 0:
+            prefix_power = 0
+        else:
+            # The defined prefixes go down to -24 but this is below
+            # the numerical precision of python
+            prefix_power = np.clip(-15, (np.log10(abs(val))//3 * 3), 24)
+        # Determine SI prefix, number 8 corresponds to no prefix
+        SI_prefix_idx = int(prefix_power/3 + 8)
+        prefix = SI_PREFIXES[SI_prefix_idx]
+        # Convert the unit
+        val = val*10**-prefix_power
+        unit = prefix+unit
+
+    value_str = str(val)
+    return value_str, unit
+  
 def data_to_table_png(data: list, filename: str, title: str='',
                       close_fig: bool=True):
     """
@@ -110,7 +134,7 @@ def data_to_table_png(data: list, filename: str, title: str='',
     if close_fig:
         plt.close(fig)
 
-
+        
 def annotate_point_pair(ax, text, xy_start, xy_end, xycoords='data',
                         text_offset=(-10, -5), arrowprops=None, **kw):
     '''
diff --git a/pycqed/instrument_drivers/virtual_instruments/ins_mon/qc_snapshot_widget.py b/pycqed/instrument_drivers/virtual_instruments/ins_mon/qc_snapshot_widget.py
index 4392940c..49f38825 100644
--- a/pycqed/instrument_drivers/virtual_instruments/ins_mon/qc_snapshot_widget.py
+++ b/pycqed/instrument_drivers/virtual_instruments/ins_mon/qc_snapshot_widget.py
@@ -1,15 +1,6 @@
 # -*- coding: utf-8 -*-
 from pyqtgraph.Qt import QtGui
-from pycqed.analysis.tools.plotting import SI_UNITS, SI_PREFIXES
-import types
-import traceback
-import numpy as np
-
-try:
-    import metaarray
-    HAVE_METAARRAY = True
-except Exception:
-    HAVE_METAARRAY = False
+from pycqed.analysis.tools.plotting import SI_val_to_msg_str
 
 
 class QcSnaphotWidget(QtGui.QTreeWidget):
@@ -25,10 +16,12 @@ class QcSnaphotWidget(QtGui.QTreeWidget):
         self.setData(data)
         self.setColumnCount(4)
         self.setHeaderLabels(['Name', 'Value', 'Unit', 'Last update'])
+        self.nodes = {}
 
     def setData(self, data):
-        """data should be a QCoDes snapshot of a station."""
-        self.clear()
+        """
+        data should be a QCoDes snapshot of a station.
+        """
         self.buildTreeSnapshot(snapshot=data)
         self.resizeColumnToContents(0)
 
@@ -40,72 +33,35 @@ class QcSnaphotWidget(QtGui.QTreeWidget):
 
         for ins in sorted(snapshot.keys()):
             ins_snapshot = snapshot[ins]
-            node = QtGui.QTreeWidgetItem([ins, "", ""])
-            parent.addChild(node)
+            if ins not in self.nodes:
+                self.nodes[ins] = QtGui.QTreeWidgetItem([ins, "", ""])
+                parent.addChild(self.nodes[ins])
+
+            node = self.nodes[ins]
             for par_name in sorted(ins_snapshot['parameters'].keys()):
                 par_snap = ins_snapshot['parameters'][par_name]
                 # Depending on the type of data stored in value do different
-                # things.
+                # things, currently only blocks non-dicts
                 if not isinstance(par_snap['value'], dict):
-                    value_str, unit = par_val_to_msg(par_snap['value'],
-                                                     par_snap['unit'])
+                    value_str, unit = SI_val_to_msg_str(par_snap['value'],
+                                                        par_snap['unit'])
 
                     # Omits printing of the date to make it more readable
                     if par_snap['ts'] is not None:
                         latest_str = par_snap['ts'][11:]
                     else:
                         latest_str = ''
-                    par_node = QtGui.QTreeWidgetItem(
-                        [par_name, value_str, unit, latest_str])
-                    node.addChild(par_node)
-
-    def buildTree(self, data, parent, name='', hideRoot=False):
-        if hideRoot:
-            node = parent
-        else:
-            typeStr = type(data).__name__
-            if typeStr == 'instance':
-                typeStr += ": " + data.__class__.__name__
-            node = QtGui.QTreeWidgetItem([name, typeStr, ""])
-            parent.addChild(node)
-
-        # convert traceback to a list of strings
-        if isinstance(data, types.TracebackType):
-            data = list(
-                map(str.strip, traceback.format_list(
-                    traceback.extract_tb(data))))
-        elif HAVE_METAARRAY and (hasattr(data, 'implements')
-                                 and data.implements('MetaArray')):
-            data = {
-                'data': data.view(np.ndarray),
-                'meta': data.infoCopy()
-            }
-
-        if isinstance(data, dict):
-            for k in data.keys():
-                self.buildTree(data[k], node, str(k))
-        elif isinstance(data, list) or isinstance(data, tuple):
-            for i, d in enumerate(data):
-                self.buildTree(d, node, str(i))
-        else:
-            node.setText(2, str(data))
-
-
-def par_val_to_msg(val, unit=None):
-    validtypes = (float, int, np.integer, np.floating)
-    if unit in SI_UNITS and isinstance(val, validtypes):
-        if val == 0:
-            prefix_power = 0
-        else:
-            # The defined prefixes go down to -24 but this is below
-            # the numerical precision of python
-            prefix_power = np.clip(-15, (np.log10(abs(val))//3 * 3), 24)
-        # Determine SI prefix, number 8 corresponds to no prefix
-        SI_prefix_idx = int(prefix_power/3 + 8)
-        prefix = SI_PREFIXES[SI_prefix_idx]
-        # Convert the unit
-        val = val*10**-prefix_power
-        unit = prefix+unit
 
-    value_str = str(val)
-    return value_str, unit
+                    # Name of the node in the self.nodes dictionary
+                    param_node_name = '{}.{}'.format(ins, par_name)
+                    # If node does not yet exist, create a node
+                    if param_node_name not in self.nodes:
+                        param_node = QtGui.QTreeWidgetItem(
+                            [par_name, value_str, unit, latest_str])
+                        node.addChild(param_node)
+                        self.nodes[param_node_name] = param_node
+                    else:  # else update existing node
+                        param_node = self.nodes[param_node_name]
+                        param_node.setData(1, 0, value_str)
+                        param_node.setData(2, 0, unit)
+                        param_node.setData(3, 0, latest_str)
diff --git a/pycqed/instrument_drivers/virtual_instruments/instrument_monitor.py b/pycqed/instrument_drivers/virtual_instruments/instrument_monitor.py
index 12bf89f8..980a97b9 100644
--- a/pycqed/instrument_drivers/virtual_instruments/instrument_monitor.py
+++ b/pycqed/instrument_drivers/virtual_instruments/instrument_monitor.py
@@ -46,7 +46,6 @@ class InstrumentMonitor(Instrument):
             snapshot = self.station.snapshot()
             self.tree.setData(snapshot['instruments'])
 
-
     def _init_qt(self):
         # starting the process for the pyqtgraph plotting
         # You do not want a new process to be created every time you start a
@@ -57,7 +56,6 @@ class InstrumentMonitor(Instrument):
         ins_mon_mod = 'pycqed.instrument_drivers.virtual_instruments.ins_mon.qc_snapshot_widget'
         self.__class__.rpg = self.proc._import(ins_mon_mod)
 
-
     def create_tree(self, figsize=(1000, 600)):
 
         self.tree = self.rpg.QcSnaphotWidget()


Test patch:
diff --git a/pycqed/tests/test_unit_conversions.py b/pycqed/tests/test_unit_conversions.py
index b43958d4..0a8dd82d 100644
--- a/pycqed/tests/test_unit_conversions.py
+++ b/pycqed/tests/test_unit_conversions.py
@@ -3,6 +3,7 @@ import numpy as np
 import matplotlib.pyplot as plt
 from pycqed.analysis.tools.plotting import SI_prefix_and_scale_factor
 from pycqed.analysis.tools.plotting import set_xlabel, set_ylabel
+from pycqed.analysis.tools.plotting import SI_val_to_msg_str
 
 
 class Test_SI_prefix_scale_factor(unittest.TestCase):
@@ -56,3 +57,10 @@ class test_SI_unit_aware_labels(unittest.TestCase):
         ytick_label_vals = [float(yl.get_text()) for yl in ytick_labels]
         for yt, yl in zip(yticks, ytick_label_vals):
             self.assertAlmostEqual(yt*1e6, yl)
+
+    def test_SI_val_to_msg_str(self):
+        val, unit = SI_val_to_msg_str(1030, 'm')
+        self.assertEqual(val, str(1.03))
+        self.assertEqual(unit, 'km')
+
+