Saltar al contenido principal

Corte de cables para la estimación de valores esperados

Estimación de uso: un minuto en un procesador Eagle (NOTA: Esto es solo una estimación. Su tiempo de ejecución puede variar.)

Antecedentes

Circuit-knitting es un término general que engloba varios métodos para particionar un circuito en múltiples subcircuitos más pequeños que involucran menos compuertas y/o qubits. Cada uno de los subcircuitos puede ejecutarse de forma independiente y el resultado final se obtiene mediante algún posprocesamiento clásico sobre el resultado de cada subcircuito. Esta técnica es accesible en el complemento de Qiskit para corte de circuitos, y se proporciona una explicación detallada de la técnica en la documentación junto con otro material introductorio.

Este cuaderno trata sobre un método llamado corte de cables donde el circuito se particiona a lo largo del cable [1], [2]. Tenga en cuenta que la partición es simple en circuitos clásicos ya que el resultado en el punto de partición puede determinarse de forma determinista, y es 0 o 1. Sin embargo, el estado del qubit en el punto del corte es, en general, un estado mixto. Por lo tanto, cada subcircuito necesita ser medido múltiples veces en diferentes bases (generalmente un conjunto tomográficamente completo de bases como la base de Pauli [3], [4]) y correspondientemente preparado en su autoestado. La figura a continuación (cortesía: Tesis de PhD, Ritajit Majumdar) muestra un ejemplo de corte de cables para un estado GHZ de 4 qubits dividido en tres subcircuitos. Aquí MjM_j denota un conjunto de bases (generalmente Pauli X, Y y Z) y PiP_i denota un conjunto de autoestados (generalmente 0|0\rangle, 1|1\rangle, +|+\rangle y +i|+i\rangle).

wc-1.png wc-2.png

Dado que cada subcircuito tiene menos qubits y/o compuertas, se espera que sean menos susceptibles al ruido. Este cuaderno muestra un ejemplo donde este método puede utilizarse para suprimir eficazmente el ruido en el sistema.

Requisitos

Antes de comenzar este tutorial, asegúrese 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 de Qiskit para corte de circuitos v0.9.0 o posterior (pip install qiskit-addon-cutting)

Consideraremos un circuito de Localización de Muchos Cuerpos (MBL, por sus siglas en inglés) para este cuaderno. El circuito MBL es un circuito eficiente en hardware y está parametrizado por dos parámetros θ\theta y ϕ\vec{\phi}. Cuando θ\theta se establece en 00 y el estado inicial se prepara en 0|0\rangle para todos los qubits, el valor esperado ideal de Zi\langle Z_i \rangle es +1+1 para cada sitio de qubit ii independientemente de los valores de ϕ\vec{\phi}. Puede consultar más detalles sobre los circuitos MBL en este artículo.

Configuración

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-cutting qiskit-ibm-runtime
import numpy as np
import matplotlib.pyplot as plt

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.quantum_info import PauliList, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.result import sampled_expectation_value

