Przejdź do głównej treści

Cięcie przewodów do estymacji wartości oczekiwanych

Szacowane wykorzystanie zasobów: jedna minuta na procesorze Eagle (UWAGA: to tylko szacunek. Twój czas wykonania może się różnić.)

Tło

Circuit-knitting to termin zbiorczy, który obejmuje różne metody podziału obwodu na wiele mniejszych podobwodów zawierających mniej bramek i/lub qubitów. Każdy z podobwodów może być wykonywany niezależnie, a końcowy wynik uzyskuje się przez klasyczne post-przetwarzanie wyników każdego podobwodu. Technika ta jest dostępna w dodatku Qiskit do cięcia obwodów; szczegółowe wyjaśnienie techniki znajduje się w dokumentacji wraz z innymi materiałami wprowadzającymi.

Ten notebook dotyczy metody zwanej cięciem przewodów, w której obwód jest dzielony wzdłuż przewodu [1], [2]. Warto zauważyć, że podział jest prosty w przypadku obwodów klasycznych, ponieważ wynik w miejscu podziału można wyznaczyć deterministycznie — jest to 0 lub 1. Jednak stan qubitu w miejscu cięcia jest, ogólnie rzecz biorąc, stanem mieszanym. Dlatego każdy podobwód musi być mierzony wielokrotnie w różnych bazach (zazwyczaj tomograficznie pełnych zbiorach baz, takich jak baza Pauliego [3], [4]) i odpowiednio przygotowany w swoim stanie własnym. Poniższy rysunek (źródło: praca doktorska, Ritajit Majumdar) pokazuje przykład cięcia przewodów dla 4-qubitowego stanu GHZ na trzy podobwody. Tutaj MjM_j oznacza zestaw baz (zazwyczaj Pauli X, Y i Z), a PiP_i oznacza zestaw stanów własnych (zazwyczaj 0|0\rangle, 1|1\rangle, +|+\rangle i +i|+i\rangle).

wc-1.png wc-2.png

Ponieważ każdy podobwód ma mniej qubitów i/lub bramek, oczekuje się, że będzie mniej podatny na szumy. Ten notebook pokazuje przykład, gdzie ta metoda może być użyta do efektywnego tłumienia szumów w systemie.

Wymagania

Przed rozpoczęciem tego samouczka upewnij się, że masz zainstalowane następujące elementy:

  • Qiskit SDK v2.0 lub nowszy, z obsługą wizualizacji
  • Qiskit Runtime v0.22 lub nowszy ( pip install qiskit-ibm-runtime )
  • Dodatek Qiskit do cięcia obwodów v0.9.0 lub nowszy (pip install qiskit-addon-cutting)

W tym notebooku rozważymy obwód Many Body Localization (MBL). Obwód MBL jest obwodem efektywnym sprzętowo i jest sparametryzowany dwoma parametrami θ\theta i ϕ\vec{\phi}. Gdy θ\theta jest ustawione na 00 i stan początkowy jest przygotowany w 0|0\rangle dla wszystkich qubitów, idealna wartość oczekiwana Zi\langle Z_i \rangle wynosi +1+1 dla każdego miejsca qubitu ii niezależnie od wartości ϕ\vec{\phi}. Więcej szczegółów na temat obwodów MBL znajdziesz w tym artykule.

Konfiguracja

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-cutting qiskit-ibm-runtime
import numpy as np
import matplotlib.pyplot as plt

from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.quantum_info import PauliList, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.result import sampled_expectation_value

