Repository: microsoft/Qcodes

Issue:
cannot Loop with VisaInstrument
When trying to perform a `Loop` in the background with a  `VisaInstrument` I get a pickling related error. (so this might be related to #53). The error message I get is `ValueError: ctypes objects containing pointers cannot be pickled`.

A minimal example is included below. I cannot see anything in the example that cannot be pickled, so I my best guess is that the `VisaInstrument` itself has issues.

``` python
from imp import reload
import time

import qcodes
import qcodes as qc

qc.set_mp_method('spawn')  # force Windows behavior on mac

from qcodes.instrument.visa import VisaInstrument
from qcodes import Instrument

class Keithley_2700(VisaInstrument):
    ''' This is a small version of the Keithley driver '''
    def __init__(self, name, address, reset=False, **kwargs):
        t0 = time.time()
        super().__init__(name, address, **kwargs)

        self.add_parameter('IDN', get_cmd='*IDN?')

        self.add_parameter('dummy',
                               get_cmd= self.dummy_get,
                               set_cmd= self.dummy_set,
                               units='s', )

        self.add_parameter('readnext',
                              units='arb.unit',
                              get_cmd=':DATA:FRESH?',
                              get_parser=float)

        t1 = time.time()
        print('Connected to: ',
              self.get('IDN').replace(',', ', ').replace('\n', ' '),
              'in %.2fs' % (t1-t0))

    def dummy_set(self, value, **kwargs):
        pass
    def dummy_get(self, **kwargs):
        return 0

if __name__ == '__main__':
    k1 = Keithley_2700('Keithley', 'GPIB0::15::INSTR', server_name=None )


    station = qc.Station(k1)
    station.set_measurement(k1.readnext)
    sweepvalues = k1.dummy[-10:10:1]

    data = qc.Loop(sweepvalues, delay=0.01).run(location=None, background=True)
```

Might be related to #53 



Discussion:
Yes, this is exactly #53, and is the whole reason for creating instrument servers. Aside from the logistical issues with pickling, there's a more fundamental issue: in as far as instrument objects hold state (which you want them to, for snapshot and other cases you just want the most recently set or measured value of some parameter) you should not be duplicating them, or the states will diverge. With the instrument server you have one real copy of the instrument, all the others just proxy it.

@alexcjohnson Ok, making at most one copy of an `Instrument` makes sense. The error messages are quite confusing though. In the long term we need logging on the server (so one can see error messages on that side, also see #98). In the short term, how about adding something like

``` python
    def __getstate__(self):
        raise Exception('QCodes instruments should not be pickled! see @@@')
        # normal operation
        return  self.__dict__
```

to the `Instrument class`?

@peendebak @alexcjohnson just randomly stumbled upon this one, I think it should be closed as in the documentation is (although not yet pushed)  clearly stated. 

Actually I quite like the idea of `__getstate__` returning a useful exception. Sorry for not looking at your comment earlier @peendebak - I'll add this in as a fix to this issue.


Resolving patch:
diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py
--- a/qcodes/instrument/base.py
+++ b/qcodes/instrument/base.py
@@ -93,6 +93,8 @@ def __init__(self, name, server_name=None, **kwargs):
 
         self._meta_attrs = ['name']
 
+        self._no_proxy_methods = {'__getstate__'}
+
         self.record_instance(self)
 
     def get_idn(self):
@@ -500,7 +502,8 @@ def _get_method_attrs(self):
             value = getattr(self, attr)
             if ((not callable(value)) or
                     value is self.parameters.get(attr) or
-                    value is self.functions.get(attr)):
+                    value is self.functions.get(attr) or
+                    attr in self._no_proxy_methods):
                 # Functions and Parameters are callable and they show up in
                 # dir(), but they have their own listing.
                 continue
@@ -508,3 +511,11 @@ def _get_method_attrs(self):
             out[attr] = ['__doc__'] if hasattr(value, '__doc__') else []
 
         return out
+
+    def __getstate__(self):
+        """Prevent pickling instruments, and give a nice error message."""
+        raise RuntimeError(
+            'qcodes Instruments should not be pickled. Likely this means you '
+            'were trying to use a local instrument (defined with '
+            'server_name=None) in a background Loop. Local instruments can '
+            'only be used in Loops with background=False.')


Test patch:
diff --git a/qcodes/tests/test_loop.py b/qcodes/tests/test_loop.py
--- a/qcodes/tests/test_loop.py
+++ b/qcodes/tests/test_loop.py
@@ -70,6 +70,7 @@ def check_loop_data(self, data):
 
     def test_background_and_datamanager(self):
         # make sure that an unpicklable instrument can indeed run in a loop
+        # because the instrument itself is in a server
 
         # TODO: if we don't save the dataset (location=False) then we can't
         # sync it when we're done. Should fix that - for now that just means
@@ -86,6 +87,20 @@ def test_background_and_datamanager(self):
         data.sync()
         self.check_loop_data(data)
 
+    def test_local_instrument(self):
+        # a local instrument should work in a foreground loop, but
+        # not in a background loop (should give a RuntimeError)
+        gates_local = MockGates(model=self.model, server_name=None)
+        c1 = gates_local.chan1
+        loop_local = Loop(c1[1:5:1], 0.001).each(c1)
+
+        with self.assertRaises(RuntimeError):
+            loop_local.run(location=self.location, quiet=True)
+
+        data = loop_local.run(location=self.location2, background=False,
+                              quiet=True)
+        self.check_loop_data(data)
+
     def test_background_no_datamanager(self):
         data = self.loop.run(location=self.location, data_manager=False,
                              quiet=True)