Przejdź do głównej treści

Optymalizacja obwodów kwantowych

uwaga

Toshinari Itoko (21 czerwca 2024)

Pobierz plik pdf oryginalnego wykładu. Zwróć uwagę, że niektóre fragmenty kodu mogą być nieaktualne, ponieważ są to obrazy statyczne.

Przybliżony czas QPU potrzebny do uruchomienia tego eksperymentu to 15 s.

(Uwaga: Niektóre komórki części 2 zostały skopiowane z notatnika "Qiskit Deep dive", napisanego przez Matthew Treinisha (opiekun Qiskit))

# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-aer qiskit-ibm-runtime
# !pip install 'qiskit[visualization]'
# !pip install qiskit_ibm_runtime qiskit_aer
# !pip install jupyter
# !pip install matplotlib pylatexenc pydot pillow
import qiskit

qiskit.__version__
'2.0.2'
import qiskit_ibm_runtime

qiskit_ibm_runtime.__version__
'0.40.1'
import qiskit_aer

qiskit_aer.__version__
'0.17.1'

1. Wprowadzenie

Ta lekcja omawia kilka aspektów optymalizacji obwodów w obliczeniach kwantowych. W szczególności zobaczymy, jaką wartość ma optymalizacja obwodów, korzystając z ustawień optymalizacji wbudowanych w Qiskit. Następnie zagłębimy się nieco bardziej i zobaczymy, co można zrobić jako ekspert w konkretnej dziedzinie zastosowań, aby budować obwody w inteligentny sposób. Na koniec przyjrzymy się uważnie temu, co dzieje się podczas transpilacji, co pomaga nam optymalizować nasze obwody.

2. Optymalizacja obwodów ma znaczenie

Najpierw porównamy wyniki uruchomienia obwodów przygotowujących 5-kubitowy stan GHZ (12(00000+11111)\frac{1}{\sqrt{2}} \left( |00000\rangle + |11111\rangle \right)) z optymalizacją i bez niej.

from qiskit.circuit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.primitives import BackendSamplerV2 as Sampler
from qiskit_ibm_runtime.fake_provider import FakeBrisbane

backend = FakeBrisbane()

Najpierw używamy obwodu GHZ zsyntetyzowanego naiwnie w następujący sposób.

num_qubits = 5

ghz_circ = QuantumCircuit(num_qubits)
ghz_circ.h(0)
[ghz_circ.cx(0, i) for i in range(1, num_qubits)]
ghz_circ.measure_all()
ghz_circ.draw("mpl")

Wynik poprzedniej komórki kodu

2.1 Poziom optymalizacji

Dostępne są 4 wartości optimization_level od 0 do 3. Im wyższy poziom optymalizacji, tym więcej wysiłku obliczeniowego jest poświęcane na optymalizację obwodu. Poziom 0 nie wykonuje żadnej optymalizacji i wykonuje jedynie minimalną ilość pracy niezbędną, aby obwód mógł zostać uruchomiony na wybranym backendzie. Poziom 3 poświęca najwięcej wysiłku (i zazwyczaj czasu wykonania) na próbę optymalizacji obwodu. Poziom 1 jest domyślnym poziomem optymalizacji. Transpilujemy obwód bez optymalizacji (optimization_level=0) oraz z optymalizacją (optimization_level=2). Widzimy dużą różnicę w długości transpilowanych obwodów.

pm0 = generate_preset_pass_manager(
optimization_level=0, backend=backend, seed_transpiler=777
)
pm2 = generate_preset_pass_manager(
optimization_level=2, backend=backend, seed_transpiler=777
)
circ0 = pm0.run(ghz_circ)
circ2 = pm2.run(ghz_circ)
print("optimization_level=0:")
display(circ0.draw("mpl", idle_wires=False, fold=-1))
print("optimization_level=2:")
display(circ2.draw("mpl", idle_wires=False, fold=-1))
optimization_level=0:

Wynik poprzedniej komórki kodu

optimization_level=2:

Wynik poprzedniej komórki kodu

2.2 Ćwiczenie

Wypróbuj również optimization_level=1 i porównaj wynikowy obwód z powyższymi dwoma. Wypróbuj to, modyfikując powyższy kod.