from qiskit_addon_cutting.instructions import CutWire
from qiskit_addon_cutting import (
cut_wires,
expand_observables,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2, Batch

class MBLChainCircuit(QuantumCircuit):
def __init__(
self, num_qubits: int, depth: int, use_cut: bool = False
) -> None:
super().__init__(
num_qubits, name=f"MBLChainCircuit<{num_qubits}, {depth}>"
)
evolution = MBLChainEvolution(num_qubits, depth, use_cut)
self.compose(evolution, inplace=True)

class MBLChainEvolution(QuantumCircuit):
def __init__(self, num_qubits: int, depth: int, use_cut) -> None:
super().__init__(
num_qubits, name=f"MBLChainEvolution<{num_qubits}, {depth}>"
)

theta = Parameter("θ")
phis = ParameterVector("φ", num_qubits)

for layer in range(depth):
layer_parity = layer % 2
# print("layer parity", layer_parity)
for qubit in range(layer_parity, num_qubits - 1, 2):
# print(qubit)
self.cz(qubit, qubit + 1)
self.u(theta, 0, np.pi, qubit)
self.u(theta, 0, np.pi, qubit + 1)
if (
use_cut
and layer_parity == 0
and (
qubit == num_qubits // 2 - 1
or qubit == num_qubits // 2
)
):
self.append(CutWire(), [num_qubits // 2])
if use_cut and layer < depth - 1 and layer_parity == 1:
if qubit == num_qubits // 2:
self.append(CutWire(), [qubit])
for qubit in range(num_qubits):
self.p(phis[qubit], qubit)

Część I. Przykład w małej skali

Krok 1: Odwzorowanie klasycznych danych wejściowych na problem kwantowy

Najpierw budujemy szablon obwodu bez żadnych konkretnych wartości parametrów. Dodajemy również miejsca zastępcze, zwane CutWire, aby oznaczyć pozycje cięć. W przykładzie małej skali rozważamy 10-qubitowy obwód MBL.

num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)

Output of the previous code cell

Przypomnijmy, że chcemy znaleźć wartość oczekiwaną obserwabli 1ni=1nZi\frac{1}{n}\sum_{i=1} ^n Z_i gdy θ=0\theta=0. Przypiszemy losowe wartości parametrowi ϕ\vec{\phi}.

phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
params
[0,
0.2376615174332788,
0.28244289857682414,
0.019248960591717768,
0.46140600996102477,
0.31408025180068433,
0.718184005135733,
0.991153920182475,
0.09289485768301442,
0.8857848280067783,
0.6177529765767047]

Teraz oznaczamy obwód do cięcia, wstawiając odpowiednie CutWire, aby utworzyć dwa w przybliżeniu równe cięcia. Ustawiamy use_cut=True w funkcji i pozwalamy jej na oznaczenie po n2\frac{n}{2} qubitach, gdzie nn to liczba qubitów w oryginalnym obwodzie.

mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)

Output of the previous code cell

Krok 2: Optymalizacja problemu pod kątem wykonania na sprzęcie kwantowym

Następnie dzielimy obwód na dwa mniejsze podobwody. W tym przykładzie ograniczamy się do tylko 2 podobwodów. W tym celu używamy Dodatku Qiskit: Cięcie Obwodów.

Podziel obwód na mniejsze podobwody

Przecięcie przewodu w danym miejscu zwiększa liczbę qubitów o jeden. Oprócz oryginalnego qubitu pojawia się teraz dodatkowy qubit jako miejsce zastępcze w obwodzie po cięciu. Poniższy obraz pokazuje reprezentację:

wc-4.png

Ten Dodatek używa funkcji cut_wires, aby uwzględnić dodatkowe qubity powstałe w wyniku cięcia.

mbl_move = cut_wires(mbl_cut)

Utwórz i rozszerz obserwable

Teraz konstruujemy obserwablę Mz=1ni=1nZiM_z = \frac{1}{n}\sum_{i=1}^n \langle Z_i \rangle. Ponieważ idealna wartość Zi\langle Z_i \rangle dla każdego ii wynosi +1+1, idealna wartość MzM_z również wynosi +1+1.

observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
observable
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])

Jednak zauważ, że liczba qubitów w obwodzie wzrosła po wstawieniu wirtualnych 2-qubitowych operacji Move po cięciu. Dlatego musimy również rozszerzyć obserwable, wstawiając tożsamości, aby dostosować je do bieżącego obwodu.

new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])

Zwróć uwagę, że każda obserwabla rozszerzyła się teraz, aby pomieścić siedem qubitów — jak w obwodzie z operacją Move — zamiast oryginalnych 6 qubitów. Następnie dzielimy obwód na dwa podobwody.

partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)

Zwizualizujmy podobwody

subcircuits = partitioned_problem.subcircuits
subcircuits[0].draw("mpl", fold=-1)

Output of the previous code cell

subcircuits[1].draw("mpl", fold=-1)

Output of the previous code cell

Obserwable zostały również podzielone, aby pasowały do podobwodów

subobservables = partitioned_problem.subobservables
subobservables
{0: PauliList(['IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IZIIII',
'IIZIII', 'IIIZII', 'IIIIZI', 'IIIIIZ']),
1: PauliList(['ZIIII', 'IZIII', 'IIZII', 'IIIZI', 'IIIIZ', 'IIIII', 'IIIII',
'IIIII', 'IIIII', 'IIIII'])}

Zauważ, że każdy podobwód prowadzi do pewnej liczby próbek. Rekonstrukcja uwzględnia wynik każdej z tych próbek. Każda z tych próbek jest określana jako subexperiment. Rozszerzenie obserwabli za pomocą operacji Move wymaga struktury danych PauliList. Możemy też utworzyć obserwablę MzM_z w bardziej ogólnej strukturze danych SparsePauliOp, która będzie przydatna później podczas rekonstrukcji podeksperymentów.

M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
M_z
SparsePauliOp(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII', 'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII', 'IIIIIIIIZI', 'IIIIIIIIIZ'],
coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
0.1+0.j, 0.1+0.j])
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)