from qiskit_addon_cutting.instructions import CutWire
from qiskit_addon_cutting import (
cut_wires,
expand_observables,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2, Batch

class MBLChainCircuit(QuantumCircuit):
def __init__(
self, num_qubits: int, depth: int, use_cut: bool = False
) -> None:
super().__init__(
num_qubits, name=f"MBLChainCircuit<{num_qubits}, {depth}>"
)
evolution = MBLChainEvolution(num_qubits, depth, use_cut)
self.compose(evolution, inplace=True)

class MBLChainEvolution(QuantumCircuit):
def __init__(self, num_qubits: int, depth: int, use_cut) -> None:
super().__init__(
num_qubits, name=f"MBLChainEvolution<{num_qubits}, {depth}>"
)

theta = Parameter("θ")
phis = ParameterVector("φ", num_qubits)

for layer in range(depth):
layer_parity = layer % 2
# print("layer parity", layer_parity)
for qubit in range(layer_parity, num_qubits - 1, 2):
# print(qubit)
self.cz(qubit, qubit + 1)
self.u(theta, 0, np.pi, qubit)
self.u(theta, 0, np.pi, qubit + 1)
if (
use_cut
and layer_parity == 0
and (
qubit == num_qubits // 2 - 1
or qubit == num_qubits // 2
)
):
self.append(CutWire(), [num_qubits // 2])
if use_cut and layer < depth - 1 and layer_parity == 1:
if qubit == num_qubits // 2:
self.append(CutWire(), [qubit])
for qubit in range(num_qubits):
self.p(phis[qubit], qubit)

Parte I. Ejemplo a pequeña escala

Paso 1: Transformar las entradas clásicas en un problema cuántico

Inicialmente construimos un circuito plantilla sin valores de parámetros específicos. También proporcionamos marcadores de posición, llamados CutWire, para anotar la posición de los cortes. Para el ejemplo a pequeña escala consideramos un circuito MBL de 10 qubits.

num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)

Output of the previous code cell

Recuerde que nuestro objetivo es encontrar el valor esperado del observable 1ni=1nZi\frac{1}{n}\sum_{i=1} ^n Z_i cuando θ=0\theta=0. Asignaremos algunos valores aleatorios para el parámetro ϕ\vec{\phi}.

phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
params
[0,
0.2376615174332788,
0.28244289857682414,
0.019248960591717768,
0.46140600996102477,
0.31408025180068433,
0.718184005135733,
0.991153920182475,
0.09289485768301442,
0.8857848280067783,
0.6177529765767047]

Ahora anotamos el circuito para el corte insertando las instrucciones CutWire apropiadas para crear dos cortes aproximadamente iguales. Establecemos use_cut=True en la función y permitimos que anote después de n2\frac{n}{2} qubits, siendo nn el número de qubits en el circuito original.

mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)

Output of the previous code cell

Paso 2: Optimizar el problema para la ejecución en hardware cuántico

A continuación, cortamos el circuito en dos subcircuitos más pequeños. Para este ejemplo, nos limitamos a solo 2 subcircuitos. Para ello, utilizamos el Complemento de Qiskit: Corte de Circuitos.

Cortar el circuito en subcircuitos más pequeños

Cortar el cable en un punto aumenta el recuento de qubits en uno. Además del qubit original, ahora hay un qubit adicional como marcador de posición para el circuito después del corte. La siguiente imagen proporciona una representación:

wc-4.png

Este complemento utiliza la función cut_wires para tener en cuenta los qubits adicionales que surgen debido al corte.

mbl_move = cut_wires(mbl_cut)

Crear y expandir los observables

Ahora construimos el observable Mz=1ni=1nZiM_z = \frac{1}{n}\sum_{i=1}^n \langle Z_i \rangle. Dado que el resultado ideal de Zi\langle Z_i \rangle para cada ii es +1+1, el resultado ideal de MzM_z también es +1+1.

observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
observable
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])

Sin embargo, tenga en cuenta que el número de qubits en el circuito ha aumentado después de insertar las operaciones virtuales Move de 2 qubits tras el corte. Por lo tanto, necesitamos expandir los observables también insertando identidades para ajustarlos al circuito actual.

new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])

Observe que cada observable se ha expandido para acomodar siete qubits, como en el circuito con la operación Move, en lugar de los 6 qubits originales. A continuación, particionamos el circuito en dos subcircuitos.

partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)

Visualicemos los subcircuitos

subcircuits = partitioned_problem.subcircuits
subcircuits[0].draw("mpl", fold=-1)

Output of the previous code cell

subcircuits[1].draw("mpl", fold=-1)

Output of the previous code cell

Los observables también han sido particionados para ajustarse a los subcircuitos

subobservables = partitioned_problem.subobservables
subobservables
{0: PauliList(['IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IZIIII',
'IIZIII', 'IIIZII', 'IIIIZI', 'IIIIIZ']),
1: PauliList(['ZIIII', 'IZIII', 'IIZII', 'IIIZI', 'IIIIZ', 'IIIII', 'IIIII',
'IIIII', 'IIIII', 'IIIII'])}

Tenga en cuenta que cada subcircuito genera una serie de muestras. La reconstrucción tiene en cuenta el resultado de cada una de estas muestras. Cada una de estas muestras se denomina un subexperiment. Extender el observable utilizando la operación Move requiere una estructura de datos PauliList. También podemos crear el observable MzM_z en la estructura de datos más genérica SparsePauliOp, que será útil más adelante durante la reconstrucción de los subexperimentos.

M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
M_z
SparsePauliOp(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII', 'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII', 'IIIIIIIIZI', 'IIIIIIIIIZ'],
coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
0.1+0.j, 0.1+0.j])
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)

Veamos dos ejemplos donde los qubits cortados se miden en dos bases diferentes. Primero, se mide en la base Z normal, y luego se mide en la base X.

