Przejdź do głównej treści

Wprowadzenie do Qiskit

W tym notatniku przyjrzymy się, jak programować bramki kwantowe i obwody kwantowe w Qiskit, a także jak uruchamiać je na symulatorach i prawdziwych komputerach kwantowych przy użyciu wzorców Qiskit. Następnie przedstawimy różne sposoby kodowania informacji i zakończymy przykładem bonusowym – Teleportacją Kwantową.

Zanim zaczniesz

Postępuj zgodnie z instrukcjami Instalacja i konfiguracja, jeśli jeszcze tego nie zrobiłeś/zrobiłaś, w tym z krokami Konfiguracja do korzystania z IBM Quantum™ Platform.

Zalecane jest korzystanie ze środowiska deweloperskiego Jupyter do interakcji z komputerami kwantowymi. Pamiętaj, aby zainstalować zalecane dodatkowe wsparcie wizualizacji ('qiskit[visualization]'). Do drugiej części tego przykładu potrzebny będzie również pakiet matplotlib.

Aby dowiedzieć się więcej o obliczeniach kwantowych ogólnie, odwiedź kurs Podstawy informacji kwantowej na IBM Quantum Learning.

Importy

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
# Import necessary modules for this notebook
import time
import qiskit

from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_bloch_multivector, plot_state_qsphere
from qiskit_aer import AerSimulator
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.visualization import plot_histogram
print(qiskit.__version__)
2.3.1

Aby uruchamiać swoje obwody kwantowe na sprzęcie, musisz najpierw skonfigurować swoje konto. Możesz to zrobić w następujący sposób:

  1. Przejdź do ulepszonej platformy IBM Quantum®.
  2. Przejdź do prawego górnego rogu (jak pokazano na powyższym obrazku), utwórz swój token API i skopiuj go w bezpieczne miejsce.
  3. W następnej komórce zastąp deleteThisAndPasteYourAPIKeyHere swoim kluczem API.
  4. Przejdź do lewego dolnego rogu (jak pokazano na powyższym obrazku) i utwórz swoje wystąpienie. Upewnij się, że wybierasz plan otwarty.
  5. Po utworzeniu wystąpienia skopiuj powiązany z nim kod CRN. Może być konieczne odświeżenie strony, aby zobaczyć wystąpienie.
  6. W komórce poniżej zastąp deleteThisAndPasteYourCRNHere swoim kodem CRN.

Więcej szczegółów na temat konfiguracji konta IBM Cloud® znajdziesz w tym przewodniku.

⚠️ Uwaga: Traktuj swój klucz API jak bezpieczne hasło. Więcej informacji na temat korzystania z klucza API w środowiskach bezpiecznych i niezaufanych znajdziesz w przewodniku Konfiguracja w chmurze.

#your_api_key = "deleteThisAndPasteYourAPIKeyHere"
#your_crn = "deleteThisAndPasteYourCRNHere"

QiskitRuntimeService.save_account(
channel="ibm_quantum_platform",
token=your_api_key,
instance=your_crn,
overwrite=True
)

1. Bramki kwantowe i obwody kwantowe

Obwody kwantowe to modele obliczeń kwantowych, w których obliczenie jest sekwencją bramek kwantowych. Przyjrzyjmy się niektórym popularnym bramkom kwantowym.

Bramka X

Gate X odpowiada obrotowi wokół osi X sfery Blocha o π\pi radianów. Mapuje 0|0\rangle na 1|1\rangle i 1|1\rangle na 0|0\rangle. Jest to kwantowy odpowiednik bramki NOT dla klasycznych komputerów i jest czasami nazywana odwróceniem bitu.

X=(0110)X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \\ \end{pmatrix}

# Let's apply an X-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.draw(output='mpl')

Quantum circuit diagram

# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)

Code output

Bramka H

Gate Hadamarda reprezentuje obrót o π\pi wokół osi leżącej w połowie drogi między osią XX a osią ZZ. Mapuje stan bazowy 0|0\rangle na 0+12\frac{|0\rangle + |1\rangle}{\sqrt{2}}, co oznacza, że pomiar będzie miał jednakowe prawdopodobieństwo dania wyniku 1 lub 0, tworząc "superpozycję" stanów. Stan ten jest również zapisywany jako +|+\rangle.

H=12(1111)H = \frac{1}{\sqrt{2}}\begin{pmatrix} 1 & 1 \\ 1 & -1 \\ \end{pmatrix}

# Let's apply an H-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.h(0)
qc.draw(output='mpl')

Quantum circuit diagram

# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)

Code output

Bramka CX (CNOT)

Gate kontrolowane NOT (CNOT lub CX) działa na dwóch Qubitach. Wykonuje operację NOT (równoważną zastosowaniu Gate X) na drugim Qubicie tylko wtedy, gdy pierwszy Qubit jest 1|1\rangle, w przeciwnym razie pozostawia go bez zmian. Uwaga: Qiskit numeruje bity w ciągu od prawej do lewej.