Rozwiązanie:

pm1 = generate_preset_pass_manager(
optimization_level=1, backend=backend, seed_transpiler=777
)
circ1 = pm1.run(ghz_circ)
print("optimization_level=1:")
display(circ1.draw("mpl", idle_wires=False, fold=-1))
optimization_level=1:

Wynik poprzedniej komórki kodu

Uruchom na fałszywym backendzie (symulacja z szumem). Zobacz Dodatek 1, aby dowiedzieć się, jak uruchomić na rzeczywistym backendzie.

# run the circuits on the fake backend (noisy simulator)
sampler = Sampler(backend=backend)
job = sampler.run([circ0, circ2], shots=10000)
print(f"Job ID: {job.job_id()}")
Job ID: 93a4ac70-e3ea-44ad-aea9-5045840c9076
# get results
result = job.result()
unoptimized_result = result[0].data.meas.get_counts()
optimized_result = result[1].data.meas.get_counts()
from qiskit.visualization import plot_histogram

# plot
sim_result = {"0" * 5: 0.5, "1" * 5: 0.5}
plot_histogram(
[result for result in [sim_result, unoptimized_result, optimized_result]],
bar_labels=False,
legend=[
"ideal",
"no optimization",
"with optimization",
],
)

Wynik poprzedniej komórki kodu

3. Synteza obwodu ma znaczenie

Następnie porównujemy wyniki uruchomienia dwóch różnie zsyntetyzowanych obwodów przygotowujących 5-kubitowy stan GHZ (12(00000+11111)\frac{1}{\sqrt{2}} \left( |00000\rangle + |11111\rangle \right)).

# Original GHZ circuit (naive synthesis)
ghz_circ.draw("mpl")

Wynik poprzedniej komórki kodu

# A cleverly-synthesized GHZ circuit
ghz_circ2 = QuantumCircuit(5)
ghz_circ2.h(2)
ghz_circ2.cx(2, 1)
ghz_circ2.cx(2, 3)
ghz_circ2.cx(1, 0)
ghz_circ2.cx(3, 4)
ghz_circ2.measure_all()
ghz_circ2.draw("mpl")

Wynik poprzedniej komórki kodu

# transpile both with the same optimization level 2
circ_org = pm2.run(ghz_circ)
circ_new = pm2.run(ghz_circ2)
print("original synthesis:")
display(circ_org.draw("mpl", idle_wires=False, fold=-1))
print("new synthesis:")
display(circ_new.draw("mpl", idle_wires=False, fold=-1))
original synthesis:

Wynik poprzedniej komórki kodu

new synthesis:

Wynik poprzedniej komórki kodu

Nowa synteza daje płytszy obwód. Dlaczego?

Dzieje się tak, ponieważ nowy obwód można rozmieścić na liniowo połączonych kubitach, a więc również na grafie połączeń o strukturze ciężkiego heksagonu (heavy-hexagon) w IBM® Brisbane, podczas gdy oryginalny obwód wymaga łączności o kształcie gwiazdy (węzeł stopnia 4) i przez to nie może być rozmieszczony na grafie połączeń heavy-hex, którego węzły mają co najwyżej stopień 3. W rezultacie oryginalny obwód wymaga routingu kubitów, który dodaje bramki SWAP, zwiększając liczbę bramek.

To, co zrobiliśmy w nowym obwodzie, można uznać za ręczną syntezę obwodu „świadomą ograniczeń łączności". Innymi słowy: ręczne rozwiązanie jednocześnie syntezy obwodu i mapowania obwodu.

# run the circuits
sampler = Sampler(backend=backend)
job = sampler.run([circ_org, circ_new], shots=10000)
print(f"Job ID: {job.job_id()}")
Job ID: 19d635b0-4d8b-44c2-a76e-49e4b9078b1b
# get results
result = job.result()
synthesis_org_result = result[0].data.meas.get_counts()
synthesis_new_result = result[1].data.meas.get_counts()
# plot
sim_result = {"0" * 5: 0.5, "1" * 5: 0.5}
plot_histogram(
[
result
for result in [
sim_result,
unoptimized_result,
synthesis_org_result,
synthesis_new_result,
]
],
bar_labels=False,
legend=[
"ideal",
"no optimization",
"synthesis_org",
"synthesis_new",
],
)

