Przejdź do głównej treści

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

Quantum circuit diagram

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

Quantum circuit diagram

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

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