Zobaczmy dwa przykłady, w których qubity cięcia są mierzone w dwóch różnych bazach. Najpierw jest mierzony w normalnej bazie Z, a następnie w bazie X.

subexperiments[0][6].draw("mpl", fold=-1)

Output of the previous code cell

subexperiments[0][2].draw("mpl", fold=-1)

Output of the previous code cell

Transpiluj każdy podeksperyment

Obecnie musimy transpilować nasze obwody przed ich przesłaniem do wykonania. Dlatego najpierw transpilujemy każdy obwód w podeksperymentach.

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)

Teraz musimy transpilować każdy z obwodów w podeksperymentach. W tym celu najpierw tworzymy menedżera przejść, a następnie używamy go do transpilacji każdego z obwodów.

pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
isa_subexperiments[0][0].draw("mpl", fold=-1, idle_wires=False)

Output of the previous code cell

Krok 3: Wykonanie przy użyciu prymitywów Qiskit

Teraz wykonamy każdy obwód w podeksperymencie. Qiskit-addon-cutting używa SamplerV2 do wykonywania podeksperymentów.

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

Krok 4: Post-przetwarzanie i zwrot wyniku w żądanym formacie klasycznym

Po wykonaniu obwodów musimy teraz pobrać wyniki i zrekonstruować wartość oczekiwaną dla nieciętego obwodu i oryginalnej obserwabli.

# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9674376845359803

Weryfikacja krzyżowa

Wykonajmy teraz obwód bez cięcia i sprawdźmy tam wynik. Zauważ, że do wykonania nieciętego obwodu możemy bezpośrednio użyć EstimatorV2 do obliczenia wartości oczekiwanych. Jednak będziemy używać tego samego Primitive przez cały czas. Dlatego użyjemy SamplerV2, aby uzyskać rozkład prawdopodobieństwa i obliczyć wartość oczekiwaną za pomocą funkcji sampled_expectation_value.

Najpierw musimy transpilować niecięty obwód mbl.

sampler = SamplerV2(mode=backend)

if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)

Następnie konstruujemy pub i uruchamiamy niecięty obwód.

pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9498046875000001

Zauważamy, że wartość oczekiwana uzyskana za pomocą cięcia przewodów jest bliższa idealnej wartości +1+1 niż ta z nieciętego obwodu. Teraz skalujmy rozmiar problemu w górę.

Część II. Skalowanie w górę!

Wcześniej pokazaliśmy wyniki dla 10-qubitowego układu MBL. Teraz pokażemy, że poprawa wartości oczekiwanej jest osiągana również dla większych układów. W tym celu powtórzymy cały proces dla 60-qubitowego układu MBL.

Krok 1: Odwzorowanie klasycznych danych wejściowych na problem kwantowy

num_qubits = 60
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)

Tworzymy losowy zestaw wartości dla ϕ\vec{\phi}

phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis

Następnie konstruujemy Circuit z cięciami

mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)

Krok 2: Optymalizacja problemu pod kątem wykonania na sprzęcie kwantowym

Podobnie jak w przykładzie na małą skalę, dzielimy Circuit i obserwablę na potrzeby eksperymentów z cięciem.

mbl_move = cut_wires(mbl_cut)

# Define observable
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)

# Partition the circuit into subcircuits
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)

# Get subcircuits
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables

Tworzymy również obiekt SparsePauliOp dla obserwabli z odpowiednimi współczynnikami.

M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)

Następnie generujemy subeksperymenty i transpilujemy każdy Circuit w subexperymencie.

subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}

Krok 3: Wykonanie przy użyciu prymitywów Qiskit

Używamy trybu Batch, aby wykonać wszystkie Circuits w subexperymentach.

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

Krok 4: Post-processing i zwracanie wyniku w pożądanym formacie klasycznym

Pobierzemy teraz wyniki dla każdego Circuitu w subexperymencie i zrekonstruujemy wartość oczekiwaną odpowiadającą nieciętemu Circuitowi i oryginalnej obserwabli.

# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9631355921427409

Weryfikacja krzyżowa

Podobnie jak w przykładzie na małą skalę, ponownie wyznaczymy wartość oczekiwaną, wykonując niecięty Circuit, i porównamy wynik z cięciem Circuit. Użyjemy SamplerV2, aby zachować jednolitość w korzystaniu z Prymitywów.

sampler = SamplerV2(mode=backend)

if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)

pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9426757812499998

Wizualizacja

Zwizualizujmy poprawę wartości oczekiwanej uzyskaną dzięki cięciu przewodów.

ax = plt.gca()
methods = ["cut", "uncut"]
values = [reconstructed_expval, uncut_expval]

plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
ax.set_ylim([0.85, 1.02])
plt.text(0.3, 0.99, "Exact result")
plt.show()

Output of the previous code cell

Wnioski