Wynik poprzedniej komórki kodu

Ogólnie synteza obwodu zależy od zastosowania i zbyt trudno jest, aby oprogramowanie pokrywało wszystkie możliwe zastosowania. Transpiler Qiskit nie ma funkcji syntezy obwodu przygotowującego stan GHZ. W takim przypadku warto rozważyć ręczną syntezę obwodu, jak pokazano powyżej. W tej sekcji przyjrzymy się szczegółom działania transpilera Qiskit, używając poniższego przykładowego obwodu.

# Build a toy example circuit
from math import pi
import itertools
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import excitation_preserving

circuit = QuantumCircuit(4, name="Example circuit")
circuit.append(excitation_preserving(4, reps=1, flatten=True), range(4))
circuit.measure_all()

value_cycle = itertools.cycle([0, pi / 4, pi / 2, 3 * pi / 4, pi, 2 * pi])
circuit.assign_parameters(
[x[1] for x in zip(range(len(circuit.parameters)), value_cycle)], inplace=True
)
circuit.draw("mpl")

Wynik poprzedniej komórki kodu

3.1 Narysuj cały przepływ transpilacji Qiskit

Przyjrzymy się przejściom (zadaniom) transpilera dla optimization_level=1.

from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

# There is no need to read this entire image, but this outputs all the steps in the transpile() call
# for optimization level 1
pm = generate_preset_pass_manager(1, backend, seed_transpiler=42)
pm.draw()

Wynik poprzedniej komórki kodu

Przepływ składa się z sześciu etapów:

print(pm.stages)
('init', 'layout', 'routing', 'translation', 'optimization', 'scheduling')

3.2 Rysowanie pojedynczego etapu

Najpierw narysujmy wszystkie zadania (przebiegi transpilatora) wykonywane na etapie init.

pm.init.draw()

Wynik poprzedniej komórki kodu

Możemy uruchomić każdy pojedynczy etap. Uruchommy etap init dla naszego obwodu. Włączając logger, możemy zobaczyć szczegóły wykonania.

import logging

logger = logging.getLogger()
logger.setLevel("INFO")

init_out = pm.init.run(circuit)
init_out.draw("mpl", fold=-1)
INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03576 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.16618 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.07176 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.27299 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00811 (ms)

Wynik poprzedniej komórki kodu

3.3 Ćwiczenie

Narysuj przebiegi etapu layout i uruchom ten etap dla obwodu wyjściowego etapu init (init_out), modyfikując komórki użyte powyżej.

Rozwiązanie:

display(pm.layout.draw())
layout_out = pm.layout.run(init_out)
layout_out.draw("mpl", idle_wires=False, fold=-1)

Wynik poprzedniej komórki kodu

INFO:qiskit.passmanager.base_tasks:Pass: SetLayout - 0.01001 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: TrivialLayout - 0.07129 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: CheckMap - 0.08917 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: VF2Layout - 1.24431 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: BarrierBeforeFinalMeasurements - 0.02599 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: SabreLayout - 5.11169 (ms)

Wynik poprzedniej komórki kodu

Zrób to samo dla etapu translation.

Rozwiązanie:

display(pm.translation.draw())
basis_out = pm.translation.run(layout_out)
basis_out.draw("mpl", idle_wires=False, fold=-1)

Wynik poprzedniej komórki kodu

INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03386 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.02718 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 2.64192 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: CheckGateDirection - 0.02217 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: GateDirection - 0.36502 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.64778 (ms)

Wynik poprzedniej komórki kodu

Uwaga: Nie każdy pojedynczy etap można zawsze uruchomić niezależnie (ponieważ niektóre z nich muszą przenosić informacje z jednego z poprzednich etapów).

3.4 Etap optymalizacji

