Cięcie przewodów wyrażone jako dwuqubitowa instrukcja `Move`
W tym samouczku zrekonstruujemy wartości oczekiwane siedmioQubitowego Circuit, dzieląc go na dwa czteroQubitowe Circuit za pomocą cięcia przewodów.
Oto kroki, które wykonamy w tym wzorcu Qiskit:
- Krok 1: Odwzorowanie problemu na Circuit i operatory:
- Odwzorowanie hamiltonianu na Circuit kwantowy.
- Krok 2: Optymalizacja pod kątem docelowego sprzętu [Wykorzystuje dodatek do cięcia]:
- Cięcie Circuit 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 Circuit do cięcia
Na początku tworzymy Circuit 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 Circuit z instrukcjami Move umieszczonymi w wybranych miejscach cięcia
Mając powyższy Circuit, chcemy umieścić dwa cięcia przewodów na środkowej linii Qubit, tak aby Circuit mógł zostać podzielony na dwa Circuit po cztery Qubit każdy. Jednym ze sposobów jest ręczne umieszczenie dwuqubitowych instrukcji Move, które przenoszą stan z jednego przewodu Qubit na inny. Instrukcja Move jest koncepcyjnie równoważna operacji reset na drugim Qubit, po której następuje bramka SWAP. Efektem tej instrukcji jest przeniesienie stanu pierwszego (źródłowego) Qubit na drugi (docelowy) Qubit, przy jednoczesnym odrzuceniu przychodzącego stanu drugiego Qubit. Aby działało to zgodnie z zamierzeniem, ważne jest, aby drugi (docelowy) Qubit nie był splątany z resztą systemu; w przeciwnym razie operacja reset spowoduje częściowe zapaść stanu reszty systemu.
Tutaj budujemy nowy Circuit z jednym dodatkowym Qubit i operacjami Move na swoich miejscach. W tym przykładzie możemy ponownie użyć Qubit: źródłowy Qubit pierwszej operacji Move staje się docelowym Qubit drugiej operacji Move.
Uwaga: Jako alternatywę dla bezpośredniej pracy z instrukcjami Move, można zaznaczyć cięcia przewodów za pomocą jednoQubitowej instrukcji CutWire. Funkcja cut_wires służy do przekształcania CutWire na instrukcje Move na nowo przydzielonych Qubit. Jednak w odróżnieniu od metody ręcznej, ta automatyczna metoda nie pozwala na ponowne użycie przewodów Qubit. 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 Circuit
Ta obserwowalna odpowiada observable, ale musimy poprawnie uwzględnić dodatkowy przewód Qubit, 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 Circuit i obserwowalnych
Podobnie jak w poprzednich samouczkach, Qubit o tej samej etykiecie partycji będą grupowane razem, a nielokalne Gate 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 Circuit 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 Backend
generate_cutting_experiments przyjmuje argumenty circuits/observables jako słowniki mapujące etykiety partycji Qubit na odpowiednie subcircuit/subobservables.
Aby zasymulować wartość oczekiwaną pełnowymiarowego Circuit, z rozkładu kwaziprawa łączonego dekompozowanych Gate generowanych jest wiele poddoświadczeń, które następnie są wykonywane na jednym lub kilku Backend. 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 Backend
Tutaj używamy fałszywego 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 Backend
Przed wysłaniem Circuit do Qiskit Runtime musimy je transpilować z naszym Backend 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 Circuit 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