Transforms a circuit into a series of GroupMatrix+CZ layers.

    Args:
        circuit: The circuit to transform.
        grouping: How the circuit's qubits are combined into groups.

    Returns:
        A list of layers. Each layer has a matrix to apply to each group of
        qubits, and a list of CZs to apply to pairs of qubits crossing
        between groups.

def _circuit_as_layers(circuit: circuits.Circuit,
                       grouping: _QubitGrouping) -> List[_TransformsThenCzs]:
    """Transforms a circuit into a series of GroupMatrix+CZ layers.

    Args:
        circuit: The circuit to transform.
        grouping: How the circuit's qubits are combined into groups.

    Returns:
        A list of layers. Each layer has a matrix to apply to each group of
        qubits, and a list of CZs to apply to pairs of qubits crossing
        between groups.
    """
    frontier = {q: 0 for q in circuit.all_qubits()}

    layers = []
    while True:
        # Pull within-group operations into per-group matrices.
        any_group_matrices = False
        group_matrices = []
        for g in grouping.groups:
            # Scan for reachable operations contained within the group qubits.
            start_frontier = {q: frontier[q] for q in g}
            end_frontier = circuit.reachable_frontier_from(start_frontier)
            mergeable_ops = circuit.findall_operations_between(start_frontier,
                                                               end_frontier)

            # Advance frontier.
            for q, v in end_frontier.items():
                frontier[q] = v

            # Fold reachable operations into a single group matrix.
            group_matrix = np.eye(1 << len(g)).reshape((2, 2) * len(g))
            if mergeable_ops:
                any_group_matrices = True
            for _, op in mergeable_ops:
                group_matrix = linalg.targeted_left_multiply(
                    left_matrix=protocols.unitary(op).reshape(
                        (2, 2) * len(op.qubits)),
                    right_target=group_matrix,
                    target_axes=[grouping.loc(q)[1] for q in op.qubits])
            group_matrices.append(np.transpose(group_matrix.reshape(
                1 << len(g), 1 << len(g))))

        # Scan for reachable CZ operations between groups.
        end_frontier = circuit.reachable_frontier_from(
            frontier,
            is_blocker=lambda op: grouping.all_in_same_group(*op.qubits))
        cz_ops = circuit.findall_operations_between(frontier, end_frontier)

        # Advance frontier.
        frontier = end_frontier

        # List out qubit index pairs for each CZ.
        cz_indices = []
        for _, cz in cz_ops:
            a, b = cz.qubits
            assert cz == ops.CZ(a, b)
            cz_indices.append((grouping.ind(a), grouping.ind(b)))

        # Combine group and CZ operations into a simulation layer.
        if not any_group_matrices and not cz_indices:
            break
        layer = _TransformsThenCzs(group_matrices=group_matrices,
                                   cz_indices=cz_indices)
        layers.append(layer)

    # We should have processed the whole circuit.
    assert frontier == {q: len(circuit) for q in circuit.all_qubits()}

    return layers