Sari la conținutul principal

Tăierea firelor formulată ca o instrucțiune `Move` pe două Qubituri

În acest tutorial, vom reconstrui valorile de așteptare ale unui Circuit cu șapte Qubituri împărțindu-l în două circuite de câte patru Qubituri, folosind tăierea firelor.

Aceștia sunt pașii pe care îi vom urma în acest tipar Qiskit:

  • Pasul 1: Maparea problemei pe circuite și operatori cuantici:
    • Maparea hamiltonianului pe un Circuit cuantic.
  • Pasul 2: Optimizarea pentru hardware-ul țintă [Folosește addon-ul de tăiere]:
    • Taie Circuit-ul și observabilul.
    • Transpilează subexperimentele pentru hardware.
  • Pasul 3: Execuția pe hardware-ul țintă:
    • Rulează subexperimentele obținute în Pasul 2 folosind primitivul Sampler.
  • Pasul 4: Post-procesarea rezultatelor [Folosește addon-ul de tăiere]:
    • Combină rezultatele din Pasul 3 pentru a reconstrui valoarea de așteptare a observabilului în cauză.

Pasul 1: Mapare

Crearea unui Circuit de tăiat

Mai întâi, pornim cu un Circuit inspirat din Fig. 1(a) a lucrării 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

Specificarea unui observabil

from qiskit.quantum_info import SparsePauliOp

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

Pasul 2: Optimizare

Crearea unui nou Circuit în care instrucțiunile Move au fost plasate la locațiile de tăiere dorite

Dat fiind Circuit-ul de mai sus, dorim să plasăm două tăieturi de fir pe linia Qubitului din mijloc, astfel încât Circuit-ul să poată fi separat în două circuite de câte patru Qubituri. O modalitate de a face acest lucru este să plasăm manual instrucțiuni Move pe două Qubituri care mută starea de pe un fir al Qubitului pe altul. O instrucțiune Move este conceptual echivalentă cu o operație de resetare pe al doilea Qubit, urmată de o poartă SWAP. Efectul acestei instrucțiuni este de a transfera starea primului Qubit (sursă) la cel de-al doilea Qubit (destinație), eliminând starea de intrare a celui de-al doilea Qubit. Pentru ca aceasta să funcționeze conform intenției, este important ca cel de-al doilea Qubit (destinație) să nu împartă nicio împletire cu restul sistemului; altfel, operația de resetare va determina colapsarea parțială a stării restului sistemului.

Aici construim un nou Circuit cu un Qubit suplimentar și operațiile Move la locul lor. În acest exemplu, putem reutiliza un Qubit: Qubit-ul sursă al primului Move devine Qubit-ul destinație al celei de-a doua operații Move.

Notă: Ca alternativă la lucrul direct cu instrucțiunile Move, se poate alege marcarea tăieturilor de fir folosind o instrucțiune CutWire pe un singur Qubit. Funcția cut_wires există pentru a transforma instrucțiunile CutWire în instrucțiuni Move pe Qubituri alocate nou. Cu toate acestea, spre deosebire de metoda manuală, această metodă automată nu permite reutilizarea firelor de Qubit. Consultă ghidul practic pentru CutWire pentru detalii.

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

Crearea observabilului pentru noul Circuit

Acest observabil corespunde cu observable, dar trebuie să ținem cont corect de firul Qubitului suplimentar care a fost adăugat (adică inserăm un „I" la indexul 4). Rețineți că în Qiskit, reprezentarea șir qubit-0 corespunde caracterului Pauli cel mai din dreapta.

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

Separarea Circuit-ului și a observabilelor

Ca în tutorialele anterioare, Qubiții care împărtășesc o etichetă de partiție comună vor fi grupați împreună, iar porțile neloc ale care acoperă mai mult de o partiție vor fi tăiate.

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

Vizualizarea problemei descompuse

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

Calcularea costului de eșantionare pentru tăieturile alese

Aici tăiem două fire, rezultând un cost de eșantionare de 444^4.

Pentru mai multe informații despre costul de eșantionare generat de tăierea circuitelor, consultă materialul explicativ.

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

Generarea subexperimentelor de rulat pe Backend

generate_cutting_experiments acceptă argumentele circuits/observables ca dicționare care mapează etichetele de partiție ale Qubiților la subcircuit/subobservables respective.

Pentru a simula valoarea de așteptare a Circuit-ului de dimensiune completă, se generează multe subexperimente din distribuția de quasiprobabilitate comună a porților descompuse și sunt executate pe unul sau mai multe Backend-uri. Numărul de eșantioane preluate din distribuție este controlat de num_samples, iar un coeficient combinat este dat pentru fiecare eșantion unic. Pentru mai multe informații despre cum sunt calculați coeficienții, consultă materialul explicativ.

from qiskit_addon_cutting import generate_cutting_experiments

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

Alegerea unui Backend

Aici folosim un Backend fals, ceea ce va determina Qiskit Runtime să ruleze în modul local (adică pe un simulator local).

from qiskit_ibm_runtime.fake_provider import FakeManilaV2

backend = FakeManilaV2()

Pregătirea subexperimentelor pentru Backend

Trebuie să transpilăm circuitele cu Backend-ul nostru ca țintă înainte de a le trimite la 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()
}

Pasul 3: Execuție

Rularea subexperimentelor folosind primitivul Qiskit Runtime Sampler

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()}

Pasul 4: Post-procesare

Reconstruirea valorii de așteptare

Reconstruiește valorile de așteptare pentru fiecare termen observabil și combină-le pentru a reconstrui valoarea de așteptare a observabilului 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)

Compararea valorii de așteptare reconstruite cu valoarea de așteptare exactă din Circuit-ul și observabilul original

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