For two block structures ``aa = (a1, a2, ..., an)``, ``bb = (b1, b2,
    ..., bm)`` generate the maximal common block structure so that every block
    from aa and bb is contained in exactly one block of the resulting
    structure.  This is useful for determining how to apply the distributive
    law when feeding two concatenated Circuit objects into each other.

    Examples:
        ``(1, 1, 1), (2, 1) -> (2, 1)``
        ``(1, 1, 2, 1), (2, 1, 2) -> (2, 3)``

    Args:
        lhs_bs (tuple): first block structure
        rhs_bs (tuple): second block structure

def _get_common_block_structure(lhs_bs, rhs_bs):
    """For two block structures ``aa = (a1, a2, ..., an)``, ``bb = (b1, b2,
    ..., bm)`` generate the maximal common block structure so that every block
    from aa and bb is contained in exactly one block of the resulting
    structure.  This is useful for determining how to apply the distributive
    law when feeding two concatenated Circuit objects into each other.

    Examples:
        ``(1, 1, 1), (2, 1) -> (2, 1)``
        ``(1, 1, 2, 1), (2, 1, 2) -> (2, 3)``

    Args:
        lhs_bs (tuple): first block structure
        rhs_bs (tuple): second block structure
    """

    # for convenience the arguments may also be Circuit objects
    if isinstance(lhs_bs, Circuit):
        lhs_bs = lhs_bs.block_structure
    if isinstance(rhs_bs, Circuit):
        rhs_bs = rhs_bs.block_structure

    if sum(lhs_bs) != sum(rhs_bs):
        raise IncompatibleBlockStructures(
            'Blockstructures have different total channel numbers.')

    if len(lhs_bs) == len(rhs_bs) == 0:
        return ()

    i = j = 1
    lsum = 0
    while True:
        lsum = sum(lhs_bs[:i])
        rsum = sum(rhs_bs[:j])
        if lsum < rsum:
            i += 1
        elif rsum < lsum:
            j += 1
        else:
            break

    return (lsum, ) + _get_common_block_structure(lhs_bs[i:], rhs_bs[j:])