Repository: microsoft/Qcodes

Issue:
on/off/0/1/True/False
I understand it's been hard to enforce a convention for this and it is different across drivers, but could we at least converge on a recommended one and enforce that as much as possible? 

Possibly this would be at least partly resolved (and also make it backwards compatible) to have a parser so that at least setting 'True' or 'on' or '1' would all map to the expected instrument setting . I understand that what the get function should return is a bit trickier (as I would argue for 1 and 0 but I also realise that could be problematic if an instrument has more settings than on and off; maybe we could make a decision on this and then enforce it though, can't be worse than the mix of things that currently exists?)


Discussion:
+1 to this. 

Even if it is an edict from the core developers it would be quite helpful to have some clarity on this while writing drivers. 

In addition, a parameter class that is in the qcodes base that can handle different variants of `1`/`0`, `'on'`/`'off'`, `True`/`False`, as opposed to several implementations that exist per driver would be quite helpful.
IMHO all these should use a bool validator but at the moment they do not. 
We may need to generalize the validator a bit to make it more flexible 
My two cents align with @spauka and @nataliejpg , we should have a name for such a thing (a flickswitch?) and have it live higher up. It should accept `True`, `'on'`, `'ON'`, and `1` as the same thing. 
further to this I was wondering where these sorts of functions should/do live? I've discovered williams   newlinestripper in the AWG5014 driver and think the same thing should also be in a couple of other drivers but at this point I feel I should pull it out of the awg driver and make them all use the same get_parser, just not sure where to put that, with the validators? Can then get on and make a pull request for this and also a first suggestion for the initial on/off/1/0 implementation :)
find myself again copy and pasting an on/off parser between instrument drivers and shuddering as I do it... this isn't a huge deal I realise but also the longer it's left the more work it will be to clean up if/when we do decide how to resolve it :(

Resolving patch:
diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py
--- a/qcodes/instrument/parameter.py
+++ b/qcodes/instrument/parameter.py
@@ -108,6 +108,11 @@ def __exit__(self, typ, value, traceback):
         self._parameter.set(self._original_value)
 
 
+def invert_val_mapping(val_mapping: Dict) -> Dict:
+    """Inverts the value mapping dictionary for allowed parameter values"""
+    return {v: k for k, v in val_mapping.items()}
+
+
 class _BaseParameter(Metadatable):
     """
     Shared behavior for all parameters. Not intended to be used
@@ -233,7 +238,7 @@ def __init__(self, name: str,
         if val_mapping is None:
             self.inverse_val_mapping = None
         else:
-            self.inverse_val_mapping = {v: k for k, v in val_mapping.items()}
+            self.inverse_val_mapping = invert_val_mapping(val_mapping)
 
         self.get_parser = get_parser
         self.set_parser = set_parser
diff --git a/qcodes/instrument_drivers/rohde_schwarz/SGS100A.py b/qcodes/instrument_drivers/rohde_schwarz/SGS100A.py
--- a/qcodes/instrument_drivers/rohde_schwarz/SGS100A.py
+++ b/qcodes/instrument_drivers/rohde_schwarz/SGS100A.py
@@ -1,4 +1,5 @@
 from qcodes import VisaInstrument, validators as vals
+from qcodes.utils.helpers import create_on_off_val_mapping
 
 
 class RohdeSchwarz_SGS100A(VisaInstrument):
@@ -54,20 +55,20 @@ def __init__(self, name, address, **kwargs):
                            label='RF Output',
                            get_cmd=':OUTP:STAT?',
                            set_cmd=':OUTP:STAT {}',
-                           get_parser=self.get_parser_on_off,
-                           set_parser=self.set_parser_on_off)
+                           val_mapping=create_on_off_val_mapping(on_val='1',
+                                                                 off_val='0'))
         self.add_parameter('IQ_state',
                            label='IQ Modulation',
                            get_cmd=':IQ:STAT?',
                            set_cmd=':IQ:STAT {}',
-                           get_parser=self.get_parser_on_off,
-                           set_parser=self.set_parser_on_off)
+                           val_mapping=create_on_off_val_mapping(on_val='1',
+                                                                 off_val='0'))
         self.add_parameter('pulsemod_state',
                            label='Pulse Modulation',
                            get_cmd=':SOUR:PULM:STAT?',
                            set_cmd=':SOUR:PULM:STAT {}',
-                           get_parser=self.get_parser_on_off,
-                           set_parser=self.set_parser_on_off)
+                           val_mapping=create_on_off_val_mapping(on_val='1',
+                                                                 off_val='0'))
         self.add_parameter('pulsemod_source',
                            label='Pulse Modulation Source',
                            get_cmd='SOUR:PULM:SOUR?',
@@ -108,8 +109,8 @@ def __init__(self, name, address, **kwargs):
                            label='IQ Impairments',
                            get_cmd=':SOUR:IQ:IMP:STAT?',
                            set_cmd=':SOUR:IQ:IMP:STAT {}',
-                           get_parser=self.get_parser_on_off,
-                           set_parser=self.set_parser_on_off)
+                           val_mapping=create_on_off_val_mapping(on_val='1',
+                                                                 off_val='0'))
         self.add_parameter('I_offset',
                            label='I Offset',
                            get_cmd='SOUR:IQ:IMP:LEAK:I?',
@@ -140,36 +141,8 @@ def __init__(self, name, address, **kwargs):
 
         self.connect_message()
 
-    def get_parser_on_off(self,value):
-        if value == '0':
-            ret = 'Off'
-        elif value == '1':
-            ret = 'On'
-        return ret
-
-    def set_parser_on_off(self,value):
-        if isinstance(value, bool):
-            if value:
-                ret = '1'
-            else:
-                ret = '0'
-        elif isinstance(value, str):
-            if value.upper() in ['0', 'OFF']:
-                ret = '0'
-            if value.upper() in ['1', 'ON']:
-                ret = '1'
-        elif value in [1, 0]:
-            if value == 1:
-                ret = '1'
-            elif value == 0:
-                ret = '0'
-        else:
-            raise ValueError('Unable to set parameter to {} '\
-                                'expected "ON"/1/True or "OFF"/0/False.'.format(value))
-        return ret
-
     def on(self):
-        self.set('status', 'on')
+        self.status('on')
 
     def off(self):
-        self.set('status', 'off')
\ No newline at end of file
+        self.status('off')
diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py
--- a/qcodes/utils/helpers.py
+++ b/qcodes/utils/helpers.py
@@ -12,6 +12,7 @@
 from asyncio import iscoroutinefunction
 from inspect import signature
 from functools import partial
+from collections import OrderedDict
 
 import numpy as np
 
@@ -666,6 +667,30 @@ def inner(**inner_kwargs):
     return inner
 
 
+def create_on_off_val_mapping(on_val: Any = True, off_val: Any = False
+                              ) -> Dict:
+    """
+    Returns a value mapping which maps inputs which reasonably mean "on"/"off"
+    to the specified on_val/off_val which are to be sent to the
+    instrument. This value mapping is such that, when inverted,
+    on_val/off_val are mapped to boolean True/False.
+    """
+    # Here are the lists of inputs which "reasonably" mean the same as
+    # "on"/"off" (note that True/False values will be added below, and they
+    # will always be added)
+    ons_  = ('On',  'ON',  'on',  '1', 1)
+    offs_ = ('Off', 'OFF', 'off', '0', 0)
+
+    # This ensures that True/False values are always added and are added at
+    # the end of on/off inputs, so that after inversion True/False will be
+    # the remaining keys in the inverted value mapping dictionary
+    ons = ons_ + (True,)
+    offs = offs_ + (False,)
+
+    return OrderedDict([(on, on_val) for on in ons]
+                       + [(off, off_val) for off in offs])
+
+
 def abstractmethod(funcobj):
     """A decorator indicating abstract methods.
 
@@ -677,3 +702,4 @@ def abstractmethod(funcobj):
     """
     funcobj.__qcodes_is_abstract_method__ = True
     return funcobj
+


Test patch:
diff --git a/qcodes/tests/test_helpers.py b/qcodes/tests/test_helpers.py
--- a/qcodes/tests/test_helpers.py
+++ b/qcodes/tests/test_helpers.py
@@ -14,7 +14,8 @@
                                   strip_attrs, full_class,
                                   named_repr, make_sweep, is_sequence_of,
                                   compare_dictionaries, NumpyJSONEncoder,
-                                  partial_with_docstring)
+                                  partial_with_docstring,
+                                  create_on_off_val_mapping)
 from qcodes.logger.logger import LogCapture
 from qcodes.utils.helpers import is_function, attribute_set_to
 
@@ -837,3 +838,20 @@ def f():
         docstring = "some docstring"
         g = partial_with_docstring(f, docstring)
         self.assertEqual(g.__doc__, docstring)
+
+
+class TestCreateOnOffValMapping(TestCase):
+    """Test function that creates val mapping for on/off parameters"""
+
+    def test_values_of_mapping_are_only_the_given_two(self):
+        val_mapping = create_on_off_val_mapping(on_val='666', off_val='000')
+        values_set = set(list(val_mapping.values()))
+        self.assertEqual(values_set, {'000', '666'})
+
+    def test_its_inverse_maps_only_to_booleans(self):
+        from qcodes.instrument.parameter import invert_val_mapping
+
+        inverse = invert_val_mapping(
+            create_on_off_val_mapping(on_val='666', off_val='000'))
+
+        self.assertDictEqual(inverse, {'666': True, '000': False})
diff --git a/qcodes/tests/test_parameter.py b/qcodes/tests/test_parameter.py
--- a/qcodes/tests/test_parameter.py
+++ b/qcodes/tests/test_parameter.py
@@ -17,6 +17,7 @@
     InstrumentRefParameter, ScaledParameter)
 import qcodes.utils.validators as vals
 from qcodes.tests.instrument_mocks import DummyInstrument
+from qcodes.utils.helpers import create_on_off_val_mapping
 from qcodes.utils.validators import Numbers
 
 
@@ -951,6 +952,37 @@ def test_val_mapping_with_parsers(self):
         self._p = 'PVAL: 1'
         self.assertEqual(p(), 'on')
 
+    def test_on_off_val_mapping(self):
+        instrument_value_for_on = 'on_'
+        instrument_value_for_off = 'off_'
+
+        parameter_return_value_for_on = True
+        parameter_return_value_for_off = False
+
+        p = Parameter('p', set_cmd=self.set_p, get_cmd=self.get_p,
+                      val_mapping=create_on_off_val_mapping(
+                          on_val=instrument_value_for_on,
+                          off_val=instrument_value_for_off))
+
+        test_data = [(instrument_value_for_on,
+                      parameter_return_value_for_on,
+                      ('On', 'on', 'ON', 1, True)),
+                     (instrument_value_for_off,
+                      parameter_return_value_for_off,
+                      ('Off', 'off', 'OFF', 0, False))]
+
+        for instr_value, parameter_return_value, inputs in test_data:
+            for inp in inputs:
+                # Setting parameter with any of the `inputs` is allowed
+                p(inp)
+                # For any value from the `inputs`, what gets send to the
+                # instrument is on_val/off_val which are specified in
+                # `create_on_off_val_mapping`
+                self.assertEqual(self._p, instr_value)
+                # When getting a value of the parameter, only specific
+                # values are returned instead of `inputs`
+                self.assertEqual(p(), parameter_return_value)
+
 
 class TestManualParameterValMapping(TestCase):
     def setUp(self):