CX=(1000010000010010)CX = \begin{pmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & 0 & 1\\ 0 & 0 & 1 & 0\\ \end{pmatrix}

# Let's apply a CX-gate on |11>
qc = QuantumCircuit(2)
qc.x(0)
qc.x(1)
qc.cx(0,1)
qc.draw(output='mpl')

Quantum circuit diagram

sv=Statevector(qc)
plot_state_qsphere(sv)

Code output

Utwórz pierwszy stan Bella

ϕ+=12(00+11)|\phi^+ \rangle = \frac{1}{\sqrt 2}(|00 \rangle + |11 \rangle)

# Create a Bell state circuit

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0,1)

# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

# Plot the state using q-sphere visualization
sv = Statevector(qc)
plot_state_qsphere(sv)
# q-sphere is useful for visualizing states when Bloch sphere fails to

Code output

Utwórz drugi stan Bella

ϕ=12(0011)|\phi^- \rangle = \frac{1}{\sqrt 2}(|00 \rangle - |11 \rangle)

# Create a circuit with the second Bell state

qc = QuantumCircuit(2)
qc.x(0)
qc.h(0)
qc.cx(0,1)

qc.draw("mpl")

Quantum circuit diagram

Wyjaśnienie jest następujące:

H1=12(01)=H|1\rangle=\frac{1}{\sqrt{2} }(|0\rangle-|1\rangle) = |-\rangle
# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

Utwórz 3-Qubitowy stan GHZ

GHZ=12(000+111)|GHZ \rangle = \frac{1}{\sqrt 2}(|000 \rangle + |111 \rangle)

# Create a circuit with 3-qubit GHZ state

qc= QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)

qc.draw("mpl")

Quantum circuit diagram

# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

Utwórz stan logo Qiskit

Qiskit=12(0010+1101)|Qiskit \rangle = \frac{1}{\sqrt 2}(|0010 \rangle + |1101 \rangle)

Centered Image
# Create a circuit with the Qiskit logo state

qc = QuantumCircuit(4)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
qc.cx(0,3)
qc.x(1)

# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

2. Tworzenie i uruchamianie prostego programu kwantowego

Cztery kroki pisania programu kwantowego przy użyciu wzorców Qiskit to:

  1. Zmapuj problem na format natywny dla kwantowości.

  2. Zoptymalizuj obwody i operatory.

  3. Wykonaj za pomocą kwantowej funkcji prymitywnej.

  4. Przeanalizuj wyniki.

2.1 Odwzoruj problem w formacie natywnym dla obliczeń kwantowych

W programie kwantowym obwody kwantowe (Circuit) są natywnym formatem reprezentacji instrukcji kwantowych, a operatory reprezentują obserwable, które mają być mierzone. Tworząc Circuit, zazwyczaj tworzysz nowy obiekt QuantumCircuit, a następnie dodajesz do niego instrukcje w kolejności.

Poniższa komórka kodu tworzy Circuit, który generuje stan GHZ — stan, w którym trzy Qubity są ze sobą w pełni splątane.

Qiskit SDK używa numerowania bitów LSb 0, gdzie nn-ta cyfra ma wartość 1n1 \ll n lub 2n2^n. Więcej szczegółów znajdziesz w temacie Bit-ordering in the Qiskit SDK.

# Create a GHZ state circuit

qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

W dokumentacji znajdziesz wszystkie dostępne operacje dla QuantumCircuit.

Tworząc obwody kwantowe, musisz również zastanowić się, jakiego rodzaju dane chcesz otrzymać po wykonaniu. Qiskit oferuje dwa sposoby zwracania danych: możesz uzyskać rozkład prawdopodobieństwa dla wybranego zestawu Qubitów do pomiaru lub wartość oczekiwaną obserwabla. Przygotuj swoje zadanie obliczeniowe tak, aby mierzyć Circuit na jeden z tych dwóch sposobów za pomocą prymitywów Qiskit (szczegółowo omówionych w Kroku 3).

Ten przykład mierzy wartości oczekiwane przy użyciu podmodułu qiskit.quantum_info, określanego przez operatory (obiekty matematyczne używane do reprezentowania działania lub procesu zmieniającego stan kwantowy). Poniższa komórka kodu tworzy sześć trój-qubitowych operatorów Pauliego: ZZZ, ZZX, ZII, XXI, ZZI oraz III.

# Set up six different observables.

observables_labels = ["ZZZ", "ZZX", "ZII", "XXI", "ZZI", "III"]