subexperiments[0][6].draw("mpl", fold=-1)

Output of the previous code cell

subexperiments[0][2].draw("mpl", fold=-1)

Output of the previous code cell

Transpilar cada subexperimento

Actualmente necesitamos transpilar nuestros circuitos antes de enviarlos para su ejecución. Por lo tanto, transpilaremos cada circuito en los subexperimentos primero.

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)

Ahora necesitamos transpilar cada uno de los circuitos en los subexperimentos. Para ello, primero creamos un gestor de pasadas y luego lo utilizamos para transpilar cada uno de los circuitos.

pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
isa_subexperiments[0][0].draw("mpl", fold=-1, idle_wires=False)

Output of the previous code cell

Paso 3: Ejecutar utilizando primitivas de Qiskit

Ahora ejecutaremos cada circuito en el subexperimento. Qiskit-addon-cutting utiliza SamplerV2 para ejecutar los subexperimentos.

with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}

Paso 4: Posprocesar y devolver el resultado en el formato clásico deseado

Una vez que los circuitos han sido ejecutados, ahora necesitamos recuperar los resultados y reconstruir el valor esperado para el circuito sin cortar y el observable original.

# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9674376845359803

Verificación cruzada

Ahora ejecutemos el circuito sin cortar y comprobemos el resultado obtenido. Tenga en cuenta que para la ejecución del circuito sin cortar podemos utilizar directamente EstimatorV2 para calcular los valores esperados. Pero utilizaremos la misma primitiva en todo momento. Por lo tanto, utilizaremos SamplerV2 para obtener la distribución de probabilidad y calcular el valor esperado usando la función sampled_expectation_value.

Primero necesitamos transpilar el circuito mbl sin cortar.

sampler = SamplerV2(mode=backend)

if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)

A continuación, construimos el pub y ejecutamos el circuito sin cortar.

pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9498046875000001

Observamos que el valor esperado obtenido mediante el corte de cables es más cercano al valor ideal de +1+1 que el obtenido sin cortar. Ahora escalemos el tamaño del problema.

Parte II. ¡Escalemos!

Anteriormente, mostramos los resultados para un circuito MBL de 10 qubits. A continuación, demostramos que la mejora en el valor esperado también se obtiene para circuitos más grandes. Para mostrar esto, repetimos el proceso para un circuito MBL de 60 qubits.

Paso 1: Transformar las entradas clásicas en un problema cuántico

num_qubits = 60
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)

Creamos un conjunto aleatorio de valores para ϕ\vec{\phi}

phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis

A continuación, construimos el circuito con cortes

mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)

Paso 2: Optimizar el problema para la ejecución en hardware cuántico

Como se mostró en el ejemplo a pequeña escala, particionamos el circuito y el observable para los experimentos de corte.

mbl_move = cut_wires(mbl_cut)

# Define observable
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)

# Partition the circuit into subcircuits
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)

# Get subcircuits
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables

También creamos un objeto SparsePauliOp para el observable con los coeficientes apropiados.

M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)

A continuación, generamos los subexperimentos y transpilamos cada circuito en el subexperimento.

subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}

Paso 3: Ejecutar utilizando primitivas de Qiskit

Utilizamos el modo Batch para ejecutar todos los circuitos en los subexperimentos.

with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}

Paso 4: Posprocesar y devolver el resultado en el formato clásico deseado

Ahora recuperemos los resultados de cada circuito en el subexperimento y reconstruyamos el valor esperado correspondiente al circuito sin cortar y al observable original.

# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9631355921427409

Verificación cruzada

Al igual que en el ejemplo a pequeña escala, obtendremos una vez más el valor esperado ejecutando el circuito sin cortar y compararemos el resultado con el corte de circuitos. Utilizaremos SamplerV2 para mantener la uniformidad en el uso de las primitivas.

sampler = SamplerV2(mode=backend)

if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)

pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9426757812499998

Visualización

Visualicemos la mejora obtenida en el valor esperado mediante el uso del corte de cables.

ax = plt.gca()
methods = ["cut", "uncut"]
values = [reconstructed_expval, uncut_expval]

plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
ax.set_ylim([0.85, 1.02])
plt.text(0.3, 0.99, "Exact result")
plt.show()

Output of the previous code cell

Inferencia

