Przejdź do głównej treści

Cięcie przewodów wyrażone jako dwukubitowa instrukcja `Move`

W tym samouczku zrekonstruujemy wartości oczekiwane siedmiokubitowego obwodu, dzieląc go na dwa czterokubitowe obwody za pomocą cięcia przewodów.

Oto kroki, które wykonamy w tym wzorcu Qiskit:

  • Krok 1: Odwzorowanie problemu na obwód i operatory:
    • Odwzorowanie hamiltonianu na obwód kwantowy.
  • Krok 2: Optymalizacja pod kątem docelowego sprzętu [Wykorzystuje dodatek do cięcia]:
    • Cięcie obwodu i obserwowalnej.
    • Transpilacja poddoświadczeń pod kątem sprzętu.
  • Krok 3: Wykonanie na docelowym sprzęcie:
    • Uruchomienie poddoświadczeń uzyskanych w Kroku 2 przy użyciu prymitywu Sampler.
  • Krok 4: Przetwarzanie końcowe wyników [Wykorzystuje dodatek do cięcia]:
    • Łączenie wyników z Kroku 3 w celu rekonstrukcji wartości oczekiwanej badanej obserwowalnej.

Krok 1: Odwzorowanie

Tworzenie obwodu do cięcia

Na początku tworzymy obwód inspirowany rys. 1(a) z 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

Określenie obserwowalnej

from qiskit.quantum_info import SparsePauliOp

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

Krok 2: Optymalizacja

Tworzenie nowego obwodu z instrukcjami Move umieszczonymi w wybranych miejscach cięcia

Mając powyższy obwód, chcemy umieścić dwa cięcia przewodów na środkowej linii kubitu, tak aby obwód mógł zostać podzielony na dwa obwody po cztery kubity każdy. Jednym ze sposobów jest ręczne umieszczenie dwukubitowych instrukcji Move, które przenoszą stan z jednego przewodu kubitu na inny. Instrukcja Move jest koncepcyjnie równoważna operacji reset na drugim kubicie, po której następuje bramka SWAP. Efektem tej instrukcji jest przeniesienie stanu pierwszego (źródłowego) kubitu na drugi (docelowy) kubit, przy jednoczesnym odrzuceniu przychodzącego stanu drugiego kubitu. Aby działało to zgodnie z zamierzeniem, ważne jest, aby drugi (docelowy) kubit nie był splątany z resztą systemu; w przeciwnym razie operacja reset spowoduje częściowe zapadnięcie stanu reszty systemu.

Tutaj budujemy nowy obwód z jednym dodatkowym kubitem i rozmieszczonymi operacjami Move. W tym przykładzie możemy ponownie użyć kubitu: źródłowy kubit pierwszej operacji Move staje się docelowym kubitem drugiej operacji Move.

Uwaga: Jako alternatywę dla bezpośredniej pracy z instrukcjami Move, można zaznaczyć cięcia przewodów za pomocą jednokubitowej instrukcji CutWire. Funkcja cut_wires służy do przekształcania CutWire na instrukcje Move na nowo przydzielonych kubitach. Jednak w odróżnieniu od metody ręcznej, ta automatyczna metoda nie pozwala na ponowne użycie przewodów kubitów. Szczegóły znajdziesz w poradniku dotyczącym CutWire.

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

Tworzenie obserwowalnej do nowego obwodu

Ta obserwowalna odpowiada observable, ale musimy poprawnie uwzględnić dodatkowy przewód kubitu, który został dodany (czyli wstawiamy "I" pod indeksem 4). Zwróć uwagę, że w Qiskit, reprezentacja ciągu znaków qubit-0 odpowiada prawemu skrajnemu znakowi Pauliego.

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

Rozdzielenie obwodu i obserwowalnych

Podobnie jak w poprzednich samouczkach, kubity o tej samej etykiecie partycji będą grupowane razem, a nielokalne bramki obejmujące więcej niż jedną partycję zostaną przecięte.

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

Wizualizacja rozłożonego problemu

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

Obliczenie narzutu próbkowania dla wybranych cięć

Tutaj przetniemy dwa przewody, co skutkuje narzutem próbkowania równym 444^4.

Więcej informacji na temat narzutu próbkowania wynikającego z cięcia obwodu znajdziesz w materiałach wyjaśniających.

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

Generowanie poddoświadczeń do uruchomienia na backendzie

generate_cutting_experiments przyjmuje argumenty circuits/observables jako słowniki mapujące etykiety partycji kubitów na odpowiednie subcircuit/subobservables.

Aby zasymulować wartość oczekiwaną pełnowymiarowego obwodu, z łącznego rozkładu kwaziprawdopodobieństwa zdekomponowanych bramek generowanych jest wiele poddoświadczeń, które następnie są wykonywane na jednym lub kilku backendach. Liczba próbek pobieranych z rozkładu jest kontrolowana przez num_samples, a dla każdej unikalnej próbki podawany jest jeden łączny współczynnik. Więcej informacji na temat sposobu obliczania współczynników znajdziesz w materiałach wyjaśniających.

from qiskit_addon_cutting import generate_cutting_experiments

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

Wybór backendu

Tutaj używamy atrapy backendu (fake backend), co spowoduje uruchomienie Qiskit Runtime w trybie lokalnym (tzn. na lokalnym symulatorze).

from qiskit_ibm_runtime.fake_provider import FakeManilaV2

backend = FakeManilaV2()

Przygotowanie poddoświadczeń dla backendu

Przed wysłaniem obwodów do Qiskit Runtime musimy je transpilować z naszym backendem jako celem.

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

Krok 3: Wykonanie

Uruchamianie poddoświadczeń przy użyciu prymitywu Sampler 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()}

Krok 4: Przetwarzanie końcowe

Rekonstrukcja wartości oczekiwanej

Rekonstruujemy wartości oczekiwane dla każdego składnika obserwowalnej i łączymy je, aby odtworzyć wartość oczekiwaną dla oryginalnej obserwowalnej.

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)

Porównanie zrekonstruowanej wartości oczekiwanej z dokładną wartością oczekiwaną uzyskaną z oryginalnego obwodu i obserwowalnej

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