Source code for QCut.single_qubit_wirecut

from __future__ import annotations

from collections import namedtuple
from copy import deepcopy

import numpy as np
from qiskit import ClassicalRegister, QuantumCircuit
from qiskit.circuit import CircuitInstruction, Qubit
from qiskit.converters import circuit_to_dag, dag_to_circuit
from qiskit_aer import AerSimulator

from QCut.backend_utility import transpile_experiments
from QCut.cutcircuit import CutCircuit
from QCut.cutlocation import SingleQubitCutLocation
from QCut.qcuterror import QCutError
from QCut.wirecut import (
    estimate_expectation_values,
    get_experiment_circuits,
    run_experiments,
)

BitLocations = namedtuple("BitLocations", ("index", "registers"))


def _get_cut_locations(circuit):
    index = 0  # index of the current instruction in circuit_data
    circuit_data = circuit.data
    cut_locations = np.array([])

    # loop through circuit instructions
    # if operation is a Cut() instruction remove it and add registers and
    # offset index to cut_locations

    # rename varibales to be more descriptive (namely qs)
    while index < len(circuit):
        if circuit_data[index].operation.name == "Cut":
            # find qubits for Cut operation
            qubits = [
                circuit.find_bit(qubit).registers[0]
                for qubit in circuit_data[index].qubits
            ]

            # remove the cut operation
            circuit_data.remove(circuit_data[index])

            # append to cut_locations
            cut_locations = np.append(
                cut_locations, SingleQubitCutLocation((qubits[0], index))
            )

            # adjust index to account for removed operation
            index -= 1
        index += 1

    return cut_locations


def _insert_cut_nodes(circuit, cut_locations):
    placeholder_locations = []
    circuit_data = circuit.data
    cut_index = 0
    offset = 0
    for cut_location in cut_locations:
        cur_placeholder = ()
        measure_node = QuantumCircuit(1, name=f"Meas_{cut_index}").to_instruction()
        initialize_node = QuantumCircuit(1, name=f"Init_{cut_index}").to_instruction()
        cut_index += 1

        circuit_data.insert(
            cut_location.index + offset,
            CircuitInstruction(
                operation=measure_node,
                qubits=[Qubit(cut_location.qubits[0], cut_location.qubits[1])],
            ),
        )
        meas_plcaholder = cut_location.index + offset
        circuit_data.insert(
            cut_location.index + offset + 1,
            CircuitInstruction(
                operation=initialize_node,
                qubits=[Qubit(cut_location.qubits[0], cut_location.qubits[1])],
            ),
        )
        init_placeholder = cut_location.index + offset + 1

        cur_placeholder = (meas_plcaholder, init_placeholder)
        placeholder_locations.append(cur_placeholder)

        offset += 2

    return circuit, placeholder_locations


def _move_to_new_wire(circuit, num_cuts):  # noqa: C901
    count = 0
    # qr = QuantumRegister(num_cuts, "qpd")
    # circuit.qregs.append(qr)
    i = 0
    j = 0
    all_cut_qubits = []
    while i < len(circuit):
        ind, op = list(enumerate(circuit.data))[i]
        if "Meas" in op.operation.name:
            consecutive_cuts = 0
            cut_qubits = []
            j = 0
            while "Meas" in circuit.data[ind + j].operation.name:
                cut_qubits.append(circuit.data[ind + j].qubits[0])
                all_cut_qubits.append(
                    circuit.find_bit(circuit.data[ind + j].qubits[0]).index
                )
                consecutive_cuts += 1
                j += 2
            for _ in range(consecutive_cuts):
                q = Qubit()
                circuit.add_bits([q])
                circuit.qubits.remove(q)
                circuit.qubits.insert(circuit.qubits.index(cut_qubits[-1]) + 1, q)
            check = 0
            for subind, subop in enumerate(circuit.data[ind::]):
                if "Meas" in subop.operation.name and check < consecutive_cuts:
                    check += 1
                    continue
                new_qubs = []
                for qub in subop.qubits:
                    if qub in cut_qubits:
                        q_ind = cut_qubits.index(qub) + 1
                        new_qubs.append(
                            circuit.qubits[circuit.qubits.index(cut_qubits[-1]) + q_ind]
                        )
                    else:
                        new_qubs.append(qub)
                if new_qubs != subop.qubits:
                    circuit.data[ind + subind] = CircuitInstruction(
                        subop.operation, new_qubs
                    )

            count += consecutive_cuts
        if j != 0:
            i += j
            j = 0
        else:
            i += 1
    return circuit


def count_gates(qc: QuantumCircuit):
    gate_count = {qubit: 0 for qubit in qc.qubits}
    for gate in qc.data:
        for qubit in gate.qubits:
            gate_count[qubit] += 1
    return gate_count


