Vai al contenuto principale

Wire Cutting Espressa come Istruzione `Move` a Due Qubit

In questo tutorial, ricostruiremo i valori attesi di un Circuit a sette Qubit suddividendolo in due Circuit a quattro Qubit ciascuno tramite wire cutting.

Questi sono i passi che seguiremo in questo pattern Qiskit:

  • Passo 1: Mappa il problema su Circuit e operatori quantistici:
    • Mappa l'hamiltoniano su un Circuit quantistico.
  • Passo 2: Ottimizza per l'hardware di destinazione [Usa il cutting addon]:
    • Taglia il Circuit e l'osservabile.
    • Transpila i sottoesperimenti per l'hardware.
  • Passo 3: Esegui sull'hardware di destinazione:
    • Esegui i sottoesperimenti ottenuti nel Passo 2 usando la primitiva Sampler.
  • Passo 4: Post-elabora i risultati [Usa il cutting addon]:
    • Combina i risultati del Passo 3 per ricostruire il valore atteso dell'osservabile in questione.

Passo 1: Mappa​

Crea un Circuit da tagliare​

Per prima cosa, partiamo da un Circuit ispirato alla Fig. 1(a) di 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

Specifica un osservabile​

from qiskit.quantum_info import SparsePauliOp

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

Passo 2: Ottimizza​

Crea un nuovo Circuit in cui le istruzioni Move sono state posizionate nelle posizioni di taglio desiderate​

Dato il Circuit sopra, vogliamo posizionare due wire cut sulla linea del Qubit centrale, in modo che il Circuit possa separarsi in due Circuit da quattro Qubit ciascuno. Un modo per farlo è inserire manualmente istruzioni Move a due Qubit che spostano lo stato da un filo di Qubit a un altro. Un'istruzione Move è concettualmente equivalente a un'operazione di reset sul secondo Qubit, seguita da un Gate SWAP. L'effetto di questa istruzione è di trasferire lo stato del primo Qubit (sorgente) al secondo (destinazione), scartando lo stato in ingresso del secondo Qubit. Affinché questo funzioni come previsto, è importante che il secondo Qubit (destinazione) non condivida entanglement con il resto del sistema; altrimenti, l'operazione di reset causerà il collasso parziale dello stato del resto del sistema.

Qui costruiamo un nuovo Circuit con un Qubit aggiuntivo e le operazioni Move al loro posto. In questo esempio, siamo in grado di riutilizzare un Qubit: il Qubit sorgente del primo Move diventa il Qubit destinazione del secondo Move.

Nota: Come alternativa al lavoro diretto con le istruzioni Move, si può scegliere di contrassegnare i wire cut usando un'istruzione CutWire a Qubit singolo. La funzione cut_wires esiste per trasformare le CutWire in istruzioni Move su Qubit appena allocati. Tuttavia, a differenza del metodo manuale, questo metodo automatico non consente il riutilizzo dei fili di Qubit. Consulta la guida pratica di CutWire per i dettagli.

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

Crea l'osservabile da abbinare al nuovo Circuit​

Questo osservabile corrisponde a observable, ma dobbiamo tenere conto correttamente del filo di Qubit aggiuntivo che è stato aggiunto (ovvero, inseriamo una "I" all'indice 4). Nota che in Qiskit, il Qubit-0 nella rappresentazione stringa corrisponde al carattere di Pauli più a destra.

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

Separa il Circuit e gli osservabili​

Come nei tutorial precedenti, i Qubit che condividono una stessa etichetta di partizione saranno raggruppati insieme, e i Gate non locali che si estendono su più partizioni verranno tagliati.

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

Visualizza il problema decomposto​

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

Calcola l'overhead di campionamento per i tagli scelti​

Qui tagliamo due fili, ottenendo un overhead di campionamento di 444^4.

Per ulteriori informazioni sull'overhead di campionamento dovuto al circuit cutting, consulta il materiale esplicativo.

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

Genera i sottoesperimenti da eseguire sul Backend​

generate_cutting_experiments accetta gli argomenti circuits/observables come dizionari che mappano le etichette di partizione dei Qubit ai rispettivi subcircuit/subobservables.

Per simulare il valore atteso del Circuit di dimensione intera, vengono generati molti sottoesperimenti dalla distribuzione di quasiprobabilità congiunta dei Gate decomposti e poi eseguiti su uno o più Backend. Il numero di campioni prelevati dalla distribuzione è controllato da num_samples, e viene fornito un coefficiente combinato per ogni campione unico. Per ulteriori informazioni su come vengono calcolati i coefficienti, consulta il materiale esplicativo.

from qiskit_addon_cutting import generate_cutting_experiments

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

Scegli un Backend​

Qui stiamo usando un Backend finto, che farà sì che Qiskit Runtime venga eseguito in modalità locale (ovvero, su un simulatore locale).

from qiskit_ibm_runtime.fake_provider import FakeManilaV2

backend = FakeManilaV2()

Prepara i sottoesperimenti per il Backend​

Dobbiamo transpilare i Circuit con il nostro Backend come target prima di inviarli 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()
}

Passo 3: Esegui​

Esegui i sottoesperimenti usando la primitiva Sampler di 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()}

Passo 4: Post-elabora​

Ricostruisci il valore atteso​

Ricostruisci i valori attesi per ogni termine dell'osservabile e combinali per ricostruire il valore atteso dell'osservabile originale.

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)

Confronta il valore atteso ricostruito con il valore atteso esatto dal Circuit e dall'osservabile originali​

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