Saltar al contenido principal

Comenzar con el corte de circuitos usando cortes de alambre

Versiones de paquetes

El código de esta página fue desarrollado usando los siguientes requisitos. Recomendamos usar estas versiones o más recientes.

qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-aer~=0.17
qiskit-addon-cutting~=0.10.0

Esta guía demuestra un ejemplo funcional de cortes de alambre con el paquete qiskit-addon-cutting. Cubre la reconstrucción de valores de expectación de un circuito de siete qubits usando cortes de alambre.

Un corte de alambre se representa en este paquete como una instrucción de dos qubits Move, que se define como un reinicio del segundo qubit sobre el que actúa la instrucción, seguido de un intercambio de ambos qubits. Esta operación es equivalente a transferir el estado del primer qubit al segundo qubit, mientras se descarta simultáneamente el estado entrante del segundo qubit.

El paquete está diseñado para ser consistente con la forma en que debes tratar los cortes de alambre al actuar sobre qubits físicos. Por ejemplo, un corte de alambre puede tomar el estado del qubit físico nn y continuarlo como qubit físico mm después del corte. Puedes pensar en el "corte de instrucciones" como un marco unificado para considerar tanto los cortes de alambre como los de compuerta dentro del mismo formalismo (ya que un corte de alambre es simplemente una instrucción Move cortada). El uso de este marco para el corte de alambre también permite la reutilización de qubits, lo cual se explica en la sección sobre corte de alambres manualmente.

La instrucción de un solo qubit CutWire actúa como una interfaz más abstracta y sencilla para trabajar con cortes de alambre. Te permite indicar en qué punto del circuito debe cortarse un alambre a alto nivel y hacer que el complemento de corte de circuitos inserte las instrucciones Move apropiadas por ti.

El siguiente ejemplo demuestra la reconstrucción del valor de expectación después de un corte de alambre. Crearás un circuito con varias compuertas no locales y definirás los observables a estimar.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
import numpy as np
from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
from qiskit_ibm_runtime import SamplerV2, Batch
from qiskit_aer.primitives import EstimatorV2

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

qc_0 = QuantumCircuit(7)
for i in range(7):
qc_0.rx(np.pi / 4, i)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
qc_0.cx(3, 4)
qc_0.cx(3, 5)
qc_0.cx(3, 6)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)

# Define observable
observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])

# Draw circuit
qc_0.draw("mpl")

Output of the previous code cell

Cortar alambres usando la instrucción de alto nivel CutWire

A continuación, realiza cortes de alambre usando la instrucción de un solo qubit CutWire en el qubit q3q_3. Una vez que los subexperimentos estén preparados para ejecutarse, usa la función cut_wires() para transformar las instrucciones CutWire en instrucciones Move sobre qubits recién asignados.

qc_1 = QuantumCircuit(7)
for i in range(7):
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(CutWire(), [3])
qc_1.cx(3, 4)
qc_1.cx(3, 5)
qc_1.cx(3, 6)
qc_1.append(CutWire(), [3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)

qc_1.draw("mpl")

Output of the previous code cell

Nota sobre la expansión de observables

Cuando un circuito se expande a través de uno o más cortes de alambre, el observable debe actualizarse para dar cuenta de los qubits adicionales que se introducen. El paquete qiskit-addon-cutting tiene una función de conveniencia expand_observables(), que toma objetos PauliList y los circuitos original y expandido como argumentos, y devuelve un nuevo PauliList.

Este PauliList devuelto no contendrá información sobre los coeficientes del observable original, pero estos pueden ignorarse hasta la reconstrucción del valor de expectación final.

# Transform CutWire instructions to Move instructions
qc_2 = cut_wires(qc_1)

# Expand the observable to match the new circuit size
expanded_observable = expand_observables(observable.paulis, qc_0, qc_2)
print(f"Expanded Observable: {expanded_observable}")
qc_2.draw("mpl")
Expanded Observable: ['ZIIIIIIII', 'IIIZIIIII', 'IIIIIIIIZ']

Output of the previous code cell

Particionar el circuito y el observable

Ahora el problema puede separarse en particiones. Esto se logra usando la función partition_problem() con un conjunto opcional de etiquetas de partición para especificar cómo separar el circuito. Los qubits que comparten una etiqueta de partición común se agrupan, y cualquier compuerta no local que abarque más de una partición se corta.

Si no se proporcionan etiquetas de partición, la partición se determinará automáticamente según la conectividad del circuito. Lee la siguiente sección sobre cortar alambres manualmente para obtener más información sobre cómo incluir etiquetas de partición.

partitioned_problem = partition_problem(
circuit=qc_2,
observables=expanded_observable,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases

print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits[0].draw("mpl")
Subobservables to measure:
{0: PauliList(['IIIII', 'ZIIII', 'IIIIZ']), 1: PauliList(['ZIII', 'IIII', 'IIII'])}

Sampling overhead: 256.0

Output of the previous code cell

subcircuits[1].draw("mpl")

Output of the previous code cell

En este esquema de partición, has cortado dos alambres, lo que resulta en una sobrecarga de muestreo de 444^4.

Generar subexperimentos para ejecutar y post-procesar resultados

Para estimar el valor de expectación del circuito completo, se generan varios subexperimentos a partir de la distribución de cuasi-probabilidad conjunta de las compuertas descompuestas y luego se ejecutan en uno o más QPUs. El método generate_cutting_experiments hace esto ingiriendo argumentos para los diccionarios subcircuits y subobservables que creaste anteriormente, así como para el número de muestras a tomar de la distribución.

Nota sobre el número de muestras

El argumento num_samples especifica cuántas muestras extraer de la distribución de cuasi-probabilidad y determina la precisión de los coeficientes utilizados para la reconstrucción. Pasar infinito (np.inf) garantiza que todos los coeficientes se calculen exactamente. Lee la documentación de la API sobre generación de pesos y generación de experimentos de corte para más información.

# Generate subexperiments
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)

# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend
)
isa_subexperiments = {
label: pass_manager.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}

