Corte de cables para la estimación de valores esperados
Estimación de uso: 22 segundos en un procesador Heron (NOTA: Esto es solo una estimación. Tu tiempo de ejecución puede variar.)
Resultados de aprendizaje
Tras completar este tutorial, los usuarios deberán ser capaces de:
- Usar
qiskit-addon-cuttingpara particionar un circuito grande en subcircuitos más pequeños, reduciendo así el efecto del ruido
Requisitos previos
Sugerimos que los usuarios estén familiarizados con el siguiente tema antes de iniciar este tutorial:
- Uso de la primitiva Sampler, que se utiliza en este flujo de trabajo
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 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; consulta la documentación junto con otro material introductorio para una explicación detallada de la técnica.
Este tutorial se centra en un método llamado corte de cables, donde el circuito se particiona a lo largo del cable [1], [2]. Ten 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 medirse múltiples veces en diferentes bases (generalmente una base tomográficamente completa, como la base de Pauli [3], [4]) y prepararse correspondientemente en su autoestado. La figura a continuación (cortesía: [7]) muestra un ejemplo de corte de cables para un estado GHZ de cuatro qubits dividido en tres subcircuitos. Aquí denota un conjunto de bases (generalmente Pauli X, Y y Z) y denota un conjunto de autoestados (generalmente , , y ).
Dado que cada subcircuito tiene menos qubits y compuertas, se espera que sean menos susceptibles al ruido. Este tutorial muestra un ejemplo donde este método puede utilizarse para suprimir eficazmente el ruido en el sistema.
Requisitos
Antes de comenzar 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 de Qiskit para corte de circuitos v0.10.0 o posterior (
pip install qiskit-addon-cutting) - Qiskit addon utils 0.3 o posterior (
pip install qiskit-addon-utils) - Qiskit Aer (
pip install qiskit-aer)
Configuración
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-cutting qiskit-aer 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_aer import AerSimulator
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
Ejemplo en simulador a pequeña escala
Este tutorial implementa un patrón de Qiskit para simular un circuito de Localización de Muchos Cuerpos (MBL) unidimensional (1D). El circuito MBL es un circuito eficiente en hardware y está parametrizado por dos parámetros y . Cuando se establece en y el estado inicial se prepara en para todos los qubits, el valor esperado ideal de es para cada sitio de qubit , independientemente de los valores de . Puedes consultar más detalles sobre este circuito en este artículo.
Ten en cuenta que en un simulador sin ruido, el valor esperado obtenido con y sin corte de circuito será el mismo.
Paso 1: Transformar las entradas clásicas en un problema cuántico
Construir el circuito MBL 1D
Primero presentamos una función para construir el circuito MBL 1D.
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)
num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)
Calculamos el valor esperado promedio sobre todos los qubits para . Dado que el valor esperado ideal de , el valor esperado ideal de también es . Los parámetros se seleccionan aleatoriamente.
np.random.seed(42)
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
El circuito debe anotarse insertando instrucciones CutWire en las ubicaciones deseadas para particionarlo. Para este tutorial, optamos por una partición equitativa. El circuito MBL está diseñado de modo que al establecer use_cut=True en la función, la anotación se inserta correctamente después de qubits, siendo el número de qubits en el circuito original. También asignamos los parámetros generados aleatoriamente al circuito.
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
Cortar el circuito en subcircuitos más pequeños
Ahora particionamos el circuito en dos subcircuitos más pequeños usando qiskit-addon-cutting. qiskit-addon-cutting agrega una compuerta virtual Move para dividir la ubicación del corte de cable ajustando apropiadamente el número de qubits. Ahora creamos el circuito con esta compuerta virtual. Dado que hay un corte de cable, el número de qubits asociados aumentará en 1.
mbl_move = cut_wires(mbl_cut)
mbl_move.draw("mpl", fold=-1)
Construir y expandir el observable
El observable, tal como se definió anteriormente, será el promedio de en cada qubit. Sin embargo, al insertar la compuerta virtual Move, el número efectivo de qubits en el circuito aumenta. El observable también debe expandirse en consecuencia para tener en cuenta este cambio en el número de qubits. Observa que el observable siempre actúa de forma trivial (como ) sobre el qubit adicional añadido para la compuerta virtual Move.
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'])
new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])
Ahora el circuito puede particionarse a lo largo de la compuerta Move y obtenemos los subcircuitos, así como el subobservable, que es la porción del observable original asociada a cada subcircuito.
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
Aquí visualizamos los dos subcircuitos:
subcircuits[0].draw("mpl", fold=-1)
subcircuits[1].draw("mpl", fold=-1)
Expandir el observable usando la operación Move requiere una estructura de datos PauliList. Para reconstruir el valor esperado del circuito original, necesitamos el observable en formato SparsePauliOp.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
Como se explicó anteriormente, para cada corte el circuito aguas arriba debe medirse en una base de Pauli, y el circuito aguas abajo debe prepararse en el autoestado de esa base. La función generate_cutting_experiments crea todos estos circuitos necesarios y los coeficientes asociados a cada circuito requeridos para la reconstrucción. Consulta más detalles en este artículo.
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
Transpilar los circuitos al backend
Para el primer ejemplo que implica solo simulación, transpilamos el circuito al conjunto de compuertas base del backend:
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)
<IBMBackend('ibm_fez')>
Paso 3: Ejecutar utilizando primitivas de Qiskit
Ahora ejecutamos cada subexperimento:
pm_basis = generate_preset_pass_manager(
optimization_level=2, basis_gates=backend.configuration().basis_gates
)
basis_subexperiments = {
label: pm_basis.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
sampler = SamplerV2(mode=AerSimulator())
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in basis_subexperiments.items()
}
Paso 4: Posprocesar y devolver el resultado en el formato clásico deseado
Ahora recuperamos el resultado de cada subexperimento ejecutado y reconstruimos el valor esperado del circuito sin cortar:
# 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
np.float64(0.9953821063041687)
methods = [
"Uncut",
"Wire cut",
]
values = [
1,
reconstructed_expval,
] # since the ideal expectation value in noiseless simulation is +1
ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
ax.set_ylabel(r"$M_Z$", fontsize=12)
Text(0, 0.5, '$M_Z$')
Ejemplo en hardware real a gran escala
Ahora demostramos el corte de cables para un circuito MBL de 60 qubits. Tanto el circuito sin cortar como el cortado se ejecutarán en hardware de IBM Quantum®:
num_qubits = 60
depth = 2
# construct the circuit
mbl = MBLChainCircuit(num_qubits, depth)
# create parameters
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
# construct the cut circuit
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_move = cut_wires(mbl_cut)
# Define observable and expand to account for the wire cut
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)
# Construct a SparsePauliOp version of the observable for later use in reconstruction
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
# Partition the circuit and get subcircuits and subobservables
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
# Obtain subexperiments and coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
# Transpile the subexperiments to the backend
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
# Execute the subexperiments and retrieve results
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
sampler.options.environment.job_tags = ["TUT_WC"]
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
results = {label: job.result() for label, job in jobs.items()}
# Reconstruct the expectation value of the original observable
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
# Compute the uncut circuit to obtain the noisy expectation value for comparison
sampler = SamplerV2(mode=backend)
sampler.options.environment.job_tags = ["TUT_WC"]
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)
# visualize the results
ax = plt.gca()
methods = ["uncut", "cut"]
values = [uncut_expval, reconstructed_expval]
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
plt.text(0.3, 0.95, "Exact result")
plt.show()
uncut_expval
0.9202473958333336
Próximos pasos
Si este trabajo te resultó interesante, puede que te interese el siguiente material:
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.
[7] Majumdar, R. (2024). Efficient Reduction of Resources and Noise in Discrete Quantum Computing Circuits (Doctoral dissertation, Indian Statistical Institute - Kolkata). https://www.proquest.com/openview/b481def90b1cc80e6b58a77c99e8385c/1?pq-origsite=gscholar&cbl=2026366&diss=y