Observamos que tanto en los problemas a pequeña como a gran escala, el corte de cables conduce a un mejor resultado que el obtenido sin cortar. Tenga en cuenta que no se han utilizado técnicas de mitigación de errores para estos experimentos. Por lo tanto, la mejora en el resultado que se ha obtenido se debe únicamente al corte de cables. Es posible mejorar aún más los resultados utilizando diferentes métodos de mitigación junto con el corte de circuitos.

Además, en este cuaderno, calculamos ambos subcircuitos en el mismo hardware. En [5], [6], los autores muestran un método para distribuir los subcircuitos en diferente hardware utilizando información de ruido con el fin de maximizar la supresión de ruido y paralelizar el proceso.

Apéndice: consideraciones sobre el escalamiento de recursos

El número de circuitos a ejecutar aumenta con el número de cortes. Por lo tanto, aunque muchos cortes pueden producir subcircuitos pequeños, mejorando así aún más el rendimiento, también conduce a un número significativamente alto de ejecuciones de circuitos, lo cual puede no ser práctico en la mayoría de los casos. A continuación, mostramos un ejemplo del número de subcircuitos correspondiente al número de cortes para un circuito de 50 qubits.

wc-5.png

Tenga en cuenta que incluso con cinco cortes, el número de subexperimentos es de alrededor de 200.000. Por lo tanto, el corte de circuitos debe utilizarse solo cuando el número de cortes es pequeño.

Un ejemplo de circuito favorable y otro desfavorable para el corte

Circuito favorable para el corte

Como se señaló anteriormente, un circuito es favorable para el corte cuando puede particionarse en subcircuitos disjuntos más pequeños con un número reducido de cortes. Cualquier circuito eficiente en hardware, es decir, un circuito que requiere pocas o ninguna compuerta SWAP cuando se mapea al mapa de acoplamiento del hardware, es, en general, favorable para el corte. A continuación, mostramos un ejemplo de un ansatz de preservación de excitación, que se utiliza en Química Cuántica. Tenga en cuenta que dicho circuito puede particionarse en dos subcircuitos con un solo corte, independientemente del número de qubits.

wc-6.png

Circuito desfavorable para el corte

Un circuito es desfavorable para el corte si, en general, el número de cortes necesarios para formar particiones disjuntas crece significativamente con la profundidad o el número de qubits. Recuerde que con cada corte se requiere un qubit adicional. Así, con el número de cortes, el número efectivo de qubits también aumenta. A continuación, mostramos un ejemplo de un circuito de Grover de 3 qubits con una posible instancia de corte.

wc-7.png

Observamos que se requieren tres cortes, y el corte es más vertical que horizontal. Esto significa que se espera que el número de cortes escale linealmente con el número de qubits, lo cual no es favorable para el corte.

Referencias

[1] Peng, T., Harrow, A. W., Ozols, M., & Wu, X. (2020). Simulating large quantum circuits on a small quantum computer. Physical review letters, 125(15), 150504.

[2] Tang, W., Tomesh, T., Suchara, M., Larson, J., & Martonosi, M. (2021, April). Cutqc: using small quantum computers for large quantum circuit evaluations. In Proceedings of the 26th ACM International conference on architectural support for programming languages and operating systems (pp. 473-486).

[3] Perlin, M. A., Saleem, Z. H., Suchara, M., & Osborn, J. C. (2021). Quantum circuit cutting with maximum-likelihood tomography. npj Quantum Information, 7(1), 64.

[4] Majumdar, R., & Wood, C. J. (2022). Error mitigated quantum circuit cutting. arXiv preprint arXiv:2211.13431.

[5] Khare, T., Majumdar, R., Sangle, R., Ray, A., Seshadri, P. V., & Simmhan, Y. (2023). Parallelizing Quantum-Classical Workloads: Profiling the Impact of Splitting Techniques. In 2023 IEEE International Conference on Quantum Computing and Engineering (QCE) (Vol. 1, pp. 990-1000). IEEE.

[6] Bhoumik, D., Majumdar, R., Saha, A., & Sur-Kolay, S. (2023). Distributed Scheduling of Quantum Circuits with Noise and Time Optimization. arXiv preprint arXiv:2309.06005.

Encuesta del tutorial

Por favor, realice esta breve encuesta para proporcionar comentarios sobre este tutorial. Sus opiniones nos ayudarán a mejorar nuestras ofertas de contenido y la experiencia del usuario.

Link to survey