Repository: microsoft/Qcodes

Issue:
Make it possible to dynamically create/discover/delete sub-modules on instruments
The QCoDeS design allowes drivers to have sub-modules. Often times these are instrument channels and are fixed in number. But sub-modules need not be tied to physical instrument channels. In fact we sometimes encounter instruments with the ability to dynamically create and delete sub-modules. An example of this are measurement traces and windows on the Keysight N52xx precision network analyzers (PNA). Another instrument which can benifit from dynamic sub-moduling is the Keysight M8020 digital pattern generator, whereby each pattern can be defined in its own sub-module. 

We therefore propose the following enhancement to the InstrumentChannel class in QCoDeS: 

1. Change the name to (something similar to) InstrumentSubmodule
2. Develop a way to create/discover/delete submodules dynamically. And implementation to accomplish this for the Keysight precision network analyzers is shown [here](https://github.com/sohailc/Qcodes/blob/doc/PNA_N5222B_example/qcodes/instrument_drivers/Keysight/N52xx/_N52xx_channel_ext.py). 

Additionally, we propose to enhance the ChannelList class by adding an `add` method which will create a new sub-module of the same type. An example of this is likewise shown in the above link. The example file for the N52xx PNA in my branch is also a good resource to understand [this](https://github.com/sohailc/Qcodes/blob/doc/PNA_N5222B_example/docs/examples/driver_examples/Qcodes_example_with_Keysight_Network_Analyzer.ipynb)  

Since a similar design will benefit other instruments as well, I propose to make dynamic sub-moduling available generally 


Discussion:
I like the general idea of this.
* the need for renaming `InstrumentChannel` into `InstrumentSubmodule` or `InstrumentModule` is good. We just need to be careful about backwards compatibility. Shall the `ChannelList` be renamed to `ModuleList` as well then? :)
* "way to create/discover/delete" - can you specify this in more detail? i looked at the code and got some humble understanding, but i'd rather have a dry list of things we want to do with respect to "dynamic sub-moduling" and their reasons in order to understand the bigger picture (and only then talk about implementation). (example of a remark about the implementation: I don't like the fact that a trace can remove itself from a list of traces via holding a reference to the list of traces that it belongs to, it seems unsafe)
* the `add` method is probably fine, it's just a nice API improvement.

PR(s) with a qcodes-wide applicable implementation is more than welcome :)
@sohailc, I think this is a great suggestion - a dangerous one however.

I am speaking of experience, since I ran into a similar problem:
For the parametric sequencer I am dynamically creating and deleting parameters according to the waveform that is generated.
Here the difficulty is that the user is likely to create a channel do something with it, delete it, create a different one and maybe eventually create the first one again. So far so good. But if you happen to keep any reference around, it will not automatically be deleted and when creating the same channel again the user might think the original reference would still be usable.
This is not a blocking problem, but we should keep at the back of our heads that this problem is present, and make an architecture that minimizes the occurrence of such a situation.
Hi guys, thanks a lot for your comments! 

@astafan8 
1) Yes, we will need to be careful how we would rename that. I would say make stub subclasses with old names and deprecation warnings. 

2) To specify the create/discover/delete in more detail, lets consider the example of the Keysight N52xx network analyzers (PNA). These instruments have windows, traces, measurements and something called "channels" which are not channels in the traditional sense, but rather grouping of measurements (very confusing!)

For each of these objects:
* We have a unique indentifier, for example a number or a name. 
 
* We have a catalog SCPI command which will give us a list of these object already defined on the instruments. For example, `SYST:WIND:CAT?` will give me an identifier list of all windows defined on the instrument. The new `InstrumentModule` class will have a *class method* `load_from_instrument` which will call the catalog command, get the list of unique identifiers and in a loop call the `__init__` method of the class, returning a list of sub-module Instances.

*  We can create new instances of these objects on the instrument with a SCPI command. For instance, we create a new window with `DISP:WINDow<N> ON` where `N` is a unique indentifier number for a window. Each sub-class of the `InstrumentModule` class shall have a `_create` method. In the case of a PNA window, we will write the mentioned SCPI string when the `_create` method is called.

* Similarly, each dynamic submodule has a SCPI command to delete it from the instrument. Again, for the window this is `DISP:WINDow<N> OFF`

To create a new sub-module, an `InstrumentModule` subclass will have a *class method* which will give us the next available unique indentifier. In code this could look like 