observables = [SparsePauliOp(label) for label in observables_labels]
print(observables)
[SparsePauliOp(['ZZZ'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZX'],
coeffs=[1.+0.j]), SparsePauliOp(['ZII'],
coeffs=[1.+0.j]), SparsePauliOp(['XXI'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZI'],
coeffs=[1.+0.j]), SparsePauliOp(['III'],
coeffs=[1.+0.j])]

Tutaj operator taki jak ZZI jest skrótem iloczynu tensorowego ZZIZ\otimes Z\otimes I, co oznacza jednoczesny pomiar Z na Qubicie 2 i Z na Qubicie 1 oraz uzyskanie informacji o korelacji między Qubitem 2 a Qubitem 1. Wartości oczekiwane tego rodzaju są zazwyczaj zapisywane jako Z2Z1\langle Z_2 Z_1 \rangle.

Jeśli obserwowany stan to trój-qubitowy stan GHZ, pomiar Z2Z1\langle Z_2 Z_1 \rangle powinien wynosić 1.

2.2 Optymalizuj obwody i operatory

Wykonując obwody na urządzeniu, ważne jest zoptymalizowanie zestawu instrukcji zawartych w Circuit oraz zminimalizowanie ogólnej głębokości (w przybliżeniu liczby instrukcji) obwodu. Dzięki temu uzyskasz jak najlepsze wyniki poprzez ograniczenie efektów błędów i szumów. Ponadto instrukcje Circuit muszą być zgodne z Instruction Set Architecture (ISA) urządzenia Backend oraz uwzględniać bramki bazowe (basis gates) i połączenia między Qubitami danego urządzenia.

Poniższy kod tworzy instancję rzeczywistego urządzenia, do którego zostanie wysłane zadanie, oraz przekształca Circuit i obserwable tak, aby były zgodne z ISA tego Backendu. Jeśli wcześniej nie zapisałeś swoich danych uwierzytelniających, postępuj zgodnie z instrukcjami tutaj, aby uwierzytelnić się przy użyciu tokenu API.

# Choose a real backend
service = QiskitRuntimeService(channel='ibm_quantum_platform',)
backend = service.least_busy(min_num_qubits=156)
# print backend details
print(
f"Name: {backend.name}\n"
f"Version: {backend.backend_version}\n"
f"No. of qubits: {backend.num_qubits}\n"
f"Processor type: {backend.processor_type}\n"
)
Name: ibm_marrakesh
Version: 1.0.21
No. of qubits: 156
Processor type: {'family': 'Heron', 'revision': '2'}
# option to use the AerSimulator instead of a real quantum device
seed_sim=42
backend=AerSimulator.from_backend(backend,seed_simulator=seed_sim)

Transpiluj Circuit do postaci obwodu ISA

# Convert to an ISA circuit and layout-mapped observables.

pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc)

isa_circuit.draw("mpl", idle_wires=False)

Quantum circuit diagram

mapped_observables = [
observable.apply_layout(isa_circuit.layout) for observable in observables
]
print(mapped_observables)
[SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIXIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])]

2.3 Wykonanie przy użyciu kwantowych primitives

Komputery kwantowe mogą dawać losowe wyniki, dlatego zazwyczaj zbierasz próbkę wyjść, uruchamiając Circuit wiele razy. Możesz oszacować wartość obserwabli, korzystając z klasy Estimator. Estimator to jeden z dwóch primitives; drugim jest Sampler, który służy do pobierania danych z komputera kwantowego. Obiekty te posiadają metodę run(), która wykonuje wybrany zestaw Circuit, obserwabli i parametrów (jeśli dotyczy), korzystając z primitive unified bloc (PUB). Jeśli uruchamiasz ten kod na prawdziwym sprzęcie kwantowym, rozważ zastosowanie technik mitygacji i tłumienia błędów, aby zredukować szumy wewnętrzne komputera kwantowego.

# Construct the Estimator instance.
estimator = Estimator(mode=backend)
estimator.options.resilience_level = 1
estimator.options.default_shots = 5000

Prześlij zadanie przy użyciu primitiva Estimator.

# One pub, with one circuit to run against six different observables.
job = estimator.run([(isa_circuit, mapped_observables)])

# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job.job_id()}")
>>> Job ID: 97ecd036-1767-49b0-a1dc-c71638c3c3c4
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")

Po przesłaniu zadania możesz poczekać na jego zakończenie w bieżącej instancji Pythona lub użyć job_id, aby pobrać dane później. (Szczegóły znajdziesz w sekcji dotyczącej pobierania zadań.)

Po zakończeniu zadania sprawdź jego wyniki za pomocą atrybutu result() zadania.

# This is the result of the entire submission.  You submitted one Pub,
# so this contains one inner result (and some metadata of its own).
job_result = job.result()

# This is the result from our single pub, which had six observables,
# so contains information on all six.
pub_result = job.result()[0]

Teraz możemy również wykonać Circuit przy użyciu primitiva Sampler.

# We include the measurements in the circuit
qc.measure_all()
sampler = Sampler(mode=backend)
qc.draw(output="mpl")

Quantum circuit diagram

Prześlij zadanie przy użyciu primitiva Sampler.

job_sampler = sampler.run(pm.run([qc]))

# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job_sampler.job_id()}")
# Get the results
results_sampler = job_sampler.result()
>>> Job ID: a6ee4d2f-c80d-4a86-9a76-e4b1a74502e7

2.4 Analiza wyników