Ostatnim domyślnym etapem w potoku jest optymalizacja. Po osadzeniu obwodu dla urządzenia docelowego obwód znacznie się rozrósł. Większość tego wynika z nieefektywności w relacjach równoważności z translacji bazy i wstawiania operacji swap. Etap optymalizacji służy do próby zminimalizowania rozmiaru i głębokości obwodu. Wykonuje serię przebiegów w pętli do while, aż osiągnie stabilne wyjście.

# pm.pre_optimization.draw()
pm.optimization.draw()

Wynik poprzedniej komórki kodu

logger = logging.getLogger()
logger.setLevel("INFO")
opt_out = pm.optimization.run(basis_out)
INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.30112 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.03195 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.01216 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.01001 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.63729 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.41723 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.01192 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.05484 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.08583 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.20599 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00787 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00715 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.16809 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.17190 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00691 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.02408 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.04935 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00525 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00620 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00286 (ms)
opt_out.draw("mpl", idle_wires=False, fold=-1)

Wynik poprzedniej komórki kodu

4. Szczegółowe przykłady

4.1 Optymalizacja bloków dwukubitowych z wykorzystaniem syntezy unitarnej dwóch kubitów

Dla poziomów 2 i 3 mamy więcej przebiegów (Collect2qBlocks, ConsolidateBlocks, UnitarySynthesis) dla bardziej zaawansowanej optymalizacji, a mianowicie optymalizacji bloków dwukubitowych. (Porównaj przepływ etapu optymalizacji dla poziomu 2 z powyższym dla poziomu 1)

Optymalizacja bloków dwukubitowych składa się z dwóch kroków: zbierania i konsolidacji bloków dwukubitowych oraz syntezy macierzy unitarnych dwóch kubitów.

pm2 = generate_preset_pass_manager(2, backend, seed_transpiler=42)
pm2.optimization.draw()

Wynik poprzedniej komórki kodu

from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import (
Collect2qBlocks,
ConsolidateBlocks,
UnitarySynthesis,
)

# Collect 2q blocks and consolidate to unitary when we expect that we can reduce the 2q gate count for that unitary
consolidate_pm = PassManager(
[
Collect2qBlocks(),
ConsolidateBlocks(target=backend.target),
]
)
display(basis_out.draw("mpl", idle_wires=False, fold=-1))

consolidated = consolidate_pm.run(basis_out)
consolidated.draw("mpl", idle_wires=False, fold=-1)

Wynik poprzedniej komórki kodu

Wynik poprzedniej komórki kodu

# Synthesize unitaries
UnitarySynthesis(target=backend.target)(consolidated).draw(
"mpl", idle_wires=False, fold=-1
)

Wynik poprzedniej komórki kodu

logger.setLevel("WARNING")

W części 2 zobaczyliśmy, że rzeczywisty przepływ kompilatora kwantowego nie jest aż tak prosty i składa się z wielu przebiegów (zadań). Wynika to głównie z inżynierii oprogramowania wymaganej do zapewnienia wydajności dla szerokiego zakresu obwodów aplikacyjnych oraz utrzymywalności oprogramowania. Transpiler Qiskit sprawdziłby się w większości przypadków, ale jeśli zdarzy ci się zauważyć, że twój obwód nie jest dobrze optymalizowany przez transpiler Qiskit, byłaby to dobra okazja, aby zbadać własną optymalizację obwodu specyficzną dla aplikacji, jak pokazano w części 1. Technologia transpilera ewoluuje, twój wkład badawczo-rozwojowy jest mile widziany.

from qiskit.circuit import QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService, Sampler
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
sampler = Sampler(backend)
circ = QuantumCircuit(3)
circ.ccx(0, 1, 2)
circ.measure_all()
circ.draw("mpl")

Wynik poprzedniej komórki kodu

sampler.run([circ])  # IBMInputValueError will be raised

4.2 Optymalizacja obwodu ma znaczenie

Najpierw porównujemy wyniki uruchamiania obwodów przygotowujących 5-kubitowy stan GHZ (12(00000+11111)\frac{1}{\sqrt{2}} \left( |00000\rangle + |11111\rangle \right)) z optymalizacją i bez niej.

from qiskit.circuit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, Sampler
service = QiskitRuntimeService()
# backend = service.backend('ibm_brisbane')
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
) # Eagle
backend

