Saltar al contenido principal

Corte de cable expresado como una instrucción `Move` de dos Qubits

En este tutorial, reconstruiremos los valores esperados de un Circuit de siete Qubits dividiéndolo en dos Circuit de cuatro Qubits mediante corte de cable.

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

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

Paso 1: Mapear

Crear un Circuit para cortar

Primero, comenzamos con un Circuit inspirado en la Fig. 1(a) de arXiv:2302.03366v1.

# 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

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)
<qiskit.circuit.instructionset.InstructionSet at 0x7f16ab191a80>
qc_0.draw("mpl")

Quantum circuit diagram

Especificar un observable

from qiskit.quantum_info import SparsePauliOp

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

Paso 2: Optimizar

Crear un nuevo Circuit donde se han colocado instrucciones Move en las ubicaciones de corte deseadas

Dado el Circuit anterior, nos gustaría colocar dos cortes de cable en la línea del Qubit central, de modo que el Circuit pueda separarse en dos Circuit de cuatro Qubits cada uno. Una forma de hacerlo es colocar manualmente instrucciones Move de dos Qubits que mueven el estado de un cable de Qubit a otro. Una instrucción Move es conceptualmente equivalente a una operación de reinicio en el segundo Qubit, seguida de una Gate SWAP. El efecto de esta instrucción es transferir el estado del primer Qubit (fuente) al segundo Qubit (destino), descartando el estado entrante del segundo Qubit. Para que esto funcione según lo previsto, es importante que el segundo Qubit (destino) no comparta ningún entrelazamiento con el resto del sistema; de lo contrario, la operación de reinicio provocará que el estado del resto del sistema colapse parcialmente.

Aquí construimos un nuevo Circuit con un Qubit adicional y las operaciones Move en su lugar. En este ejemplo, podemos reutilizar un Qubit: el Qubit fuente del primer Move se convierte en el Qubit destino de la segunda operación Move.

Nota: Como alternativa a trabajar directamente con instrucciones Move, se puede optar por marcar los cortes de cable usando una instrucción CutWire de un solo Qubit. La función cut_wires existe para transformar CutWires en instrucciones Move sobre Qubits recién asignados. Sin embargo, a diferencia del método manual, este método automático no permite la reutilización de cables de Qubits. Consulta la guía práctica de CutWire para más detalles.

from qiskit_addon_cutting.instructions import Move

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)

qc_1.draw("mpl")

Quantum circuit diagram

Crear el observable que corresponde al nuevo Circuit

Este observable corresponde a observable, pero debemos tener en cuenta correctamente el cable de Qubit adicional que se ha añadido (es decir, insertamos una "I" en el índice 4). Ten en cuenta que en Qiskit, la representación en cadena del Qubit-0 corresponde al carácter de Pauli más a la derecha.

observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])

Separar el Circuit y los observables

Como en los tutoriales anteriores, los Qubits que comparten una etiqueta de partición común se agruparán, y las Gates no locales que abarcan más de una partición se cortarán.

from qiskit_addon_cutting import partition_problem

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

Visualizar el problema descompuesto

subobservables
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']),
'B': PauliList(['ZIII', 'IIII', 'IIII'])}
subcircuits["A"].draw("mpl")

Quantum circuit diagram

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

Quantum circuit diagram

Calcular la sobrecarga de muestreo para los cortes elegidos

Aquí cortamos dos cables, lo que resulta en una sobrecarga de muestreo de 444^4.

Para más información sobre la sobrecarga de muestreo incurrida por el corte de Circuit, consulta el material explicativo.

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

Generar los subexperimentos para ejecutar en el Backend

generate_cutting_experiments acepta argumentos circuits/observables como diccionarios que mapean etiquetas de partición de Qubits a los respectivos subcircuit/subobservables.

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 las Gates descompuestas y luego se ejecutan en uno o más Backends. El número de muestras tomadas de la distribución está controlado por 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.

from qiskit_addon_cutting import generate_cutting_experiments

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

Elegir un Backend

Aquí usamos un Backend falso, lo que hará que Qiskit Runtime se ejecute en modo local (es decir, en un simulador local).

from qiskit_ibm_runtime.fake_provider import FakeManilaV2

backend = FakeManilaV2()

Preparar los subexperimentos para el Backend

Debemos transpilar los Circuit con nuestro Backend como objetivo antes de enviarlos a Qiskit Runtime.

from qiskit.transpiler import generate_preset_pass_manager

# Transpile the subexperiments to ISA circuits
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()
}

Paso 3: Ejecutar

Ejecutar los subexperimentos usando la primitiva Sampler de Qiskit Runtime

from qiskit_ibm_runtime import SamplerV2, Batch

# 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()
}
/home/garrison/Qiskit/qiskit-ibm-runtime/qiskit_ibm_runtime/session.py:157: UserWarning: Session is not supported in local testing mode or when using a simulator.
warnings.warn(
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}

Paso 4: Posprocesar

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,
subobservables,
)
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([(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.51319069
Exact expectation value: 1.59099026
Error in estimation: -0.07779957
Relative error in estimation: -0.04890009