4.2. Impementing a multiplexer (demonstrates the use of a multi-input connector as arbitrarily many single-input connectors)

This tutorial shows and explains a simple implementation of a multiplexer. With this example, the usage of a multi-input connector as arbitrarily many single-input connectors is demonstrated. A follow-up for this tutorial demonstrates the use of conditional input connectors, to avoid unnecessary computations.

4.2.1. What is a multiplexer

A multiplexer is a device with many inputs and one output, which allows to select, which one of the inputs shall be routed to the output.

digraph Multiplexer{
   rankdir = LR;

   input1 [label="Input 1", shape=parallelogram];
   input2 [label="Input 2", shape=parallelogram];
   inputx [label="...", shape=none];
   inputn [label="Input N", shape=parallelogram];
   output [label="Output", shape=trapezium];

   {rank="same"; input1; input2; inputx; inputn};

   input2 -> output [label="selected"];
   inputx -> output [style="invis"];
   input1 -> input2 -> inputx -> inputn [style="invis"];
}

A very common application for multiplexers is signal routing in electronic circuits, for which there is a huge variety of integrated circuits, such as the 74LS151 or the CMOS 4097. In some occasions, a multiplexer can also be helpful to implement a processing networs, which is why the Connectors package provides the Multiplexer class.

4.2.2. Arbitrarily many input connectors

To suit most applications, the number of inputs of the multiplexer should not be hard coded. Instead it should dynamically scale the number of input connectors. Also, the keys for selecting the input, that shall be routed to the output, should ideally be arbitrary, so the user can decide, if the keys are integers, strings or any other objects.

Theoretically, it is possible to implement such an array of an arbitrary number of input connectors by instantiating SingleInputConnectors dynamically. But such an implementation would require a lot of code and it would depend on implementation details of the Connectors package, that might change in the future.

For applications like this, a MultiInputConnector can be accessed with the [] operator, which returns a an object, that behaves like a single-input connector. This virtual input connector can be called directly or be connected to an output connector. The key, that is passed to the [] operator is the data ID, under which the MultiInputConnector stores the given input value. This allows the user to select the data ID manually, rather than having it generated by the connector, which in turn allows to use the data ID as selector for a multiplexer.

4.2.3. Implementation of the multiplexer

>>> import connectors

The following code shows the implementation of a multiplexer.

>>> class Multiplexer:
...     def __init__(self, selector=None):
...         self.__selector = selector
...         self.__data = connectors.MultiInputData()
...
...     @connectors.Output()
...     def output(self):
...         if self.__selector in self.__data:
...             return self.__data[self.__selector]
...         else:
...             return None
...
...     @connectors.Input("output")
...     def select(self, selector):
...         self.__selector = selector
...         return self
...
...     @connectors.MultiInput("output")
...     def input(self, data):
...         return self.__data.add(data)
...
...     @input.remove
...     def remove(self, data_id):
...         del self.__data[data_id]
...         return self
...
...     @input.replace
...     def replace(self, data_id, data):
...         self.__data[data_id] = data
...         return data_id

Note, that it is required, that the replace() method returns the ID, under which the new input value is stored. Apart from this, the implementation is straight forward.

  • The input(), remove() and replace() methods implement a very common pattern for multi-input connectors, in which the input values are stored in a MultiInputData instance.
  • The select() method is an input connector, through which the key for selecting the input, that is routed to the output.
  • The output() method returns the value from the selected input or None, if the selector key is invalid.
  • The select() and remove() methods return self to allow method chaining.

4.2.4. Usage of the multiplexer

Instantiating the multiplexer is done the usual way.

>>> multiplexer = Multiplexer()

When calling the input, it can be accessed with the [] operator to specify the selector key.

>>> multiplexer.input["key 1"]("value 1")
<__main__.Multiplexer object at 0x...>
>>> multiplexer.input["key 2"]("value 2")
<__main__.Multiplexer object at 0x...>
>>> multiplexer.select("key 2")
<__main__.Multiplexer object at 0x...>
>>> multiplexer.output()
'value 2'

Note, that the call of the virtual single-input method returns the multiplexer instance. This is an implementation choice of the Connectors package and cannot be influenced by how the decorated method is implemented. The idea behind this choice is, that it allows chaining the calls of the input method. Theoretically, all of the above can be written in one line:

>>> Multiplexer().input["key 1"]("value 1").input["key 2"]("value 2").select("key 2").output()
'value 2'

Under the hood, the virtual single-inputs, that are created with the [] operator, call the replace() method. So the above script is equivalent to the following.

>>> multiplexer.replace("key 1", "value 1")
'key 1'
>>> multiplexer.replace("key 2", "value 2")
'key 2'
>>> _ = multiplexer.select("key 2")
>>> multiplexer.output()
'value 2'

The input() method can also be called like an ordinary multi-input connector. In this case, the returned data ID must be stored in a variable, so it can be used as selector key.

>>> key1 = multiplexer.input("value 1")
>>> key2 = multiplexer.input("value 2")
>>> _ = multiplexer.select(key2)
>>> multiplexer.output()
'value 2'

The latter two approaches do not work in the context of connecting an output connector to one of the inputs of the multiplexer. The replace() method does not accept connections, while when using the input() method the usual way, the data ID is unknown to the user, so it cannot be used as a selector key. Therefore, connections to the multiplexer have to use the virtual single-inputs from the [] operator.

>>> data_source = connectors.blocks.PassThrough("value 3 (value from the data source)")
>>> _ = data_source.output.connect(multiplexer.input["key 3"])
>>> _ = multiplexer.select("key 3")
>>> multiplexer.output()
'value 3 (value from the data source)'

4.2.5. Restrictions and requirements for virtual single-input connectors

When the [] operator calls the replace method of the multi-input connector, it is possible, that the data ID, which is passed to the method, does not exist, yet. Therefore, the replace methods of multi-input connectors, that shall be used as virtual single-inputs, must be able to hanlde unknown data IDs in a reasonable manner. This is usually the case, when the input data is managed by dictionaries like a MultiInputData instance.

For ordinary multi-input connectors, it is optional to specify a replace method. If none is specified, replacing data is done with the remove method and the decorated input method. This will obviously not work with the [] operator, because calling the decorated input method will generate a new data ID, that is not known outside the class.

When managing the input data of a multi-input connector with dictionaries like a MultiInputData instance, the data IDs must be hashable. Therefore it is not possible to use mutable objects like list instances as selector keys for this Multiplexer.