Simulasi klasik circuit dengan OBP
Di panduan ini, kamu akan belajar cara mensimulasikan instance QuantumCircuit secara klasik untuk memperkirakan nilai ekspektasi sepenuhnya lewat OBP.
Karena OBP mengambil sebuah observable dan melakukan backpropagation melewati circuit tertentu, "simulasi" sebuah circuit sama dengan menghitung nilai ekspektasi observable target terhadap circuit tersebut.
Seperti yang akan kamu lihat nanti, paket qiskit-addon-obp bahkan bisa menangani model noise sederhana, sehingga kamu bisa menghitung nilai ekspektasi yang noisy juga!
Membangun contoh circuit
Untuk keperluan panduan ini, kita akan memakai contoh circuit yang sama seperti di panduan pemangkasan suku Pauli:
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "rustworkx": "rustworkx"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import rustworkx.generators
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import (
PauliOrderStrategy,
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types
# we generate a linear chain of 10 qubits
num_qubits = 10
linear_chain = rustworkx.generators.path_graph(num_qubits)
# we use an arbitrary XY model
hamiltonian = generate_xyz_hamiltonian(
linear_chain,
coupling_constants=(0.05, 0.02, 0.0),
ext_magnetic_field=(0.02, 0.08, 0.0),
pauli_order_strategy=PauliOrderStrategy.InteractionThenColor,
)
# we evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=3), time=2.0
)
# slice the circuit by gate type
slices = slice_by_gate_types(circuit)
Tapi, yang di atas itu murni circuit yang menggambarkan evolusi waktu di bawah Hamiltonian pilihan. Kita juga butuh state awal sebagai titik mulai, yang terhadapnya kita menghitung nilai ekspektasi observable kita.
Tentu saja kita bisa memilih state all-zero (atau vakum) sebagai state awal, tapi untuk menunjukkan cara memasukkan state awal sendiri, di bawah ini kita pilih yang lain.
Salah satu caranya adalah menaruh state awal di depan circuit evolusi waktu di atas: circuit.compose(initial_state, front=True).
Tapi karena kita sudah mengiris circuit kita, lebih mudah memasukkan state awal sebagai slice pertama, yang kita lakukan di bawah.
Dengan cara ini, kita bisa dengan mudah mengganti slice pertama dengan state awal lain kalau nanti ingin menukarnya, tanpa harus menghitung ulang slice kita.
from qiskit.circuit import QuantumCircuit
initial_state = QuantumCircuit(num_qubits)
for i in range(0, num_qubits, 2):
initial_state.x(i)
slices.insert(0, initial_state)
# for visualization purposes only, we recombine the slices with barriers between them and draw the resulting circuit
combine_slices(slices, include_barriers=True).draw("mpl", fold=50, scale=0.6)