def _remove_idle_wires(qc: QuantumCircuit):
    qc_out = deepcopy(qc)
    gate_count = count_gates(qc_out)
    for qubit, count in gate_count.items():
        if count == 0:
            qc_out.qubits.remove(qubit)
            for i in qc_out.qregs:
                if qubit in i._bits:
                    i._bits.remove(qubit)
    qc_out.qregs[0]._bit_indices = {
        qubit: qc_out.qubits.index(qubit) for qubit in qc_out.qubits
    }
    qc_out.qregs[0]._bits = qc_out.qubits
    qc_out.qregs[0]._size = len(qc_out.qregs[0]._bits)
    return qc_out


def _separate_subcircuits(circuit):
    dag = circuit_to_dag(circuit)

    circs = dag.separable_circuits()

    new_circs = []
    for i in circs:
        circ = _remove_idle_wires(dag_to_circuit(i))
        if len(circ.qubits) == 0:
            continue
        new_circs.append(circ)

    return new_circs


def _add_cbits(subcircuits):
    for circ in subcircuits:
        clbits = 0
        for i in circ:
            if "Meas" in i.operation.name:
                clbits += 1
        circ.add_register(ClassicalRegister(clbits, "qpd_meas"))
        circ.add_register(ClassicalRegister(circ.num_qubits - clbits, "meas"))

    return subcircuits


def get_qubit_map(subcircuits: list[QuantumCircuit]):
    def filter_obs_i(qc_data):
        return [i for i in qc_data if "obs" in i.operation.name]

    def sort_func(obs):
        return int(obs.operation.name.split("_")[1])

    map_qubit = {}
    count = 0
    for ind, i in enumerate(reversed(subcircuits)):
        for j in sorted(filter_obs_i(i.data), key=sort_func, reverse=True):
            map_qubit[int(j.operation.name.split("_")[1])] = count
            count += 1

    return map_qubit


[docs] def get_locations_and_subcircuits( circuit: QuantumCircuit, ): """Get cut locations and subcircuits with placeholder operations. Args: circuit (QuantumCircuit): circuit with cuts inserted Returns: tuple: A tuple containing: - list[SingleQubitCutLocation]: Locations of the cuts as a list - list[QuantumCircuit]: Subcircuits with placeholder operations """ circuit = circuit.copy() # copy to avoid modifying the original circuit for i in range(circuit.num_qubits): obs_m = QuantumCircuit(1, name=f"obs_{i}") obs_m = obs_m.to_instruction() circuit.append(obs_m, [i]) cut_locations = _get_cut_locations(circuit) circuit1, _placeholder_locations = _insert_cut_nodes(circuit, cut_locations) circuit = _move_to_new_wire(circuit1.copy(), len(cut_locations)) subcircuits = _separate_subcircuits(circuit) subcircuits = _add_cbits(subcircuits) fixed_circs = [] for i in subcircuits: test = QuantumCircuit(i.num_qubits) test.add_register(i.cregs[0]) test.add_register(i.cregs[1]) for j in i.data: qubits = [test.qubits[i.qubits.index(q)] for q in j.qubits] test.append(CircuitInstruction(j.operation, qubits)) fixed_circs.append(test) if len(fixed_circs) <= 1: raise QCutError( "Invalid cuts. Check documentation to see how cuts should be placed." ) map_qubits = get_qubit_map(fixed_circs) return cut_locations, fixed_circs, map_qubits
[docs] def run_cut_circuit( subcircuits: list[QuantumCircuit], cut_locations: np.ndarray[SingleQubitCutLocation], observables: list[int | list[int]], map_qubits: dict[int, int], backend=AerSimulator(), mitigate: bool = False, ) -> np.ndarray[float]: """After splitting the circuit run the rest of the circuit knitting sequence. Args: subcircuits (list[QuantumCircuit]): subcircuits containing the placeholder operations cut_locations (np.ndarray[CutLocation]): list of cut locations observables (list[int | list[int]]): list of observables as qubit indices (Z observable) backend: backend to use for running experiment circuits (optional) mitigate (bool): wether or not to use readout error mitigation (optional) Returns: list: a list of expectation values """ subexperiments, coefs, id_meas = get_experiment_circuits(subcircuits, cut_locations) if not isinstance(backend, AerSimulator): subexperiments = transpile_experiments(subexperiments.circuits, backend) subexperiments = CutCircuit(subexperiments) results = run_experiments( subexperiments, cut_locations, id_meas=id_meas, backend=backend, mitigate=mitigate, ) return estimate_expectation_values( results, coefs, cut_locations, observables, map_qubits )
[docs] def run( circuit: QuantumCircuit, observables: list[int, list[int]], backend=AerSimulator(), mitigate: bool = False, ) -> list[float]: """Run the whole circuit knitting sequence with one function call. Args: circuit (QuantumCircuit): circuit with cut experiments observables (list[int | list[int]]): list of observbles in the form of qubit indices (Z-obsevable). backend: backend to use for running experiment circuits (optional) mitigate (bool): wether or not to use readout error mitigation (optional) Returns: list: a list of expectation values """ # circuit = circuit.copy() qss, circs, map_qubits = get_locations_and_subcircuits(circuit) return run_cut_circuit(circs, qss, observables, map_qubits, backend, mitigate)