# Submit each partition's subexperiments to the Qiskit Runtime Sampler
# primitive, in a single batch so that the jobs will run back-to-back.
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()
}
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}

Por último, el valor de expectación del circuito completo puede reconstruirse usando el método reconstruct_expectation_values().

El bloque de código a continuación reconstruye los resultados y los compara con el valor de expectación exacto.

reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
# Apply the coefficients of the original observable
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)

# Compute the exact expectation value using the `qiskit_aer` package.
estimator = EstimatorV2()
exact_expval = estimator.run([(qc_0, 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: 1.45965266
Exact expectation value: 1.59099026
Error in estimation: -0.1313376
Relative error in estimation: -0.08255085
Nota sobre los coeficientes del observable

Para reconstruir con precisión el valor de expectación, los coeficientes del observable original (que son distintos de la salida de generate_cutting_experiments()) deben aplicarse a la salida de la reconstrucción, ya que esta información se perdió cuando se generaron los experimentos de corte o cuando el observable fue expandido.

Normalmente, estos coeficientes pueden aplicarse mediante numpy.dot() como se mostró anteriormente.

Cortar alambres usando la instrucción de bajo nivel Move

Una limitación del uso de la instrucción de alto nivel CutWire es que no permite la reutilización de qubits. Si esto es deseable para un experimento de corte, puedes en cambio colocar instrucciones Move manualmente. Sin embargo, dado que la instrucción Move descarta el estado del qubit de destino, es importante que este qubit no comparta ningún entrelazamiento con el resto del sistema. De lo contrario, la operación de reinicio provocará que el estado del circuito colapse parcialmente después del corte de alambre.

El bloque de código a continuación realiza un corte de alambre en el qubit q3q_3 para el mismo circuito de ejemplo mostrado anteriormente. La diferencia aquí es que puedes reutilizar un qubit invirtiendo la operación Move donde se realizó el segundo corte de alambre (sin embargo, esto no siempre es posible y depende del circuito que se esté cortando).

qc_1 = QuantumCircuit(8)
for i in [*range(4), *range(5, 8)]:
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(Move(), [3, 4])
qc_1.cx(4, 5)
qc_1.cx(4, 6)
qc_1.cx(4, 7)
qc_1.append(Move(), [4, 3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)

# Expand observable
observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])
qc_1.draw("mpl")

Output of the previous code cell

El circuito anterior ahora puede particionarse y generarse los experimentos de corte. Para especificar explícitamente cómo debe particionarse el circuito, puedes agregar etiquetas de partición a la función partition_problem(). Los qubits que comparten una etiqueta de partición común se agrupan, y cualquier compuerta no local que abarque más de una partición se corta. Las claves del diccionario que devuelve partition_problem() coincidirán con las especificadas en la cadena de etiquetas.

partitioned_problem = partition_problem(
circuit=qc_1,
partition_labels="AAAABBBB",
observables=observable_expanded.paulis,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases

print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits["A"].draw("mpl")
Subobservables to measure:
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']), 'B': PauliList(['ZIII', 'IIII', 'IIII'])}

Sampling overhead: 256.0

Output of the previous code cell

subcircuits["B"].draw("mpl")

Output of the previous code cell

Ahora los experimentos de corte pueden generarse y el valor de expectación reconstruirse de la misma manera que en la sección anterior.

Próximos pasos

Recomendaciones