Krok analizy to zazwyczaj miejsce, w którym możesz przeprowadzić postprocesing wyników, korzystając na przykład z mitygacji błędów pomiarowych lub ekstrapolacji zerowego szumu (ZNE). Możesz podać te wyniki do innego przepływu pracy w celu dalszej analizy lub przygotować wykres kluczowych wartości i danych. Ogólnie rzecz biorąc, ten krok jest specyficzny dla twojego problemu. W tym przykładzie wykreśl każdą z wartości oczekiwanych zmierzonych dla naszego Circuit.

Wartości oczekiwane i odchylenia standardowe dla obserwabli podanych do Estimator są dostępne przez atrybuty PubResult.data.evs i PubResult.data.stds wyniku zadania. Aby uzyskać wyniki z Sampler, użyj funkcji PubResult.data.meas.get_counts(), która zwróci dict pomiarów w postaci bitstring jako kluczy i zliczeń jako odpowiadających im wartości. Więcej informacji znajdziesz w artykule Zacznij korzystać z Sampler.

# Plot the result
from matplotlib import pyplot as plt
values = pub_result.data.evs
errors = pub_result.data.stds
# plotting graph
# Plotting with error bars
plt.errorbar(observables_labels, values, yerr=errors, fmt='-o', capsize=5)
plt.xlabel("Observables")
plt.ylabel("Values")
plt.title("Plot of Observables vs Values with Error Bars")
plt.grid(True)
plt.tight_layout()
plt.show()

Plot output

Widzimy, że obserwable ZZIZZI i IIIIII mają wartość oczekiwaną równą 1, ponieważ ZZIZZI wprowadza dwa znaki minus, które się znoszą, a IIIIII działa jako tożsamość, pozostawiając stan GHZ niezmieniony. Pozostałe obserwable mają wartość oczekiwaną równą 0, gdyż ich operatory ZZ wprowadzają nieparzystą liczbę znaków minus lub operatory XX odwracają pewną liczbę Qubitów, powodując ortogonalność nakładających się stanów.

Teraz wykreślamy wyniki dla Sampler.

counts_list = results_sampler[0].data.meas.get_counts()
print(counts_list)
print(f"Outcomes : {counts_list}")
display(plot_histogram(counts_list, title="GHZ state"))
{'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}
Outcomes : {'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}

Code output

2.5 Skalowanie do dużej liczby qubitów

W obliczeniach kwantowych praca na skalę użytkową jest kluczowa dla postępu w tej dziedzinie. Tego rodzaju praca wymaga obliczeń prowadzonych na znacznie większą skalę – z Circuit, które mogą wykorzystywać ponad 100 qubitów i ponad 1000 Gate. Ten przykład stawia mały krok w tym kierunku, skalując problem GHZ do n=10n=10 qubitów. Korzysta z przepływu pracy wzorców Qiskit i kończy się pomiarem wartości oczekiwanej Z0Zi\langle Z_0 Z_i \rangle.

Krok 1. Odwzorowanie problemu

Napisz funkcję zwracającą QuantumCircuit, która przygotowuje nn-qubitowy stan GHZ (w istocie rozszerzony stan Bella), a następnie użyj tej funkcji do przygotowania 10-qubitowego stanu GHZ i zebrania obserwabli do zmierzenia.

def get_qc_for_n_qubit_GHZ_state(n: int) -> QuantumCircuit:

qc = QuantumCircuit(n)
qc.h(0)
for i in range(n-1):
qc.cx(i, i+1)
return qc
n = 10
qc_n_GHZ = get_qc_for_n_qubit_GHZ_state(n)
qc_n_GHZ.draw("mpl")

Diagram kwantowego Circuit

Następnie odwzoruj na interesujące nas operatory. W tym przykładzie używamy operatorów ZZ między qubitami, aby zbadać zachowanie w miarę jak się od siebie oddalają. Coraz bardziej niedokładne (zniekształcone) wartości oczekiwane między odległymi qubitami ujawniałyby poziom szumów.

# ZZII...II, ZIZI...II, ... , ZIII...IZ
operator_strings = [
"Z" + i * "I" + "Z" + "I" * (n-i-2) for i in range(n-1)
]
print(operator_strings)
print(len(operator_strings))

operators = [SparsePauliOp(operator) for operator in operator_strings]
['ZZIIIIIIII', 'ZIZIIIIIII', 'ZIIZIIIIII', 'ZIIIZIIIII', 'ZIIIIZIIII', 'ZIIIIIZIII', 'ZIIIIIIZII', 'ZIIIIIIIZI', 'ZIIIIIIIIZ']
9

Krok 2. Optymalizacja problemu do wykonania na kwantowym Backend

Przekształć Circuit i obserwable, aby dopasować je do ISA Backendu.

# Convert to an ISA circuit and layout-mapped observables.
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc_n_GHZ)
isa_operators_list = [operator.apply_layout(isa_circuit.layout) for operator in operators]

Krok 3. Wykonanie na Backend

Wyślij zadanie, a jeśli wykonujesz je na sprzęcie, włącz tłumienie błędów, stosując technikę redukcji błędów zwaną dynamicznym odsprzęganiem (dynamical decoupling). Poziom odporności określa, jak duże zabezpieczenie przed błędami ma zostać zbudowane. Wyższe poziomy generują dokładniejsze wyniki kosztem dłuższego czasu przetwarzania. Szczegółowe wyjaśnienie opcji ustawianych w poniższym kodzie znajdziesz w artykule Configure error mitigation for Qiskit Runtime.

# Submit the circuit to Estimator
job = estimator.run([(isa_circuit, isa_operators_list)])
job_id = job.job_id()
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")

Krok 4. Przetwarzanie końcowe wyników

Aby lepiej zrozumieć zachowanie splątanych stanów kwantowych na rzeczywistym sprzęcie, analizujemy parowe korelacje między qubitami w bazie Z. Konkretnie przyglądamy się wartościom oczekiwanym ⟨Z₀Zᵢ⟩, które mierzą, jak silnie qubit 0 jest skorelowany z każdym innym qubitem i. W szczególności zamierzamy wykreślić:

ZiZ0/Z1Z0\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle

Jakich wartości ZiZ0/Z1Z0\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle spodziewasz się zobaczyć na wykresie?

Opcje:

a) Malejące wraz ze wzrostem ii

