Repository: microsoft/Qcodes

Issue:
GroupParameter initial_value cannot be set
Here's a MWE:
```py
from qcodes.instrument.group_parameter import GroupParameter, Group
from qcodes import Instrument

class MyInstrument(Instrument):
    def __init__(self, name, *args, **kwargs):
        super().__init__(name, *args, **kwargs)

        self.add_parameter(name="foo",
                           initial_value=42,
                           parameter_class=GroupParameter
                          )

        self.group = Group([self.foo])

instr = MyInstrument("test")
```

### Expected behaviour
The instrument should have the GroupParameter with the given initial value.

### Actual behaviour
Raises `RuntimeError("Trying to set Group value but no  group defined")`.

### Proposed fix
The `GroupParameter` should defer setting the initial value until it has been added to the group. One way of doing it would be to add something like the following to `GroupParameter.__init__`, before the `super().__init__` call:
```py
if "initial_value" in kwargs:
    self._initial_value = kwargs["initial_value"]
    kwargs["initial_value"] = None
```
and then adding a `GroupParameter.add_to_group` method where the value is actually set, and calling that instead of just setting `parameter.group = self` in `Group.__init__`. I'm not 100% sure if this is the right way to do it.

### System
**qcodes branch**: master

**qcodes commit**: c7eef82d9ab68afb3546fb3c736f2d5b2ff02a14



Discussion:
@mgunyho could you clarify your use case? (a group with a single parameter does not seem to have value in my opinion)

in attempt to still answer you back now, i tend to think that instead of passing initial values to each parameters in a group, a simpler (and perhaps cleaner) way is to request from the instrument a value of one of the parameters from the group AFTER defining the whole group. This request will automatically update all the parameters in the group (because they are in the group). Will this approach work for your case?
Of course in a real driver the group would have multiple parameters. In my case I have a group where for some values I would like to define an initial value, but not necessarily all of them.

I think it would definitely be cleanest to specify the initial values in the `add_parameter` call. Currently I have a workaround, where I do
```py
self.add_parameter("param1", parameter_class=GroupParameter)
self.add_parameter("param2", parameter_class=GroupParameter)
...

self.group = Group([self.param1, self.param2, ...])

initial_values = {
    "param1": 123,
    "param2": 456,
    ...
}

for p, v in initial_values.items():
    self.parameters[p](v)
```
which is quite cumbersome and error-prone, as each parameter has to be handled twice.

I'm not sure if I understand your suggestion. Would it differ much from this workaround I have? Or do you mean something like what I was initially trying to suggest, that the parameter `set` functions are called when the `Group(...)` is created?
Thanks for the explanation!

> a group where for some values I would like to define an initial value, but not necessarily all of them

So the `but not necessarily all of them` part is the tricky one. The point of having a group of group parameters is that they are being `set` and `get` by a single VISA command. That is, when setting one of the parameters from the group to some value, all the other parameters also HAVE to be set to some values.