```python
def _generate_unique_id(self): 
    idn = 0
    while True: 
        yield id 
        idn += 1 

def next_id(self): 
    id_generator = self._generate_unique_id()
    existing_ids = self._discover_list_from_instrument()
    idn = next(id_generator)
    while idn in existing_ids: 
        idn = next(id_generator)

    return idn 
```
We are splitting up the generator of the unique id so we can implement specific ones for subclasses; not all unique id's are numbers, they can also be names. 

The new modules list class will have an `add` method which will call `next_id` to create a new instance of a sub module. 

@Dominik-Vogel 

Yes, I have run into simular issues as well. The way I propose to solve this is that the submodule class will have an `_exists_on_instrument` attribute. When instantiating a submodule class, this attribute will be `False`. When we call the `create` method of the submodule to create an instance on the instrument, it will become `True`. When calling the `delete` method it will become `False` again. The `write` and `ask` methods of the submodule class will first check if the a representation exists on the instrument before sending SCPI strings. An error is raised if  `_exists_on_instrument == False`

I have tried this with the N52xx class and it seems to be working well. What do you think? 
  
I will make a PR over the weekend to implement all of this. This will faciliate the discussion. 

Resolving patch:
diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py
--- a/qcodes/instrument/channel.py
+++ b/qcodes/instrument/channel.py
@@ -1,5 +1,8 @@
 """ Base class for the channel of an instrument """
-from typing import List, Tuple, Union, Optional, Dict, Sequence, cast
+from typing import (
+    List, Union, Optional, Dict, Sequence,
+    cast, Any
+)
 
 from .base import InstrumentBase, Instrument
 from .parameter import MultiParameter, ArrayParameter, Parameter