b) Stałe równe 1

c) Małe odchylenia wokół 1

d) Naprzemiennie 1 i 0 dla nieparzystych i parzystych wartości ii

data = list(range(1, len(operators) + 1))  # Distance between the Z operators
result = job.result()[0]
values = result.data.evs # Expectation value at each Z operator.
values = [
v / values[0] for v in values
] # Normalize the expectation values to evaluate how they decay with distance.

plt.plot(data, values, marker="o", label=f"{n}-qubit GHZ state")
plt.xlabel("Distance between qubits $i$")
plt.ylabel(r"$\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle $")
plt.legend()
plt.show()

Wynik wykresu

Na tym wykresie zauważamy, że Z0Zi\langle Z_0 Z_i \rangle waha się wokół wartości 1, choć w idealnej symulacji wszystkie Z0Zi\langle Z_0 Z_i \rangle powinny wynosić 1.

Jak widać, wyniki eksperymentów z 10 qubitami są dobre, ale nadal zawierają pewne błędy. Jednym ze sposobów na poprawę wyników jest bardziej efektywna implementacja stanu GHZ.

Zazwyczaj stan GHZ implementuje się za pomocą sekwencji bramek CNOT ułożonych schodkowo. Możesz jednak zaimplementować stan GHZ wydajniej, redukując głębokość 2-qubitową z n do n/2 lub mniej.

Jednym z ważnych wskaźników pozwalających ocenić, jak dokładne będą wyniki lub jak mały szum będzie miał Circuit, jest głębokość bramek 2-qubitowych. Wynika to z faktu, że współczynniki błędów bramek 2-qubitowych (~10 razy wyższe niż w przypadku bramek jednoQubitowych) dominują nad błędami całego Circuit. Użyj poniższego kodu, aby uzyskać głębokość 2-qubitowych Gate w Circuit.

qc.depth(lambda x: x.operation.num_qubits == 2)
def better_ghz(n):
"fan out"
s = int(n / 2)
qc = QuantumCircuit(n)
qc.h(s)
for m in range(s, 0, -1):
qc.cx(m, m - 1)
if not (n % 2 == 0 and m == s):
qc.cx(n - m - 1, n - m)
return qc

better_ghz(n).draw("mpl")

Diagram kwantowego Circuit

# Check 2-qubit gate depth before transpilation
qc_better_ghz = better_ghz(n)
qc_better_ghz.depth(lambda x: x.operation.num_qubits == 2)
5

Warto tu zauważyć, że udało nam się zredukować głębokość kwantową Circuit, który chcemy wykonać, po prostu dzięki pomysłowości i zaprogramowaniu go w inny sposób. Będą jednak sytuacje i algorytmy, w których nie będziemy mogli polegać na takich sprytnych sztuczkach. Właśnie tutaj z pomocą przychodzi Transpiler – efektywnie optymalizuje wszystkie te aspekty, dzięki czemu nie musimy się nimi zbytnio przejmować.

3. Kodowanie informacji

3.1 Kodowanie amplitudowe

Teraz, gdy wiemy już, jak budować obwody kwantowe, warto zbadać, w jaki sposób można zakodować informacje klasyczne w stanach kwantowych. Jedną z potężnych metod jest kodowanie amplitudowe, w którym amplitudy stanu kwantowego reprezentują składowe klasycznego wektora.

Rozważmy prosty przykład. Załóżmy, że chcemy zakodować klasyczny wektor

x=[x0x1x2x3]\vec{x} = \begin{bmatrix} x_0 \\ x_1 \\ x_2 \\ x_3 \end{bmatrix}

w stanie kwantowym dwóch Qubitów. Celem jest przygotowanie stanu kwantowego:

ψ=x000+x101+x210+x311\ket{\psi} = x_0\ket{00} + x_1\ket{01} + x_2\ket{10} + x_3\ket{11}