So my initial suggestion (apologies that i wasn't clear when describing it) was:
* create all the parameters
* create a group of them
* call `.get()` on one of the parameters in the group to populate all the group parameters with some values (that is, the values that are currently in the instrument)
* and now finally go through the parameters that you care about and set them manually to some initial values that you want

And this suggestion of mine looks indeed similar to the "workaround snippet" from your last message. And it is indeed cumbersome, as in, a lot of boilerplate code is needed.

> ... do you mean something like what I was initially trying to suggest, that the parameter set functions are called when the Group(...) is created?

Indeed!

-----

My suggestion is the following:
* when defining a group, allow to pass initial values but for ALL the parameters at the same time.
```python
self.add_parameter("param1", parameter_class=GroupParameter)
self.add_parameter("param2", parameter_class=GroupParameter)
self.add_parameter("param3", parameter_class=GroupParameter)

self.group = Group(
    [self.param1, self.param2, self.param3],
    set_cmd='ABC {param1}, {param2}, {param3}',
    get_cmd='ABC?',
    initial_values={'param1': 42, 'param2': 0.5, 'param3': False}
)
# `initial_values` dictionary HAS to contain initial values
# for ALL the parameters in the group
```
* raise when a group parameter gets inital_value passed in:
```python
self.add_parameter(
    "param1",
    parameter_class=GroupParameter,
    initial_value=42)

---> Exception: it's not possible to set initial_value for a group parameter
```

Would you agree with this suggestion? Will it solve your use case?
That would indeed solve the issue, but I think it's unwieldy having to go through all parameters three times (`add_parameter`, `Group([...])` and with the default values dict). It also doesn't feel right that GroupParameters have a special way of setting the initial value compared to all other subclasses.

The most correct way in my opinion would be that 
1. `initial_value` is (optionally) given in the `add_parameter` function
1. `GroupParameter.__init__` takes care of storing the initial value in the class but not setting it on the device
1. In `Group.__init__`, when all the parameters have been added to the group, their current values are read from the device, and for those GroupParameters that had an initial value the values are replaced, and then all parameters are written back simultaneously.

Does this make sense? I can try to write something tomorrow and submit a PR.
Thanks for raising a valid usability and parallelism concerns.

I like you suggestion, but let's not implement the extra smartness of `..their current values are read from the device, and for those GroupParameters that had an initial value the values are replaced...`. Explicit is better than implicit. Moreover, from experience with debugging instrument drivers and isntruments in general, "extra smartness" is difficult to remember of and difficult to debug with, hence when developing drivers we always try to make them as straightforward/dumb as possible.

In other words, allow me to modify the bullet points above (see the last one):

1. `initial_value` is (optionally) given in the `add_parameter` function
1. `GroupParameter.__init__` takes care of storing the initial value in the class but not setting it on the device
1. In `Group.__init__`, when all the parameters have been added to the group, the initial values for all the parameters of the group are being set. `Group` requires either ALL parameters in the group to have initial values specified, or NONE of the parameters in the group to have the initial values specified; in other cases, it throws an exception.

And great that you are willing to initiate a PR!

Resolving patch:
diff --git a/qcodes/instrument/group_parameter.py b/qcodes/instrument/group_parameter.py
--- a/qcodes/instrument/group_parameter.py
+++ b/qcodes/instrument/group_parameter.py
@@ -30,8 +30,11 @@ class GroupParameter(Parameter):
 
     Args:
         name: Name of the parameter.
-        instrument: Instrument that this parameter belongs to; this
-              instrument is used by the group to call its get and set commands.
+        instrument: Instrument that this parameter belongs to; this instrument
+            is used by the group to call its get and set commands.
+        initial_value: Initial value of the parameter. Note that either none or
+            all of the parameters in a :class:`.Group` should have an initial
+            value.
 
         **kwargs: All kwargs used by the :class:`.Parameter` class, except
              ``set_cmd`` and ``get_cmd``.
@@ -40,6 +43,7 @@ class GroupParameter(Parameter):
     def __init__(self,
                  name: str,
                  instrument: Optional['Instrument'] = None,
+                 initial_value: Union[float, int, str, None] = None,
                  **kwargs
                  ) -> None:
 
@@ -48,6 +52,7 @@ def __init__(self,
                              "'get_cmd' kwarg")
 
         self.group: Union[Group, None] = None
+        self._initial_value = initial_value
         super().__init__(name, instrument=instrument, **kwargs)
 
         self.set = self._wrap_set(self.set_raw)
@@ -176,6 +181,27 @@ def __init__(self,
         else:
             self.get_parser = self._separator_parser(separator)
 
+        have_initial_values = [p._initial_value is not None
+                               for p in parameters]
+        if any(have_initial_values):
+            if not all(have_initial_values):
+                params_with_initial_values = [p.name for p in parameters
+                                              if p._initial_value is not None]
+                params_without_initial_values = [p.name for p in parameters
+                                                 if p._initial_value is None]
+                error_msg = (f'Either none or all of the parameters in a '
+                             f'group should have an initial value. Found '
+                             f'initial values for '
+                             f'{params_with_initial_values} but not for '
+                             f'{params_without_initial_values}.')
+                raise ValueError(error_msg)
+
+            calling_dict = {name: p._initial_value
+                            for name, p in self.parameters.items()}
+
+            self._set_from_dict(calling_dict)
+
+
     def _separator_parser(self, separator: str
                           ) -> Callable[[str], Dict[str, Any]]:
         """A default separator-based string parser"""
@@ -200,6 +226,14 @@ def set(self, set_parameter: GroupParameter, value: Any):
         calling_dict = {name: p.raw_value
                         for name, p in self.parameters.items()}
         calling_dict[set_parameter.name] = value
+
+        self._set_from_dict(calling_dict)
+
+    def _set_from_dict(self, calling_dict: Dict[str, Any]):
+        """
+        Use ``set_cmd`` to parse a dict that maps parameter names to parameter
+        values, and actually perform setting the values.
+        """
         if self.set_cmd is None:
             raise RuntimeError("Calling set but no `set_cmd` defined")
         command_str = self.set_cmd.format(**calling_dict)


Test patch:
diff --git a/qcodes/tests/test_group_parameter.py b/qcodes/tests/test_group_parameter.py
--- a/qcodes/tests/test_group_parameter.py
+++ b/qcodes/tests/test_group_parameter.py
@@ -1,12 +1,23 @@
 import re
 import pytest
+from typing import Optional
 
 from qcodes.instrument.group_parameter import GroupParameter, Group
 from qcodes import Instrument
 
 
+@pytest.fixture(autouse=True)
+def close_all_instruments():
+    """Makes sure that after startup and teardown all instruments are closed"""
+    Instrument.close_all()
+    yield
+    Instrument.close_all()
+
+
 class Dummy(Instrument):
-    def __init__(self, name: str) -> None:
+    def __init__(self, name: str,
+                 initial_a: Optional[int] = None,
+                 initial_b: Optional[int] = None) -> None:
         super().__init__(name)
 
         self._a = 0
@@ -18,7 +29,8 @@ def __init__(self, name: str) -> None:
             parameter_class=GroupParameter,
             docstring="Some succinct description",
             label="label",
-            unit="SI"
+            unit="SI",
+            initial_value=initial_a
         )
 
         self.add_parameter(
@@ -27,7 +39,8 @@ def __init__(self, name: str) -> None:
             parameter_class=GroupParameter,
             docstring="Some succinct description",
             label="label",
-            unit="SI"
+            unit="SI",
+            initial_value=initial_b
         )
 
         Group(
@@ -78,6 +91,7 @@ def test_raise_on_get_set_cmd():
         assert str(e.value) == "A GroupParameter does not use 'set_cmd' or " \
                                "'get_cmd' kwarg"
 
+
 def test_raises_on_get_set_without_group():
     param = GroupParameter(name='b')
 
@@ -88,3 +102,20 @@ def test_raises_on_get_set_without_group():
     with pytest.raises(RuntimeError) as e:
         param.set(1)
     assert str(e.value) == "('Trying to set Group value but no group defined', 'setting b to 1')"
+
+
+def test_initial_values():
+    initial_a = 42
+    initial_b = 43
+    dummy = Dummy("dummy", initial_a=initial_a, initial_b=initial_b)
+
+    assert dummy.a() == initial_a
+    assert dummy.b() == initial_b
+
+
+def test_raise_on_not_all_initial_values():
+    expected_err_msg = (r'Either none or all of the parameters in a group '
+                        r'should have an initial value. Found initial values '
+                        r'for \[.*\] but not for \[.*\].')
+    with pytest.raises(ValueError, match=expected_err_msg):
+        dummy = Dummy("dummy", initial_a=42)