Connect a list of components according to a list of connections.

    Args:
        components (list): List of Circuit instances
        connections (list): List of pairs ``((c1, port1), (c2, port2))`` where
            ``c1`` and ``c2`` are elements of `components` (or the index of the
            element in `components`), and ``port1`` and ``port2`` are the
            indices (or port names) of the ports of the two components that
            should be connected
        force_SLH (bool): If True, convert the result to an SLH object
        expand_simplify (bool): If the result is an SLH object, expand and
            simplify the circuit after each feedback connection is added

    Example:
        >>> A = CircuitSymbol('A', cdim=2)
        >>> B = CircuitSymbol('B', cdim=2)
        >>> BS = Beamsplitter()
        >>> circuit = connect(
        ...     components=[A, B, BS],
        ...     connections=[
        ...         ((A, 0),  (BS, 'in')),
        ...         ((BS, 'tr'), (B, 0)),
        ...         ((A, 1), (B, 1))])
        >>> print(unicode(circuit).replace('cid(1)', '1'))
        (B ⊞ 1) ◁ Perm(0, 2, 1) ◁ (BS(π/4) ⊞ 1) ◁ Perm(0, 2, 1) ◁ (A ⊞ 1)

        The above example corresponds to the circuit diagram::

             ┌─┐    ┌───────┐    ┌─┐
            >┤ ├───>┤       ├───>┤ ├
             │A│    │BS(π/4)│    │B│
            >┤ ├┐┌─>┤       ├┐┌─>┤ ├
             └─┘└│  └───────┘└│  └─┘
                 │┐           │┐
            ─────┘└───────────┘└────

    Raises:
        ValueError: if `connections` includes any invalid entries

    Note:
        The list of `components` may contain duplicate entries, but in this
        case you must use a positional index in `connections` to refer to any
        duplicate component. Alternatively, use unique components by defining
        different labels.

def connect(components, connections, force_SLH=False, expand_simplify=True):
    """Connect a list of components according to a list of connections.

    Args:
        components (list): List of Circuit instances
        connections (list): List of pairs ``((c1, port1), (c2, port2))`` where
            ``c1`` and ``c2`` are elements of `components` (or the index of the
            element in `components`), and ``port1`` and ``port2`` are the
            indices (or port names) of the ports of the two components that
            should be connected
        force_SLH (bool): If True, convert the result to an SLH object
        expand_simplify (bool): If the result is an SLH object, expand and
            simplify the circuit after each feedback connection is added

    Example:
        >>> A = CircuitSymbol('A', cdim=2)
        >>> B = CircuitSymbol('B', cdim=2)
        >>> BS = Beamsplitter()
        >>> circuit = connect(
        ...     components=[A, B, BS],
        ...     connections=[
        ...         ((A, 0),  (BS, 'in')),
        ...         ((BS, 'tr'), (B, 0)),
        ...         ((A, 1), (B, 1))])
        >>> print(unicode(circuit).replace('cid(1)', '1'))
        (B ⊞ 1) ◁ Perm(0, 2, 1) ◁ (BS(π/4) ⊞ 1) ◁ Perm(0, 2, 1) ◁ (A ⊞ 1)

        The above example corresponds to the circuit diagram::

             ┌─┐    ┌───────┐    ┌─┐
            >┤ ├───>┤       ├───>┤ ├
             │A│    │BS(π/4)│    │B│
            >┤ ├┐┌─>┤       ├┐┌─>┤ ├
             └─┘└│  └───────┘└│  └─┘
                 │┐           │┐
            ─────┘└───────────┘└────

    Raises:
        ValueError: if `connections` includes any invalid entries

    Note:
        The list of `components` may contain duplicate entries, but in this
        case you must use a positional index in `connections` to refer to any
        duplicate component. Alternatively, use unique components by defining
        different labels.
    """
    combined = Concatenation.create(*components)
    cdims = [c.cdim for c in components]
    offsets = _cumsum([0] + cdims[:-1])
    imap = []
    omap = []
    counts = defaultdict(int)
    for component in components:
        counts[component] += 1

    for (ic, ((c1, op), (c2, ip))) in enumerate(connections):

        # check c1; convert to index int
        if not isinstance(c1, int):
            if counts[c1] > 1:
                raise ValueError(
                    "Component %s appears more than once in list of "
                    "components %r. You must reference it by index in the "
                    "connection %r" % (c1, components, connections[ic]))
            try:
                c1 = components.index(c1)
            except ValueError:
                raise ValueError(
                    "The component %s in connection %r is not in the list of "
                    "components %r" % (c1, connections[ic], components))
        else:
            if c1 < 0 or c1 >= len(components):
                raise ValueError(
                    "Invalid index %d in connection %r"
                    % (c1, connections[ic]))
        # check c2; convert to index int
        if not isinstance(c2, int):
            if counts[c2] > 1:
                raise ValueError(
                    "Component %s appears more than once in list of "
                    "components %r. You must reference it by index in the "
                    "connection %r" % (c2, components, connections[ic]))
            try:
                c2 = components.index(c2)
            except ValueError:
                raise ValueError(
                    "The component %s in connection %r is not in the list of "
                    "components %r" % (c2, connections[ic], components))
        else:
            if c2 < 0 or c2 >= len(components):
                raise ValueError(
                    "Invalid index %d in connection %r"
                    % (c2, connections[ic]))
        # check op; convert to index int
        if not (isinstance(op, int)):
            try:
                op = components[c1].PORTSOUT.index(op)
            except AttributeError:
                raise ValueError(
                    "The component %s does not define PORTSOUT labels. "
                    "You cannot use the string %r to refer to a port"
                    % (components[c1], op))
            except ValueError:
                raise ValueError(
                    "The connection %r refers to an invalid output "
                    "channel %s for component %r"
                    % (connections[ic], op, components[c1]))
        else:
            if op < 0 or op >= components[c1].cdim:
                raise ValueError(
                    "Invalid output channel %d <0 or >=%d (cdim of %r) in %r"
                    % (op, components[c1].cdim, components[c1],
                       connections[ic]))
        # check ip; convert to index int
        if not (isinstance(ip, int)):
            try:
                ip = components[c2].PORTSIN.index(ip)
            except AttributeError:
                raise ValueError(
                    "The component %s does not define PORTSIN labels. "
                    "You cannot use the string %r to refer to a port"
                    % (components[c2], ip))
            except ValueError:
                raise ValueError(
                    "The connection %r refers to an invalid input channel "
                    "%s for component %r"
                    % (connections[ic], ip, components[c2]))
        else:
            if ip < 0 or ip >= components[c2].cdim:
                raise ValueError(
                    "Invalid input channel %d <0 or >=%d (cdim of %r) in %r"
                    % (ip, components[c2].cdim, components[c2],
                       connections[ic]))

        op_idx = offsets[c1] + op
        ip_idx = offsets[c2] + ip
        imap.append(ip_idx)
        omap.append(op_idx)

    n = combined.cdim
    nfb = len(connections)

    imapping = map_channels(
        {k: im for (k, im) in zip(range(n-nfb, n), imap)},
        n)

    omapping = map_channels(
        {om: k for (k, om) in zip(range(n-nfb, n), omap)},
        n)

    combined = omapping << combined << imapping

    if force_SLH:
        combined = combined.toSLH()

    for k in range(nfb):
        combined = combined.feedback()
        if isinstance(combined, SLH) and expand_simplify:
            combined = combined.expand().simplify_scalar()

    return combined