gdzie x0,x1,x2,x3Rx_0, x_1, x_2, x_3 \in \mathbb{R} (lub C\mathbb{C}), a wektor jest znormalizowany tak, że:

x02+x12+x22+x32=1|x_0|^2 + |x_1|^2 + |x_2|^2 + |x_3|^2 = 1

Rozważmy teraz konkretny przykład: x=[0.8924,0.3696,0.2391,0.0990]\vec{x} = [0.8924, 0.3696, 0.2391, 0.0990]

Odpowiadający mu stan kwantowy to:

ψ=0.892400+0.369601+0.239110+0.099011\begin{aligned} \ket{\psi} &= 0.8924\,\ket{00} + 0.3696\,\ket{01} + 0.2391\,\ket{10} + 0.0990\,\ket{11} \end{aligned}

Ten stan można przygotować za pomocą kombinacji bramek rotacji RyR_y o kątach π/6\pi/6 i π/4\pi/4 odpowiednio dla Qubitów 0 i 1.

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
import numpy as np

qc = QuantumCircuit(2)

qc.ry(np.pi / 6, 0)
qc.ry(np.pi / 4, 1)

simulator = AerSimulator()
qc.save_statevector()
result = simulator.run(qc).result()
statevector = result.get_statevector()

print("Statevector:", statevector)
qc.draw(output="mpl")
Statevector: Statevector([0.8923991 +0.j, 0.23911762+0.j, 0.36964381+0.j,
0.09904576+0.j],
dims=(2, 2))

Quantum circuit diagram

from qiskit.quantum_info import Statevector

# Define our vector
v = np.array([0.8924, 0.3696, 0.2391, 0.0990])
v = v/np.linalg.norm(v)
# Create a statevector from the vector
state = Statevector(v)

# Initialize a quantum circuit with 2 qubits
qc = QuantumCircuit(2)
qc.initialize(state.data, [0, 1])

# Optional: simulate the state
print("Statevector:", state)

# Visualize the circuit
qc.decompose().decompose().decompose().decompose().decompose().draw("mpl")
Statevector: Statevector([0.89242154+0.j, 0.36960892+0.j, 0.23910577+0.j,
0.09900239+0.j],
dims=(2, 2))

Quantum circuit diagram

Widzimy więc, jak kodować informacje za pomocą bramek rotacji.

3.2 Kodowanie kątowe i obwody parametryczne

Szczególnie interesującym sposobem kodowania informacji w komputerze kwantowym jest projektowanie obwodów kwantowych zawierających pewne kąty rotacji θ\vec{\theta} lub parametry, które można dostrajać w celu reprezentowania rodziny funkcji f(θ)f(\vec{\theta}). Rozważmy na przykład następujący parametryzowany Circuit kwantowy:

from qiskit import QuantumCircuit
from qiskit.circuit import Parameter

# Define a symbolic parameter
theta = Parameter("θ")

qc = QuantumCircuit(2)
# We applied a parametrized RX gate
qc.rx(theta, 0)
qc.cx(0, 1)
qc.draw("mpl")

Quantum circuit diagram

Matematycznie możemy przeanalizować, jaką rodzinę funkcji możemy reprezentować za pomocą tego obwodu:

CNOT01Rx{0}(θ)00=CNOT01(cos(θ/2)00isin(θ/2)10)=cos(θ/2)00isin(θ/2)11\text{CNOT}_{01} \, R_x^{\{0\}}(\theta) |00\rangle = \text{CNOT}_{01} \left( \cos(\theta/2)\ket{00} - i\sin(\theta/2)\ket{10} \right) = \cos(\theta/2)\ket{00} - i\sin(\theta/2)\ket{11}

Dość wyraźnie widać, że liczba stanów, jakie możemy reprezentować za pomocą tego Circuit kwantowego, jest ograniczona — nie możemy na przykład reprezentować stanów 10\ket{10} ani 01\ket{01}. Jednak rodzina stanów, które możemy reprezentować, zaczyna się poszerzać, gdy wprowadzamy więcej rotacji w odpowiednich miejscach:

from qiskit import QuantumCircuit
from qiskit.circuit import Parameter

# Define a symbolic parameter
theta1 = Parameter("θ1")
theta2 = Parameter("θ2")

qc = QuantumCircuit(2)
qc.rx(theta1, 0)
qc.rx(theta2, 1)
qc.cx(0, 1)
qc.draw("mpl")

Quantum circuit diagram

W tym przypadku stany kwantowe, które będziemy reprezentować, to:

\begin{align*} \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2) R_x^{\{0}}(\theta_1) \ket{00} &= \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2)\left( \cos(\theta_1/2)\ket{00} - i\sin(\theta_1/2)\ket{10} \right) \\ &= \text{CNOT}_{01}\left( \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \right. \\ &\quad \left. - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{10} + \sin(\theta_1/2)\sin(\theta_2/2)\ket{11} \right) \\ &= \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \\ &\quad + \sin(\theta_1/2)\sin(\theta_2/2)\ket{10} - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{11} \end{align*}

