Saltar al contenido principal

Gate Cutting para Reducir la Profundidad del Circuit

En este tutorial, reduciremos la profundidad de un Circuit cortando Gates distantes, evitando los swap gates que de otro modo serían introducidos por el enrutamiento.

Estos son los pasos que seguiremos en este patrón de Qiskit:

  • Paso 1: Mapear el problema a circuits cuánticos y operadores:
    • Mapear el hamiltoniano a un Circuit cuántico.
  • Paso 2: Optimizar para el hardware objetivo [Usa el cutting addon]:
    • Cortar el Circuit y el observable.
    • Transpilar los subexperimentos para el hardware.
  • Paso 3: Ejecutar en el hardware objetivo:
    • Ejecutar los subexperimentos obtenidos en el Paso 2 usando una primitiva Sampler.
  • Paso 4: Post-procesar los resultados [Usa el cutting addon]:
    • Combinar los resultados del Paso 3 para reconstruir el valor esperado del observable en cuestión.

Paso 1: Mapear

Crear un Circuit para ejecutar en el Backend

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
from qiskit.circuit.library import efficient_su2

circuit = efficient_su2(num_qubits=4, entanglement="circular")
circuit.assign_parameters([0.4] * len(circuit.parameters), inplace=True)
circuit.draw("mpl", scale=0.8)

Diagrama del Circuit cuántico

Especificar un observable

from qiskit.quantum_info import SparsePauliOp

observable = SparsePauliOp(["ZZII", "IZZI", "-IIZZ", "XIXI", "ZIZZ", "IXIX"])

Paso 2: Optimizar

Especificar un Backend

Puedes proporcionar un Backend falso o un Backend de hardware de Qiskit Runtime.

from qiskit_ibm_runtime.fake_provider import FakeManilaV2

backend = FakeManilaV2()

Transpilar el Circuit, visualizar los swaps y observar la profundidad

Elegimos un layout que requiere dos swaps para ejecutar los Gates entre los Qubits 3 y 0, y otros dos swaps para devolver los Qubits a sus posiciones iniciales.

from qiskit.transpiler import generate_preset_pass_manager

pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend, initial_layout=[0, 1, 2, 3]
)

transpiled_qc = pass_manager.run(circuit)
print(f"Transpiled circuit depth: {transpiled_qc.depth(lambda x: len(x.qubits) >= 2)}")
Transpiled circuit depth: 30
transpiled_qc.draw("mpl", scale=0.4, idle_wires=False, fold=-1)

Diagrama del Circuit cuántico

Reemplazar Gates distantes con TwoQubitQPDGates especificando sus índices

cut_gates reemplazará los Gates en los índices especificados con TwoQubitQPDGates y también devolverá una lista de instancias de QPDBasis, una por cada descomposición de Gate.

from qiskit_addon_cutting import cut_gates

# Find the indices of the distant gates
cut_indices = [
i
for i, instruction in enumerate(circuit.data)
if {circuit.find_bit(q)[0] for q in instruction.qubits} == {0, 3}
]

# Decompose distant CNOTs into TwoQubitQPDGate instances
qpd_circuit, bases = cut_gates(circuit, cut_indices)

qpd_circuit.draw("mpl", scale=0.8)

Diagrama del Circuit cuántico

Generar los subexperimentos para ejecutar en el Backend

generate_cutting_experiments acepta un Circuit que contiene instancias de TwoQubitQPDGate y observables como una PauliList.

Para simular el valor esperado del Circuit de tamaño completo, se generan muchos subexperimentos a partir de la distribución de cuasiprobabilidad conjunta de los Gates descompuestos y luego se ejecutan en uno o más Backends. El número de muestras tomadas de la distribución se controla con num_samples, y se da un coeficiente combinado para cada muestra única. Para más información sobre cómo se calculan los coeficientes, consulta el material explicativo.

Nota: El argumento observables de generate_cutting_experiments es de tipo PauliList. Los coeficientes y las fases de los términos del observable se ignoran durante la descomposición del problema y la ejecución de los subexperimentos. Pueden volver a aplicarse durante la reconstrucción del valor esperado.

import numpy as np
from qiskit_addon_cutting import generate_cutting_experiments

# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observable.paulis, num_samples=np.inf
)

Calcular la sobrecarga de muestreo para los cortes elegidos

Aquí cortamos tres Gates CNOT, lo que resulta en una sobrecarga de muestreo de 939^3.

Para más información sobre la sobrecarga de muestreo generada por el circuit cutting, consulta el material explicativo.

print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
Sampling overhead: 729.0

Demostrar que los subexperimentos QPD serán más superficiales después de cortar los Gates distantes

Aquí hay un ejemplo de un subexperimento elegido arbitrariamente generado a partir del Circuit QPD. Su profundidad se ha reducido en más de la mitad. Muchos de estos subexperimentos probabilísticos deben generarse y evaluarse para reconstruir un valor esperado del Circuit más profundo.

# Transpile the decomposed circuit to the same layout
transpiled_qpd_circuit = pass_manager.run(subexperiments[100])

print(
f"Original circuit depth after transpile: {transpiled_qc.depth(lambda x: len(x.qubits) >= 2)}"
)
print(
f"QPD subexperiment depth after transpile: {transpiled_qpd_circuit.depth(lambda x: len(x.qubits) >= 2)}"
)
transpiled_qpd_circuit.draw("mpl", scale=0.8, idle_wires=False, fold=-1)
Original circuit depth after transpile: 30
QPD subexperiment depth after transpile: 7

Diagrama del Circuit cuántico

Preparar los subexperimentos para el Backend

# Transpile the subeperiments to the backend's instruction set architecture (ISA)
isa_subexperiments = pass_manager.run(subexperiments)

Paso 3: Ejecutar

Ejecutar los subexperimentos usando la primitiva Sampler de Qiskit Runtime

from qiskit_ibm_runtime import SamplerV2

# Set up the Qiskit Runtime Sampler primitive. For a fake backend, this will use a local simulator.
sampler = SamplerV2(backend)

# Submit the subexperiments
job = sampler.run(isa_subexperiments)
# Retrieve the results
results = job.result()

Paso 4: Post-procesar

Reconstruir el valor esperado

Reconstruye los valores esperados para cada término del observable y combínalos para reconstruir el valor esperado del observable original.

from qiskit_addon_cutting import reconstruct_expectation_values

reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
observable.paulis,
)
# Reconstruct final expectation value
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)

Comparar el valor esperado reconstruido con el valor esperado exacto del Circuit y observable originales

from qiskit_aer.primitives import EstimatorV2

estimator = EstimatorV2()
exact_expval = estimator.run([(circuit, observable)]).result()[0].data.evs
print(f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}")
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}")
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 0.44018555
Exact expectation value: 0.50497603
Error in estimation: -0.06479049
Relative error in estimation: -0.12830408