Corte de circuitos para reducir la profundidad
Estimación de uso: Ocho minutos en un procesador Eagle (NOTA: Esto es sólo una estimación. Su tiempo de ejecución puede variar)
En segundo plano
Este tutorial muestra cómo construir un Qiskit pattern para cortar puertas en un circuito cuántico para reducir la profundidad del circuito. Para obtener una discusión más detallada sobre el corte de circuitos, visita la documentación del complemento Qiskit de corte de circuitos.
Requisitos
Antes de empezar este tutorial, asegúrate de tener instalado lo siguiente:
- Qiskit SDK v2.0 o posterior, con soporte de visualización
- Qiskit Runtime v0.22 o posterior (
pip install qiskit-ibm-runtime) - Complemento Qiskit para corte de circuitos v0.9.0 o posterior (
pip install qiskit-addon-cutting)
Configuración
import numpy as np
from qiskit.circuit.library import EfficientSU2
from qiskit.quantum_info import PauliList, Statevector, SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_addon_cutting import (
cut_gates,
generate_cutting_experiments,
reconstruct_expectation_values,
)
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2Paso 1: Asignar entradas clásicas a un problema cuántico
Implementaremos nuestro patrón Qiskit siguiendo los cuatro pasos descritos en la documentación. En este caso, simularemos valores de expectativa en un circuito de cierta profundidad cortando puertas que den lugar a puertas de intercambio y ejecutando subexperimentos en circuitos menos profundos. El corte de puertas es relevante para los pasos 2 (optimizar el circuito para la ejecución cuántica descomponiendo las puertas distantes) y 4 (post-procesamiento para reconstruir los valores de expectativa en el circuito original). En el primer paso, generaremos un circuito a partir de la librería de circuitos Qiskit y definiremos algunos observables.
- Entrada: Parámetros clásicos para definir un circuito
- Salida: Circuito abstracto y observables
circuit = EfficientSU2(num_qubits=4, entanglement="circular").decompose()
circuit.assign_parameters([0.4] * len(circuit.parameters), inplace=True)
observables = PauliList(["ZZII", "IZZI", "IIZZ", "XIXI", "ZIZZ", "IXIX"])
circuit.draw("mpl", scale=0.8, style="iqp")Output:
Paso 2: Optimizar el problema para la ejecución en hardware cuántico
- Entrada: Circuito abstracto y observables
- Resultado: Circuito objetivo y observables producidos cortando puertas distantes para reducir la profundidad del circuito transpilado
Elegimos una disposición inicial que requiere dos intercambios para ejecutar las puertas entre los qubits 3 y 0 y otros dos intercambios para devolver los qubits a sus posiciones iniciales. Elegimos optimization_level=3, que es el nivel más alto de optimización disponible con un gestor de pases preestablecido.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, min_num_qubits=circuit.num_qubits, simulator=False
)
pm = generate_preset_pass_manager(
optimization_level=3, initial_layout=[0, 1, 2, 3], backend=backend
)
transpiled_qc = pm.run(circuit)
print(f"Transpiled circuit depth: {transpiled_qc.depth()}")
transpiled_qc.draw("mpl", scale=0.4, idle_wires=False, style="iqp", fold=-1)Output:
Transpiled circuit depth: 103
Buscar y cortar las puertas distantes: Reemplazaremos las puertas distantes (puertas que conectan qubits no locales, 0 y 3) con TwoQubitQPDGate objetos especificando sus índices. cut_gates Reemplazará las puertas en los índices especificados con TwoQubitQPDGate objetos y también devolverá una lista de QPDBasis instancias, una por cada descomposición de puerta. El QPDBasis objeto contiene información sobre cómo descomponer las puertas cortadas en operaciones de un solo qubit.
# 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)Output:
Generar los subexperimentos que se ejecutarán en el backend : generate_cutting_experiments acepta un circuito que contiene instancias y observables de TwoQubitQPDGate como PauliList.
Para simular el valor esperado del circuito de tamaño completo, se generan muchos subexperimentos a partir de la distribución de cuasiprobabilidad conjunta de las puertas descompuestas y, a continuación, se ejecutan en uno o más backends. El número de muestras tomadas de la distribución se controla mediante num_samples, y se da un coeficiente combinado para cada muestra única. Para más información sobre cómo se calculan los coeficientes, consulte el material explicativo.
# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observables, num_samples=np.inf
)Para comparar, vemos que los subexperimentos QPD serán menos profundos después de cortar puertas distantes : He aquí un ejemplo de subexperimento elegido arbitrariamente y generado a partir del circuito QPD. Su profundidad se ha reducido a más de la mitad. Hay que generar y evaluar muchos de estos subexperimentos probabilísticos para reconstruir un valor de expectativa del circuito más profundo.
# Transpile the decomposed circuit to the same layout
transpiled_qpd_circuit = pm.run(subexperiments[100])
print(f"Original circuit depth after transpile: {transpiled_qc.depth()}")
print(
f"QPD subexperiment depth after transpile: {transpiled_qpd_circuit.depth()}"
)
transpiled_qpd_circuit.draw(
"mpl", scale=0.6, style="iqp", idle_wires=False, fold=-1
)Output:
Original circuit depth after transpile: 103
QPD subexperiment depth after transpile: 46
Por otro lado, el corte hace necesario un muestreo adicional. Aquí cortamos tres puertas CNOT, lo que resulta en una sobrecarga de muestreo de . Para más información sobre la sobrecarga de muestreo incurrida por el corte de circuitos, consulte la documentación de Circuit Knitting Toolbox.
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")Output:
Sampling overhead: 729.0
Paso 3: Ejecutar utilizando Qiskit primitives
Ejecuta los circuitos de destino («subexperimentos») con la primitiva Sampler.
- Entrada: Circuitos objetivo
- Resultados: Distribuciones de cuasiprobabilidad
# Transpile the subexperiments to the backend's instruction set architecture (ISA)
isa_subexperiments = pm.run(subexperiments)
# 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()print(job.job_id())Output:
czypg1r6rr3g008mgp6g
Paso 4: Procesamiento posterior y devolución del resultado en el formato clásico deseado
Utiliza los resultados del subexperimento, las subobservables y los coeficientes de muestreo para reconstruir el valor esperado del circuito original.
Entrada: Distribuciones de cuasiprobabilidad Salida: Valores de expectativa reconstruidos
reconstructed_expvals = reconstruct_expectation_values(
results,
coefficients,
observables,
)
# Reconstruct final expectation value
final_expval = np.dot(reconstructed_expvals, [1] * len(observables))
print("Final reconstructed expectation value")
print(final_expval)Output:
Final reconstructed expectation value
1.0751342773437473
ideal_expvals = [
Statevector(circuit).expectation_value(SparsePauliOp(observable))
for observable in observables
]
print("Ideal expectation value")
print(np.dot(ideal_expvals, [1] * len(observables)).real)Output:
Ideal expectation value
1.2283177520039992
Encuesta tutorial
Responda a esta breve encuesta para darnos su opinión sobre este tutorial. Su opinión nos ayudará a mejorar nuestra oferta de contenidos y la experiencia de los usuarios.