Widzimy, że ten Circuit generuje szerszą rodzinę stanów kwantowych w porównaniu z poprzednim. W szczególności może teraz tworzyć stany z niezerowymi amplitudami dla 01\ket{01} lub 10\ket{10}, które były niemożliwe przy poprzednim obwodzie. Niemniej jednak ten Circuit wciąż nie jest uniwersalnym generatorem stanów kwantowych, choć może być wystarczająco ekspresywny do projektowania obwodów z pewną elastycznością w reprezentowaniu określonych funkcji. Ogólnie rzecz biorąc, im więcej niezależnych parametrów (kątów) wprowadzamy, tym większą ekspresywność ma Circuit do aproksymowania dowolnych stanów kwantowych.

Ansatze i biblioteka Circuit

Ten rodzaj sparametryzowanego Circuit kwantowego można wykorzystać do budowania Ansatzów — próbnych stanów kwantowych mających przybliżać rozwiązanie danego problemu. Ansatze stanowią kluczowy element Wariacyjnych Algorytmów Kwantowych, klasy hybrydowych algorytmów kwantowo-klasycznych, w których komputer kwantowy służy do wyznaczania funkcji kosztu, a klasyczny optymalizator ją minimalizuje. Szczegółowo omówimy te zagadnienia w późniejszej jednostce, lecz na razie pokażemy, jak zbudować prosty Ansatz przy użyciu biblioteki Circuit w Qiskit.

from qiskit.circuit.library import efficient_su2

SU2_ansatz = efficient_su2(4, su2_gates=["rx", "y"], entanglement="linear", reps=1)
SU2_ansatz.decompose().draw(output="mpl")

Diagram Circuit kwantowego

Zobaczyliśmy, jak zbudować prosty Ansatz przy użyciu funkcji efficient_su2 z modułu qiskit.circuit.library, który jest w stanie wygenerować szeroki zakres stanów kwantowych poprzez strojenie swoich parametrów θ\vec{\theta}.

Podsumowanie

W tym notebooku nauczyłeś się, jak konstruować Circuit kwantowe — od budowania Gate'ów kwantowych, przez definiowanie i pomiar obserwowalnych, aż po efektywne wykonywanie tych Circuit na symulatorach i rzeczywistym sprzęcie kwantowym. Zobaczyłeś również, jak ważne jest staranne projektowanie Circuit, aby zminimalizować błędy podczas pracy z prawdziwymi urządzeniami kwantowymi, oraz poznałeś strategie skalowania Circuit na większą liczbę Qubitów — szczególnie na przykładzie stanu GHZ. Ponadto zgłębiłeś różne techniki kodowania informacji klasycznej w stanach kwantowych, w tym kodowanie amplitudowe i kodowanie kątowe. Masz teraz pełne przygotowanie, by przejść do kolejnej sesji i zacząć pracę z algorytmami kwantowymi.

Instalacja Qiskit Code Assistant w VSCode

Kliknij link i postępuj zgodnie z instrukcjami.

Bonus: Teleportacja Kwantowa

Gdy słyszysz wyrażenie „teleportacja kwantowa", możesz wyobrażać sobie futurystyczną technologię science-fiction, która rozkłada obiekt w jednym miejscu i materializuje go gdzieś daleko. Teleportacja kwantowa nie ma jednak z tym nic wspólnego. W rzeczywistości teleportowane nie jest materia — teleportowana jest informacja.

Teleportacja kwantowa umożliwia transfer stanu kwantowego Qubitu z jednej lokalizacji do drugiej. Choć transfer ten wydaje się natychmiastowy, nie narusza praw fizyki. Jak to możliwe? Przyjrzyjmy się temu bliżej!

Teleportacja kwantowa to protokół umożliwiający nadawcy (Alice) przesłanie stanu ψ|\psi\rangle Qubitu q do odbiorcy (Boba) przy użyciu dwóch kluczowych zasobów: współdzielonej splątanej pary Qubitów a i b oraz dwóch bitów klasycznej komunikacji c0 i c1.

W skrócie protokół wymaga:

  • q: Qubit Alice, początkowo w stanie ψ|\psi\rangle, który chcemy teleportować.
  • a: połowa splątanej pary należąca do Alice.
  • b: połowa splątanej pary należąca do Boba.
  • c0, c1: bity klasyczne do przechowania wyników pomiarów Alice.

Jak to działa? Przebieg jest następujący:

  1. Przygotowanie stanu ψ|\psi\rangle Alice na q. Stworzymy konkretny stan, np. +|+\rangle, na potrzeby weryfikacji.
  2. Tworzenie splątania: wygenerowanie pary Bella między a i b.
  3. Operacje Alice: Alice wykonuje „pomiar Bella" na swoich dwóch Qubitach (q i a) i zapisuje klasyczne wyniki w c0 oraz c1.
  4. Komunikacja klasyczna: Alice przesyła Bobowi swoje dwa bity klasyczne (c0, c1).
  5. Korekcje Boba: Bob stosuje odpowiednie Gate'y kwantowe (X i/lub Z) do swojego Qubitu (b), uwarunkowane wartościami otrzymanych c0 i c1.