Najpierw używamy obwodu GHZ zsyntetyzowanego w naiwny sposób, jak poniżej.

num_qubits = 5

ghz_circ = QuantumCircuit(num_qubits)
ghz_circ.h(0)
[ghz_circ.cx(0, i) for i in range(1, num_qubits)]
ghz_circ.measure_all()
ghz_circ.draw("mpl")

Wynik poprzedniej komórki kodu

Transpilujemy obwód bez optymalizacji (optimization_level=0) i z optymalizacją (optimization_level=2). Jak widać, istnieje duża różnica w długości obwodów transpilowanych.

pm0 = generate_preset_pass_manager(
optimization_level=0, backend=backend, seed_transpiler=777
)
pm2 = generate_preset_pass_manager(
optimization_level=2, backend=backend, seed_transpiler=777
)
circ0 = pm0.run(ghz_circ)
circ2 = pm2.run(ghz_circ)
print("optimization_level=0:")
display(circ0.draw("mpl", idle_wires=False, fold=-1))
print("optimization_level=2:")
display(circ2.draw("mpl", idle_wires=False, fold=-1))
optimization_level=0:

Wynik poprzedniej komórki kodu

optimization_level=2:

Wynik poprzedniej komórki kodu

# run the circuits
sampler = Sampler(backend)
job = sampler.run([circ0, circ2], shots=10000)
job_id = job.job_id()
print(f"Job ID: {job_id}")
Job ID: d13rnnemya70008ek1zg
# REPLACE WITH YOUR OWN JOB IDS
job = service.job(job_id)
# get results
result = job.result()
unoptimized_result = result[0].data.meas.get_counts()
optimized_result = result[1].data.meas.get_counts()
from qiskit.visualization import plot_histogram

# plot
sim_result = {"0" * 5: 0.5, "1" * 5: 0.5}
plot_histogram(
[result for result in [sim_result, unoptimized_result, optimized_result]],
bar_labels=False,
legend=[
"ideal",
"no optimization",
"with optimization",
],
)

Wynik poprzedniej komórki kodu

4.3 Synteza obwodu ma znaczenie

Następnie porównujemy wyniki uruchamiania dwóch różnie zsyntetyzowanych obwodów przygotowujących 5-kubitowy stan GHZ (12(00000+11111)\frac{1}{\sqrt{2}} \left( |00000\rangle + |11111\rangle \right)).

# Original GHZ circuit (naive synthesis)
ghz_circ.draw("mpl")

Wynik poprzedniej komórki kodu

# A better GHZ circuit (smarter synthesis), you learned in a previous lecture
ghz_circ2 = QuantumCircuit(5)
ghz_circ2.h(2)
ghz_circ2.cx(2, 1)
ghz_circ2.cx(2, 3)
ghz_circ2.cx(1, 0)
ghz_circ2.cx(3, 4)
ghz_circ2.measure_all()
ghz_circ2.draw("mpl")

Wynik poprzedniej komórki kodu

circ_org = pm2.run(ghz_circ)
circ_new = pm2.run(ghz_circ2)
print("original synthesis:")
display(circ_org.draw("mpl", idle_wires=False, fold=-1))
print("new synthesis:")
display(circ_new.draw("mpl", idle_wires=False, fold=-1))
original synthesis:

Wynik poprzedniej komórki kodu

new synthesis:

Wynik poprzedniej komórki kodu

# run the circuits
sampler = Sampler(backend)
job = sampler.run([circ_org, circ_new], shots=10000)
job_id = job.job_id()
print(f"Job ID: {job_id}")
Job ID: d13rp283grvg008j12fg
# REPLACE WITH YOUR OWN JOB IDS
job = service.job(job_id)
# get results
result = job.result()
synthesis_org_result = result[0].data.meas.get_counts()
synthesis_new_result = result[1].data.meas.get_counts()
# plot
sim_result = {"0" * 5: 0.5, "1" * 5: 0.5}
plot_histogram(
[result for result in [sim_result, synthesis_org_result, synthesis_new_result]],
bar_labels=False,
legend=[
"ideal",
"synthesis_org",
"synthesis_new",
],
)

