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.
- Uruchomienie poddoświadczeń uzyskanych w Kroku 2 przy użyciu prymitywu
- 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")

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")

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")

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

Obliczenie narzutu próbkowania dla wybranych cięć
Tutaj przetniemy dwa przewody, co skutkuje narzutem próbkowania równym .
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