Jeśli wszystko zostanie wykonane poprawnie, Qubit Boba b znajdzie się w stanie ψ|\psi\rangle — oryginalnym stanie Qubitu q Alice!

Bardziej szczegółowe wyjaśnienie i eksplorację teleportacji kwantowej, w tym matematyczne uzasadnienie działania tego protokołu, znajdziesz w zasobach IBM Quantum Learning: Quantum Teleportation. Jest to część kursu Basics of Quantum Information.


import matplotlib.pyplot as plt
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram, plot_bloch_multivector

# Define individual quantum registers for each qubit
q = QuantumRegister(1, name='q') # message qubit
a = QuantumRegister(1, name='a') # Alice's entangled qubit
b = QuantumRegister(1, name='b') # Bob's entangled qubit

# Classical register for Alice's measurements
cr_alice = ClassicalRegister(2, name='c_alice')

# Create quantum circuit
teleport_qc = QuantumCircuit(q, a, b, cr_alice, name='Teleportation')

# Step 1: Prepare message state |+⟩ on q
teleport_qc.h(q[0])
teleport_qc.barrier()

# Step 2: Create entanglement between a and b
teleport_qc.h(a[0])
teleport_qc.cx(a[0], b[0])
teleport_qc.barrier()

# Step 3: Alice's Bell measurement
teleport_qc.cx(q[0], a[0])
teleport_qc.h(q[0])
teleport_qc.barrier()

# Step 4: Alice measures q and a
teleport_qc.measure(q[0], cr_alice[0])
teleport_qc.measure(a[0], cr_alice[1])
teleport_qc.barrier()

# Step 5: Bob's conditional measurements
with teleport_qc.if_test((cr_alice[1], 1)):
teleport_qc.x(b[0])
with teleport_qc.if_test((cr_alice[0], 1)):
teleport_qc.z(b[0])

# Draw the circuit
teleport_qc.draw(output='mpl')

Diagram Circuit kwantowego

Po wykonaniu protokołu pojawia się kluczowe pytanie: jak zweryfikować, że teleportacja zadziałała? Nie możemy bezpośrednio „zobaczyć" stanu Qubitu Boba po zakończeniu protokołu. Ponieważ jednak sami przygotowaliśmy początkowy stan ψ|\psi\rangle Alice (wybraliśmy +|+\rangle), możemy skorzystać ze specjalnego rodzaju symulacji, aby sprawdzić, czy Qubit Boba b znalazł się w tym samym stanie.

Użyjemy AerSimulator z save_statevector, aby sprawdzić, czy Qubit Boba b kończy w oryginalnym stanie Alice (+|+\rangle). Ten symulator oblicza końcowy wektor stanu kwantowego, a następnie przedstawia go za pomocą plot_bloch_multivector, wizualizując Qubit Boba (b) w porównaniu z początkowym stanem Alice (q).

# Simulate the teleportation circuit
sv_simulator = AerSimulator(method='statevector')
teleport_qc_sv = teleport_qc.copy()
teleport_qc_sv.save_statevector()

# Execute the circuit on the statevector simulator
job_sv = sv_simulator.run(teleport_qc_sv)
result_sv = job_sv.result()

# Get the final statevector
final_statevector = result_sv.get_statevector()
print("Visualizing final qubit states:")
display(plot_bloch_multivector(final_statevector))
print("Note that Alice's qubits have collapsed to |00⟩, |01⟩, |10⟩, or |11⟩, while Bob's qubit is in the original state |+⟩.")
Visualizing final qubit states:

Diagram Circuit kwantowego

Note that Alice's qubits have collapsed to |00⟩, |01⟩, |10⟩, or |11⟩, while Bob's qubit is in the original state |+⟩.

Jak widać z wizualizacji, dwa pierwsze Qubity (należące do Alice) zakolapsowały do wartości 0 lub 1. Tymczasem trzeci Qubit (należący do Boba), przedstawiony na trzeciej sferze Blocha, wskazuje wzdłuż osi x, co oznacza, że jest w stanie +|+\rangle — pomyślnie zaimplementowaliśmy zatem protokół teleportacji kwantowej!

Podsumowanie

W tym miejscu warto pokrótce zebrać to, co udało nam się osiągnąć:

  • Alice przesłała Bobowi nieznany stan kwantowy.
  • Żadna fizyczna cząstka nie została przetransferowana.
  • Oryginalny stan Qubitu Alice uległ zniszczeniu, zgodnie z twierdzeniem o zakazie klonowania.

Teleportacja kwantowa wymaga jednak nadal komunikacji klasycznej (wyniki pomiarów Alice wysyłane do Boba), co wyjaśnia, dlaczego proces ten nie umożliwia transferu informacji z prędkością nadświetlną i jest w pełni zgodny ze wszystkimi poznanymi prawami fizyki.