@@ -42,7 +45,7 @@ def __init__(self,
         # Naming insanity:
         # (see https://github.com/QCoDeS/Qcodes/issues/1140 for a nice table)
         # this has been a confusion about names. don't use name but
-        # full_name, or short_name. 
+        # full_name, or short_name.
         self.name = "{}_{}".format(parent.name, str(name))
 
 
@@ -532,3 +535,304 @@ def validate(self, value, context: str='') -> None:
             raise ValueError(
                 '{} is not part of the expected channel list; {}'.format(
                     repr(value), context))
+
+
+class AutoLoadableInstrumentChannel(InstrumentChannel):
+    """
+    This subclass provides extensions to auto-load channels
+    from instruments and adds methods to create and delete
+    channels when possible. Please note that `channel` in this
+    context does not necessarily mean a physical instrument channel,
+    but rather an instrument sub-module. For some instruments,
+    these sub-modules can be created and deleted at will.
+    """
+
+    @classmethod
+    def load_from_instrument(
+            cls, parent: Instrument,
+            channel_list: 'AutoLoadableChannelList'=None,
+            **kwargs
+    )->List['AutoLoadableInstrumentChannel']:
+        """
+        Load channels that already exist on the instrument
+
+        Args:
+            parent: The instrument through which the instrument
+                channel is accessible
+            channel_list: The channel list this
+                channel is a part of
+            **kwargs: Keyword arguments needed to create the channels
+
+        Returns:
+            List of instrument channel instances created for channels
+            that already exist on the instrument
+        """
+
+        obj_list = []
+        for new_kwargs in cls._discover_from_instrument(parent, **kwargs):
+            obj = cls(
+                parent, existence=True, channel_list=channel_list,
+                **new_kwargs
+            )
+            obj_list.append(obj)
+
+        return obj_list
+
+    @classmethod
+    def _discover_from_instrument(
+            cls, parent: Instrument, **kwargs) ->List[dict]:
+        """
+        Discover channels on the instrument and return a list kwargs to create
+        these channels in memory
+
+        Args:
+            parent: The instrument through which the instrument
+                channel is accessible
+            **kwargs: Keyword arguments needed to discover the channels
+
+        Returns:
+              List of keyword arguments for channel instance initialization
+              for each channel that already exists on the physical instrument
+        """
+        raise NotImplementedError(
+            "Please subclass and implement this method in the subclass")
+
+    @classmethod
+    def new_instance(
+            cls, parent: Instrument, create_on_instrument: bool=True,
+            channel_list: 'AutoLoadableChannelList'=None, **kwargs
+    )->'AutoLoadableInstrumentChannel':
+        """
+        Create a new instance of the channel on the instrument: This involves
+        finding initialization arguments which will create a channel with a
+        unique name and create the channel on the instrument.
+
+        Args:
+            parent: The instrument through which the instrument
+                channel is accessible
+            create_on_instrument: When True, the channel is immediately
+                created on the instrument
+            channel_list: The channel list this
+                channel is going to belong to
+            **kwargs: Keyword arguments needed to create a new instance.
+        """
+        new_kwargs = cls._get_new_instance_kwargs(parent=parent, **kwargs)
+
+        try:
+            new_instance = cls(parent, channel_list=channel_list, **new_kwargs)
+        except TypeError as err:
+            # The 'new_kwargs' dict is malformed. Investigate more precisely
+            # why and give the user a more helpful hint how this can be
+            # solved.
+            if "name" not in new_kwargs:
+                raise TypeError(
+                    "A 'name' argument should be supplied by the "
+                    "'_get_new_instance_kwargs' method"
+                ) from err
+            if "parent" in new_kwargs:
+                raise TypeError(
+                    "A 'parent' argument should *not* be supplied by the "
+                    "'_get_new_instance_kwargs' method"
+                ) from err
+            # Something else has gone wrong. Probably, not all mandatory keyword
+            # arguments are supplied
+            raise TypeError(
+                "Probably, the '_get_new_instance_kwargs' method does not "
+                "return all of the required keyword arguments") from err
+
+        if create_on_instrument:
+            new_instance.create()
+
+        return new_instance
+
+    @classmethod
+    def _get_new_instance_kwargs(cls, parent: Instrument=None, **kwargs)->dict:
+        """
+        Returns a dictionary which is used as keyword args when instantiating a
+        channel
+
+        Args:
+            parent: The instrument the new channel will belong to. Not all
+                instruments need this so it is an optional argument
+            **kwargs: Additional arguments which are needed to
+                instantiate a channel can be given directly by the calling
+                function.
+
+        Returns:
+            A keyword argument dictionary with at least a `name` key which is
+            unique on the instrument. The parent instrument is passed as an
+            argument in this function so we can query if the generated name is
+            indeed unique.
+
+        Notes:
+            The init arguments `parent` and `channel_list` are automatically
+            added by the `new_instance` method and should not be added in the
+            kwarg dictionary returned here. Additionally, the argument
+            `existence` either needs to be omitted or be False.
+        """
+        raise NotImplementedError(
+            "Please subclass and implement this method in the subclass")
+
+    def __init__(
+            self,
+            parent: Union[Instrument, 'InstrumentChannel'],
+            name: str,
+            exists_on_instrument: bool=False,
+            channel_list: 'AutoLoadableChannelList'=None,
+            **kwargs
+    ) ->None:
+        """
+        Instantiate a channel object. Note that this is not the same as actually
+        creating the channel on the instrument. Parameters defined on this
+        channels will not be able to query/write to the instrument until it
+        has been created on the instrument
+
+        Args:
+            parent: The instrument through which the instrument
+                channel is accessible
+            name: channel name
+            exists_on_instrument: True if the channel exists on the instrument
+            channel_list: Reference to the list that this channel is a member
+                of; this is used when deleting the channel so that it can remove
+                itself from the list
+        """
+        super().__init__(parent, name=name, **kwargs)
+        self._exists_on_instrument = exists_on_instrument
+        self._channel_list = channel_list
+
+    def create(self) ->None:
+        """Create the channel on the instrument"""
+        if self._exists_on_instrument:
+            raise RuntimeError("Channel already exists on instrument")
+
+        self._create()
+        self._exists_on_instrument = True
+
+    def _create(self)->None:
+        """
+        (SCPI) commands needed to create the channel. Note that we
+        need to use `self.root_instrument.write` to send commands,
+        because self.write will cause _assert_existence to raise a
+        runtime error.
+        """
+        raise NotImplementedError("Please subclass")
+
+    def remove(self)->None:
+        """
+        Delete the channel from the instrument and remove from channel list
+        """
+        self._assert_existence()
+        self._remove()
+        if self._channel_list is not None and self in self._channel_list:
+            self._channel_list.remove(self)
+        self._exists_on_instrument = False
+
+    def _remove(self) ->None:
+        """
+        (SCPI) commands needed to delete the channel from the instrument
+        """
+        raise NotImplementedError("Please subclass")
+
+    def _assert_existence(self)->None:
+        if not self._exists_on_instrument:
+            raise RuntimeError(
+                "Object does not exist (anymore) on the instrument")
+
+    def write(self, cmd: str)->Any:
+        """
+        Write to the instrument only if the channel is present on the instrument
+        """
+        self._assert_existence()
+        return super().write(cmd)
+
+    def ask(self, cmd: str)->None:
+        """
+        Ask the instrument only if the channel is present on the instrument
+        """
+        self._assert_existence()
+        return super().ask(cmd)
+
+    @property
+    def exists_on_instrument(self)->bool:
+        return self._exists_on_instrument
+
+
+class AutoLoadableChannelList(ChannelList):
+    """
+    Extends the QCoDeS ChannelList class to add the following features:
+    - Automatically create channel objects on initialization
+    - Make a `add` method to create channel objects
+
+    Args:
+        parent: the instrument to which this channel
+            should be attached
+
+        name: the name of the channel list
+
+        chan_type: the type of channel contained
+            within this list
+
+        chan_list: An optional iterable of
+            channels of type chan_type.  This will create a list and
+            immediately lock the ChannelList.
+
+        snapshotable: Optionally disables taking of snapshots
+            for a given channel list.  This is used when objects
+            stored inside a channel list are accessible in multiple
+            ways and should not be repeated in an instrument snapshot.
+
+        multichan_paramclass: The class of
+            the object to be returned by the ChanneList's __getattr__ method.
+            Should be a subclass of MultiChannelInstrumentParameter.
+
+        **kwargs: Keyword arguments to be passed to the `load_from_instrument`
+            method of the channel class. Note that the kwargs are *NOT* passed
+            to the `__init__` of the super class.
+
+    Raises:
+        ValueError: If chan_type is not a subclass of InstrumentChannel
+        ValueError: If multichan_paramclass if not a subclass of
+            MultiChannelInstrumentParameter (note that a class is a subclass
+            of itself).
+
+    """
+    def __init__(
+            self,
+            parent: Instrument,
+            name: str,
+            chan_type: type,
+            chan_list: Optional[Sequence['AutoLoadableInstrumentChannel']]=None,
+            snapshotable: bool=True,
+            multichan_paramclass: type=MultiChannelInstrumentParameter,
+            **kwargs
+    ) ->None:
+
+        super().__init__(
+            parent, name, chan_type, chan_list, snapshotable,
+            multichan_paramclass
+        )
+        new_channels = self._chan_type.load_from_instrument(  # type: ignore
+            self._parent, channel_list=self, **kwargs)
+
+        self.extend(new_channels)
+
+    def add(self, **kwargs) ->'AutoLoadableInstrumentChannel':
+        """
+        Add a channel to the list
+
+        Args:
+            kwargs: Keyword arguments passed to the `new_instance` method of
+                the channel class
+
+        Returns:
+            Newly created instance of the channel class
+        """
+        new_channel = self._chan_type.new_instance(  # type: ignore
+            self._parent,
+            create_on_instrument=True,
+            channel_list=self,
+            **kwargs
+        )
+
+        self.append(new_channel)
+        return new_channel


Test patch:
diff --git a/qcodes/tests/test_autoloadable_channels.py b/qcodes/tests/test_autoloadable_channels.py
new file mode 100644
--- /dev/null
+++ b/qcodes/tests/test_autoloadable_channels.py
@@ -0,0 +1,287 @@
+"""
+Test the auto-loadable channels and channels list. These channels are helpful
+when dealing with channel types which can be added or deleted from an
+instrument. Please note that `channel` in this context does not necessarily
+mean a physical instrument channel, but rather an instrument sub-module.
+"""
+
+from typing import List, Union, Any, Dict, Callable
+import pytest
+import re
+
+from qcodes import Instrument
+from qcodes.instrument.channel import (
+    AutoLoadableInstrumentChannel, AutoLoadableChannelList, InstrumentChannel
+)
+
+
+class MockBackendBase:
+    """
+    A very simple mock backend that contains a dictionary with string keys and
+    callable values. The keys are matched to input commands with regular
+    expressions and on match the corresponding callable is called.
+    """
+    def __init__(self)->None:
+        self._command_dict: Dict[str, Callable] = {}
+
+    def send(self, cmd: str)->Any:
+        """
+        Instead of sending a string to the Visa backend, we use this function
+        as a mock
+        """
+        keys = self._command_dict.keys()
+        ans = None
+        key = ""
+        for key in keys:
+            ans = re.match(key, cmd)
+            if ans is not None:
+                break
+
+        if ans is None:
+            raise ValueError(f"Command {cmd} unknown")
+
+        args = ans.groups()
+        return self._command_dict[key](*args)
+
+
+class MockBackend(MockBackendBase):
+    """
+    A mock backend for our test instrument. It defines the following SCPI (like)
+    commands:
+
+    INST:CHN<n>:HLO
+    Return the string "Hello from channel <n>" where n is a channel number
+
+    INST:CHN:ADD <n>, <greeting>
+    Add a channel with channel number n. We use the greeting when the hello
+    parameter is called.
+
+    INST:CHN:DEL <n>
+    Delete a channel with channel number n
+
+    INST:CHN:CAT
+    Return a catalog of currently defined channels
+
+    INST:CHN<n>:GRT
+    Return the greeting of this channel
+    """
+    def __init__(self)->None:
+        super().__init__()
+        self._channel_catalog: List[str] = ["1", "2", "4", "5"]  # Pre-existing
+        # channels
+        self._greetings = {chn: "Hello" for chn in self._channel_catalog}
+
+        self._command_dict = {
+            r":INST:CHN(\d):HLO":
+                lambda chn: self._greetings[chn] + " from channel " + str(chn),
+            r":INST:CHN:ADD (\d), (.+)": self._add_channel,
+            r":INST:CHN:DEL (\d)": self._channel_catalog.remove,
+            r":INST:CHN:CAT": lambda: ",".join([
+                str(i) for i in self._channel_catalog]),
+            r":INST:CHN(\d):GRT":  self._greetings.get
+        }
+
+    def _add_channel(self, chn: int, greeting: str)->None:
+        """
+        Add a channel on the mock instrument
+        """
+        self._channel_catalog.append(str(chn))
+        self._greetings[str(chn)] = greeting
+
+
+class SimpleTestChannel(AutoLoadableInstrumentChannel):
+    """
+    A channel to test if we can create and delete channel instances
+    """
+
+    @classmethod
+    def _discover_from_instrument(
+            cls, parent: Instrument, **kwargs) -> List[dict]:
+        """
+        New channels need `name` and `channel` keyword arguments.
+        """
+        channels_str = parent.channel_catalog()
+        channels_to_skip = kwargs.get("channels_to_skip", [])  # Note that
+        # `channels_to_skip` is an optional kwarg for loading from instrument.
+        # We test this by giving this keyword during the initialization of the
+        # AutoLoadableChannelList.
+        kwarg_list = []
+        for channel_str in channels_str.split(","):
+
+            if channel_str in channels_to_skip:
+                continue
+
+            channel = int(channel_str)
+            greeting = parent.ask(f":INST:CHN{channel}:GRT")
+            new_kwargs = {
+                "name": f"channel{channel}",
+                "channel": channel,
+                "greeting": greeting
+            }
+            kwarg_list.append(new_kwargs)
+
+        return kwarg_list
+
+    @classmethod
+    def _get_new_instance_kwargs(
+            cls, parent: Instrument=None, **kwargs) -> dict:
+        """
+        Find the smallest channel number not yet occupied. An optional keyword
+        `greeting` is extracted from the kwargs. The default is "Hello"
+        """
+        channels_str = parent.channel_catalog()
+        existing_channels = [int(i) for i in channels_str.split(",")]
+
+        new_channel = 1
+        while new_channel in existing_channels:
+            new_channel += 1
+
+        new_kwargs = {
+            "name": f"channel{new_channel}",
+            "channel": new_channel,
+            "greeting": kwargs.get("greeting", "Hello")
+        }
+
+        return new_kwargs
+
+    def __init__(
+            self,
+            parent: Union[Instrument, InstrumentChannel],
+            name: str,
+            channel: int,
+            greeting: str,
+            existence: bool = False,
+            channel_list: AutoLoadableChannelList = None,
+    ) -> None:
+
+        super().__init__(parent, name, existence, channel_list)
+        self._channel = channel
+        self._greeting = greeting
+
+        self.add_parameter(
+            "hello",
+            get_cmd=f":INST:CHN{self._channel}:HLO"
+        )
+
+    def _create(self)->None:
+        """Create the channel on the instrument"""
+        self.parent.root_instrument.write(
+            f":INST:CHN:ADD {self._channel}, {self._greeting}")
+
+    def _remove(self)->None:
+        """Remove the channel from the instrument"""
+        self.write(f":INST:CHN:DEL {self._channel}")
+
+
+class DummyInstrument(Instrument):
+    """
+    This dummy instrument allows the creation and deletion of
+    channels
+    """
+
+    def __init__(self, name: str)->None:
+        super().__init__(name)
+
+        self._backend = MockBackend()
+
+        self.add_parameter(
+            "channel_catalog",
+            get_cmd=":INST:CHN:CAT",
+        )
+
+        channels = AutoLoadableChannelList(
+            self, "channels", SimpleTestChannel, channels_to_skip=["5"])
+        self.add_submodule("channels", channels)
+
+    def write_raw(self, cmd: str)->None:
+        self._backend.send(cmd)
+
+    def ask_raw(self, cmd: str)->str:
+        return self._backend.send(cmd)
+
+
+@pytest.fixture(scope='function')
+def dummy_instrument():
+    instrument = DummyInstrument("instrument")
+    yield instrument
+    instrument.close()
+
+
+def test_sanity(dummy_instrument):
+    """
+    Test the basic functionality of the auto-loadable channels, without using
+    the auto-loadable channels list. Please note that the `channels_to_skip`
+    argument in the dummy instrument applies when accessing channels
+    from the channels list. Since we are calling `load_from_instrument` directly
+    in this test without this keyword argument, we will see all channels.
+    """
+    # Test that we are able to discover instruments automatically
+    channels = SimpleTestChannel.load_from_instrument(dummy_instrument)
+    assert len(channels) == 4
+    assert channels[0].hello() == "Hello from channel 1"
+    assert channels[1].hello() == "Hello from channel 2"
+    assert channels[2].hello() == "Hello from channel 4"
+    assert channels[3].hello() == "Hello from channel 5"
+    # Test that we can generate a new instance of the channels without
+    # conflicting names
+    new_channel_kwargs = SimpleTestChannel._get_new_instance_kwargs(
+        dummy_instrument)
+
+    new_channel = SimpleTestChannel(dummy_instrument, **new_channel_kwargs)
+    # Instrument IO through the newly instantiated channel should raise an
+    # exception before actually creating the channel on the instrument
+    with pytest.raises(
+            RuntimeError,
+            match=r"Object does not exist \(anymore\) on the instrument"):
+
+        new_channel.hello()
+
+    # After creating the channel we should be able to talk to the instrument.
+    new_channel.create()
+    assert new_channel.hello() == "Hello from channel 3"
+
+    # Once we remove the channel we should no longer be able to talk to the
+    # instrument
+    new_channel.remove()
+    with pytest.raises(
+            RuntimeError,
+            match=r"Object does not exist \(anymore\) on the instrument"):
+
+        new_channel.hello()  # We have deleted the channel and it should no
+        # longer be available
+
+
+def test_channels_list(dummy_instrument):
+    """
+    Test the auto-loadable channels list
+    """
+    assert dummy_instrument.channels[0].hello() == "Hello from channel 1"
+    assert dummy_instrument.channels[1].hello() == "Hello from channel 2"
+    assert dummy_instrument.channels[2].hello() == "Hello from channel 4"
+    assert len(dummy_instrument.channels) == 3
+    # Test that we can add channels
+    new_channel = dummy_instrument.channels.add()
+    assert len(dummy_instrument.channels) == 4
+    assert new_channel is dummy_instrument.channels[-1]
+    assert new_channel.hello() == "Hello from channel 3"
+    # Test that we can remove them
+    new_channel.remove()
+    assert len(dummy_instrument.channels) == 3
+    assert new_channel not in dummy_instrument.channels
+    # Once removed we should no longer be able to talk to the channel
+    with pytest.raises(
+            RuntimeError,
+            match=r"Object does not exist \(anymore\) on the instrument"):
+
+        new_channel.hello()
+    # Remove a channel that was pre-existing on the instrument.
+    dummy_instrument.channels[-1].remove()
+    assert len(dummy_instrument.channels) == 2
+
+
+def test_with_kwargs(dummy_instrument):
+    """
+    Test keyword arguments given to the add method
+    """
+    new_channel = dummy_instrument.channels.add(greeting="Guden tag")
+    assert new_channel.hello() == "Guden tag from channel 3"