Mensimulasikan nilai ekspektasi tanpa noise
Sebagai observable target, kita pilih observable ZZ pada qubit di tengah:
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIZZIIII")
Sampai di sini, kita sudah siap mensimulasikan nilai ekspektasi secara klasik dengan OBP.
Caranya, kita cukup memberikan semua slice ke metode backpropagate, seperti ini:
from qiskit_addon_obp import backpropagate
vacuum_state_obs, _, metadata = backpropagate(obs, slices)
Sekarang kita sudah melakukan backpropagation pada observable target obs melewati seluruh circuit (termasuk initial_state yang kita taruh di slices[0]), menghasilkan SparsePauliOp baru yang nilai ekspektasinya kita dapatkan dengan memproyeksikannya ke state vakum (|00...00>).
Ini bisa dicapai dengan cara yang lugas, yaitu menjumlahkan koefisien semua suku Pauli yang didefinisikan dalam basis komputasional:
vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()
np.complex128(-0.8285688012239535+4.9487770271457865e-20j)
Sebagai sanity check (dan untuk membuktikan bahwa ini berhasil), kita bisa membandingkan hasil kita dengan Statevector milik Qiskit:
from qiskit.quantum_info import Statevector
Statevector(combine_slices(slices)).expectation_value(obs)
np.complex128(-0.8285687255430366+0j)
Beberapa catatan soal performa
Efisiensi komputasi dari pemanggilan backpropagate di atas sangat bergantung pada banyak hal, antara lain:
- struktur
circuit - metode pengirisan circuit
- observable target
- parameter pemangkasan
Karena metode backpropagate menyederhanakan observable setiap kali satu slice selesai diterapkan, jumlah gate dalam sebuah slice bisa sangat memengaruhi beban komputasi.
Strategi penyederhanaan operator yang paling agresif bisa dicapai dengan mengiris circuit kamu menjadi slice berisi gate tunggal.
Selain itu, kamu bisa memanfaatkan semua mekanisme pemangkasan yang tertanam di metode backpropagate.
Di atas kita tidak melakukannya, sehingga hasilnya adalah nilai ekspektasi yang tepat, tapi kamu bisa belajar caranya di panduan pemangkasan suku Pauli.
Mensimulasikan nilai ekspektasi yang noisy
Paket qiskit-addon-obp juga mendukung penanganan model noise dalam bentuk PauliLindbladError.
Ini sangat berguna kalau kamu sudah mengkarakterisasi model noise dari layer 2-qubit di circuit kamu, misalnya memakai NoiseLearner.
Di bagian ini, kamu akan melihat cara memakai objek LayerError yang dikembalikan oleh NoiseLearner untuk menghitung nilai ekspektasi yang noisy dengan OBP.
Mendapatkan model noise
Biasanya, kamu akan menjalankan NoiseLearner untuk mendapatkan model noise dari circuit spesifik kamu.
Untuk menghindari kerumitan (dan keacakan) di tutorial ini, kita tidak melakukannya, dan sebagai gantinya kita menulis langsung (hard-code) sebuah model noise untuk circuit kita di bawah.
Meski begitu, kita pastikan struktur data kita cocok dengan NoiseLearnerResult.
Dalam bentuknya saat ini (belum di-transpile), circuit kita berisi 4 layer unik gate 2-qubit:
slices[1]: berisi gateRxxyang bekerja pada semua pasangan qubit ganjilslices[2]: berisi gateRxxyang bekerja pada semua pasangan qubit genapslices[3]: berisi gateRyyyang bekerja pada semua pasangan qubit ganjilslices[4]: berisi gateRyyyang bekerja pada semua pasangan qubit genap
Di cell di bawah, kita membuat secara manual 4 instance LayerError untuk masing-masing layer ini dengan error rate acak.
import numpy as np
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.utils.noise_learner_result import (
LayerError,
PauliLindbladError,
)
# fmt: off
pauli_errors_even = ['IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIXX', 'IIIIIIIIXY', 'IIIIIIIIXZ', 'IIIIIIIIYI', 'IIIIIIIIYX', 'IIIIIIIIYY', 'IIIIIIIIYZ', 'IIIIIIIIZI', 'IIIIIIIIZX', 'IIIIIIIIZY', 'IIIIIIIIZZ', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII', 'XIIIIIIIII', 'XXIIIIIIII', 'XYIIIIIIII', 'XZIIIIIIII', 'YIIIIIIIII', 'YXIIIIIIII', 'YYIIIIIIII', 'YZIIIIIIII', 'ZIIIIIIIII', 'ZXIIIIIIII', 'ZYIIIIIIII', 'ZZIIIIIIII']
pauli_errors_odd = ['IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII']
# fmt: on
np.random.seed(42)
layer_error_odd_xx = LayerError(
circuit=slices[1],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)
layer_error_even_xx = LayerError(
circuit=slices[2],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)
layer_error_odd_yy = LayerError(
circuit=slices[3],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)
layer_error_even_yy = LayerError(
circuit=slices[4],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)
Kalau kamu memakai NoiseLearner untuk mengidentifikasi layer gate 2-qubit yang unik dari circuit kamu dan mengkarakterisasi noise-nya, kamu akan mendapatkan objek NoiseLearnerResult.
Hasil ini akan berisi daftar objek LayerError, seperti yang sudah kita buat secara manual di atas.
Untuk setiap layer 2-qubit yang unik, LayerError berisi:
QuantumCircuityang merepresentasikan layer gate 2-qubit tersebut- indeks qubit tempat circuit ini bekerja
PauliLindbladErroryang merepresentasikan model noise hasil karakterisasi dari layer ini
PauliLindbladError akan berisi dua objek:
- daftar error Pauli yang sudah dikarakterisasi
- error rate yang sesuai dengan masing-masing error Pauli tersebut
Biasanya, daftar error Pauli bersifat sparse. Lebih spesifik, daftar ini berisi error Pauli satu-qubit pada semua qubit yang dikenai gate, serta error Pauli dua-qubit pada semua qubit yang saling terhubung.
Menyisipkan layer noisy ke circuit kita
Di bagian sebelumnya, kita secara khusus membuat satu LayerError untuk setiap layer gate 2-qubit unik yang kita ketahui.
Artinya, kita tahu persis LayerError mana yang cocok dengan salah satu slice kita.
Biasanya, saat memakai LayerError, kamu perlu mencari tahu apa layer gate 2-qubit yang unik itu, dan di mana letaknya di dalam circuit kamu.
Lalu kamu perlu menyesuaikan circuit dan/atau slices kamu untuk menyisipkan LayerError tersebut.
Cara melakukannya untuk kasus umum berada di luar cakupan panduan how-to ini.
TODO: link to external documentation, once it exists!
Di sini, urusannya lebih sederhana karena kita tahu slice mana yang sesuai dengan sebuah LayerError.
Jadi, tinggal menyisipkan slice baru untuk merepresentasikan noise.
Perhatikan bahwa kita harus membungkus setiap PauliLindbladError dari LayerError.error dalam PauliLindbladErrorInstruction agar menjadi instruksi QuantumCircuit yang valid.
from qiskit_addon_obp.utils.noise import PauliLindbladErrorInstruction
noisy_slices = []
for slice_ in slices:
if slice_ == layer_error_even_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_even_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_yy.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_yy.error
),
slice_.qubits,
)
]
)
)
noisy_slices.append(slice_)
Kita bisa memeriksa hasil kerja kita dan menggambar circuit di bawah:
combine_slices(noisy_slices, include_barriers=True).draw(
"mpl", fold=100, scale=0.8
)
Mensimulasikan nilai ekspektasi yang noisy
Sampai di sini, mensimulasikan nilai ekspektasi secara klasik caranya persis sama seperti sebelumnya, hanya saja
vacuum_state_noisy_obs, _, metadata = backpropagate(obs, noisy_slices)
vacuum_state_noisy_obs.coeffs[
~vacuum_state_noisy_obs.paulis.x.any(axis=1)
].sum()
np.complex128(-0.7230801696448901+7.082755280463563e-19j)
Kami tegaskan lagi bahwa beberapa pertimbangan performa perlu diperhatikan. Silakan kembali ke bagian terkait di atas.