Obserwujemy, że zarówno w problemach na małą, jak i dużą skalę cięcie przewodów prowadzi do lepszych wyników niż wykonanie nieciętego Circuitu. Należy zauważyć, że w tych eksperymentach nie zastosowano żadnych technik mitygacji błędów. Poprawa wyników jest zatem wyłącznie efektem cięcia przewodów. Możliwe jest dalsze polepszenie wyników poprzez łączenie różnych metod mitygacji z cięciem Circuitu.

Co więcej, w tym notatniku obliczenia obu podukładów przeprowadziliśmy na tym samym sprzęcie. W [5], [6] autorzy przedstawiają metodę rozdzielania podukładów na różny sprzęt z uwzględnieniem informacji o szumach, co pozwala zmaksymalizować tłumienie szumów i zrównoleglić proces.

Dodatek: uwagi dotyczące skalowania zasobów

Liczba Circuitów do wykonania rośnie wraz z liczbą cięć. Dlatego choć wiele cięć może tworzyć małe podukłady, co dalej poprawia wydajność, prowadzi to również do znacznie większej liczby wykonań Circuitów, co w większości przypadków może być niepraktyczne. Poniżej pokazujemy przykład liczby podukładów odpowiadającej liczbie cięć dla 50-qubitowego Circuitu.

wc-5.png

Zwróć uwagę, że już przy pięciu cięciach liczba subexperymentów wynosi około 200 tys. Dlatego cięcie Circuitów należy stosować tylko wtedy, gdy liczba cięć jest mała.

Przykład Circuitu przyjaznego i nieprzyjaznego dla cięcia

Circuit przyjazny dla cięcia

Jak wspomniano wcześniej, Circuit jest przyjazny dla cięcia, gdy można go podzielić na mniejsze rozłączne podukłady przy niewielkiej liczbie cięć. Każdy Circuit sprzętowo-wydajny, czyli taki, który wymaga niewielu lub żadnych bramek SWAP przy mapowaniu na topologię sprzętu, jest generalnie przyjazny dla cięcia. Poniżej pokazujemy przykład ansatzu zachowującego wzbudzenia, stosowanego w chemii kwantowej. Zwróć uwagę, że taki Circuit można podzielić na dwa podukłady jednym cięciem, niezależnie od liczby Qubitów.

wc-6.png

Circuit nieprzyjazny dla cięcia

Circuit jest nieprzyjazny dla cięcia, jeśli liczba cięć wymaganych do uformowania rozłącznych partycji rośnie znacznie wraz z głębokością lub liczbą Qubitów. Przypomnij, że każde cięcie wymaga dodatkowego Qubitu, więc wraz z liczbą cięć efektywna liczba Qubitów również rośnie. Poniżej pokazujemy przykład 3-qubitowego Circuitu Grovera z możliwym przypadkiem cięcia.

wc-7.png

Obserwujemy, że wymagane są trzy cięcia, a cięcie jest bardziej pionowe niż poziome. Oznacza to, że liczba cięć ma liniowo rosnąć wraz z liczbą Qubitów, co nie jest korzystne dla cięcia.

Odniesienia

[1] Peng, T., Harrow, A. W., Ozols, M., & Wu, X. (2020). Simulating large quantum circuits on a small quantum computer. Physical review letters, 125(15), 150504.

[2] Tang, W., Tomesh, T., Suchara, M., Larson, J., & Martonosi, M. (2021, April). Cutqc: using small quantum computers for large quantum circuit evaluations. In Proceedings of the 26th ACM International conference on architectural support for programming languages and operating systems (pp. 473-486).

[3] Perlin, M. A., Saleem, Z. H., Suchara, M., & Osborn, J. C. (2021). Quantum circuit cutting with maximum-likelihood tomography. npj Quantum Information, 7(1), 64.

[4] Majumdar, R., & Wood, C. J. (2022). Error mitigated quantum circuit cutting. arXiv preprint arXiv:2211.13431.

[5] Khare, T., Majumdar, R., Sangle, R., Ray, A., Seshadri, P. V., & Simmhan, Y. (2023). Parallelizing Quantum-Classical Workloads: Profiling the Impact of Splitting Techniques. In 2023 IEEE International Conference on Quantum Computing and Engineering (QCE) (Vol. 1, pp. 990-1000). IEEE.

[6] Bhoumik, D., Majumdar, R., Saha, A., & Sur-Kolay, S. (2023). Distributed Scheduling of Quantum Circuits with Noise and Time Optimization. arXiv preprint arXiv:2309.06005.

Ankieta dotycząca samouczka

Prosimy o wypełnienie krótkiej ankiety z opinią na temat tego samouczka. Twoje spostrzeżenia pomogą nam ulepszyć nasze materiały i doświadczenie użytkownika.

Link do ankiety

Note: This survey is provided by IBM Quantum and relates to the original English content. To give feedback on doQumentation's website, translations, or code execution, please open a GitHub issue.