Wynik poprzedniej komórki kodu

4.4 Ogólna dekompozycja bramki 1-kubitowej

from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.circuit.library.standard_gates import UGate

phi, theta, lam = Parameter("φ"), Parameter("θ"), Parameter("λ")
qc = QuantumCircuit(1)
qc.append(UGate(theta, phi, lam), [0])
qc.draw(output="mpl")

Wynik poprzedniej komórki kodu

transpile(qc, basis_gates=["rz", "sx"]).draw(output="mpl")

Wynik poprzedniej komórki kodu

4.5 Optymalizacja bloku jednokubitowego

from qiskit import QuantumCircuit

qc = QuantumCircuit(1)
qc.x(0)
qc.y(0)
qc.z(0)
qc.rx(1.23, 0)
qc.ry(1.23, 0)
qc.rz(1.23, 0)
qc.h(0)
qc.s(0)
qc.t(0)
qc.sx(0)
qc.sdg(0)
qc.tdg(0)
qc.draw(output="mpl")

Wynik poprzedniej komórki kodu

from qiskit.quantum_info import Operator

Operator(qc)
Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],
[ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],
input_dims=(2,), output_dims=(2,))
from qiskit import transpile

qc_opt = transpile(qc, basis_gates=["rz", "sx"])
qc_opt.draw(output="mpl")

Wynik poprzedniej komórki kodu

Operator(qc_opt)
Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],
[ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],
input_dims=(2,), output_dims=(2,))
Operator(qc).equiv(Operator(qc_opt))
True

4.6 Dekompozycja bramki Toffoli

qc = QuantumCircuit(3)
qc.ccx(0, 1, 2)
qc.draw(output="mpl")

Wynik poprzedniej komórki kodu

from qiskit import QuantumCircuit, transpile

qc = QuantumCircuit(3)
qc.ccx(0, 1, 2)
qc = transpile(qc, basis_gates=["rz", "sx", "cx"])
qc.draw(output="mpl")

Wynik poprzedniej komórki kodu

4.7 Dekompozycja bramki CU

from qiskit.circuit.library.standard_gates import CUGate

phi, theta, lam, gamma = Parameter("φ"), Parameter("θ"), Parameter("λ"), Parameter("γ")
qc = QuantumCircuit(2)
# qc.cu(theta, phi, lam, gamma, 0, 1)
qc.append(CUGate(theta, phi, lam, gamma), [0, 1])
qc.draw(output="mpl")

Wynik poprzedniej komórki kodu

from qiskit.circuit.library.standard_gates import CUGate

phi, theta, lam, gamma = Parameter("φ"), Parameter("θ"), Parameter("λ"), Parameter("γ")
qc = QuantumCircuit(2)
qc.append(CUGate(theta, phi, lam, gamma), [0, 1])
qc = transpile(qc, basis_gates=["rz", "sx", "cx"])
qc.draw(output="mpl")

Wynik poprzedniej komórki kodu

4.8 CX, ECR, CZ są równe z dokładnością do lokalnych bramek Clifforda

Zauważ, że HH(Hadamard), SS(rotacja Z o π/2\pi/2), SS^\dagger(rotacja Z o π/2-\pi/2), XX(Pauli X) są wszystkie bramkami Clifforda.

qc = QuantumCircuit(2)
qc.cx(0, 1)
qc.draw(output="mpl", style="bw")

Wynik poprzedniej komórki kodu

qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["x", "s", "h", "sdg", "ecr"]).draw(output="mpl", style="bw")

Wynik poprzedniej komórki kodu

qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["h", "cz"]).draw(output="mpl", style="bw")

Wynik poprzedniej komórki kodu

Wykorzystanie bramek bazowych 1q backendu IBM "rz", "sx" oraz "x".

qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["rz", "sx", "x", "ecr"]).draw(output="mpl", style="bw")

Wynik poprzedniej komórki kodu

qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["rz", "sx", "x", "cz"]).draw(output="mpl", style="bw")

Wynik poprzedniej komórki kodu

# Check Qiskit version
import qiskit

qiskit.__version__
'2.0.2'