Prinsip desain representasi operator
Panduan ini menjelaskan bagaimana sebuah BosonOperator disusun dan cara bekerja dengannya: cara membuatnya, cara memeriksa dan menggabungkan operator, serta cara membawa dua operator ke bentuk yang bisa dibandingkan. Mulai dari sini saat kamu menuliskan sebuah Hamiltonian.
Sebuah BosonOperator adalah jumlah eksak dari hasil kali operator penciptaan dan pemusnahan bosonik dengan koefisien kompleks. Ia tidak membawa cutoff okupansi. Sebuah mode bosonik bisa menampung boson dalam jumlah sembarang, sehingga ruang Fock-nya berdimensi tak hingga, dan pemotongan baru muncul ketika operator dipetakan ke qubit (lihat Memotong ruang Fock). Karena itu, semua yang ada di panduan ini terjadi dalam aljabar eksak dari relasi komutasi kanonik,
Dua konsekuensi membentuk antarmukanya. Indeks mode bisa berulang dalam satu suku, karena adalah operator yang valid yang menciptakan dua boson. Selain itu, menukar urutan dua faktor tidak pernah memunculkan tanda, walaupun memindahkan operator pemusnahan melewati operator penciptaan pada mode yang sama menambah satu suku, .
Penyimpanan
Sebuah operator menyimpan suku-sukunya dalam empat array datar, mirip dengan format compressed sparse row pada matriks jarang:
coeffs | Satu koefisien kompleks per suku. |
actions | Satu boolean per faktor: True untuk operator penciptaan dan False untuk pemusnahan. |
modes | Satu indeks mode per faktor. |
boundaries | Posisi di actions dan modes tempat setiap suku dimulai dan berakhir. |
Konstruktor menerima keempat array ini secara langsung. Operator di bawah punya tiga suku, yaitu , , dan konstanta , dan kamu bisa membaca kembali array-nya dengan method get_*:
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> op = BosonOperator(
... coeffs=[2.0, -1.0j, 0.5],
... actions=[True, False, True, True],
... modes=[0, 1, 1, 1],
... boundaries=[0, 2, 4, 4],
... )
>>> op.get_coeffs()
[(2+0j), (-0-1j), (0.5+0j)]
>>> op.get_actions()
[True, False, True, True]
>>> op.get_modes()
[0, 1, 1, 1]
>>> op.get_boundaries()
[0, 2, 4, 4]
>>> len(op)
3
Suku pertama mencakup posisi 0 sampai 2 dari actions dan modes, suku kedua mencakup 2 sampai 4, dan suku ketiga mencakup 4 sampai 4, begitulah suku konstanta, yaitu suku tanpa faktor, disimpan. Mode 1 yang berulang di suku kedua dipertahankan seperti yang ditulis; sebuah suku tidak pernah dibuang atau digabung hanya karena indeks mode muncul dua kali.
Operator yang dibuat dengan from_dict() tidak menyimpan suku-sukunya menurut urutan dictionary, sehingga array-nya bisa mencantumkan suku dalam urutan berbeda dari satu run ke run lain. Operatornya sendiri tetap sama, dan tidak ada hal di bawah ini yang membandingkan atau mencetak operator yang bergantung pada urutan penyimpanan.
Tata letak ini membuat konstruksi dan aritmetika murah, dengan satu konsekuensi: sebuah operator bisa memuat suku yang sama beberapa kali sekaligus. Penjumlahan dan perkalian menambahkan suku tanpa menggabungkannya, jadi panggil simplify() saat kamu butuh suku yang unik (lihat Melakukan aritmetika).
Membuat operator
Cara paling langsung untuk membuat operator adalah from_dict(), yang menerima dictionary dari tuple faktor ke koefisien. Fungsi cre() dan ann() membuat faktor-faktornya, dan tuple kosong adalah suku konstanta:
>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dag = BosonOperator.from_dict({(cre(0),): 1.0})
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> print(format(number))
1.000000e0 +0.000000e0j * (+0 -0)
Pada bentuk cetak, +0 mewakili dan -0 mewakili . Konstruktor zero() dan one() mengembalikan identitas aditif dan multiplikatif, dan from_terms() membangun ulang operator dari iterator iter_terms().
Library operator menyediakan kuadratur posisi dan momentum tanpa dimensi,
sebagai position() dan momentum(). Bersama fungsi komutator dari modul yang sama, kamu bisa memeriksa aljabarnya secara langsung:
>>> from qiskit_bosons.operators.library import commutator, momentum, position
>>> q, p = position(0), momentum(0)
>>> q.is_hermitian() and p.is_hermitian()
True
>>> commutator(q, p).equiv(1j * BosonOperator.one())
True
>>> commutator(b, b_dag).equiv(BosonOperator.one())
True
Modul ini juga menyediakan anti_commutator() dan double_commutator(). Ketiganya menerima operator bertipe sama dan mengembalikan operator baru.
Perhatikan urutan sebuah hasil kali
Dua operator bisa dikalikan dengan dua cara, dan keduanya mudah tertukar. Operator @ adalah hasil kali matematis, jadi a @ b adalah . Operator & adalah komposisi, yang mengikuti konvensi Qiskit dengan membaca dari kiri ke kanan: a & b menerapkan a lebih dulu lalu b, jadi hasilnya . Fungsi komutator memakai @, jadi commutator(a, b) adalah . Kalau kamu membangun sendiri braket itu dengan &, tandanya diam-diam terbalik:
>>> (b @ b_dag).equiv(b_dag @ b + BosonOperator.one())
True
>>> (b & b_dag).equiv(b_dag @ b)
True
>>> ((b @ b_dag) - (b_dag @ b)).equiv(BosonOperator.one())
True
>>> ((b & b_dag) - (b_dag & b)).equiv(-BosonOperator.one())
True
Memeriksa operator
Operator tidak bisa diiterasi secara langsung, karena suku-sukunya berada di array datar di atas. Gunakan iter_terms() sebagai gantinya, yang menghasilkan daftar pasangan (is_creation, mode) dan sebuah koefisien untuk setiap suku. Iterator mengembalikan salinan, jadi mengubahnya tidak mengubah operator.
>>> hopping = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(2), cre(2), ann(2), ann(2)): 0.5,
... }
... )
>>> len(hopping)
3
>>> sorted(hopping.iter_terms())
[([(True, 0), (False, 1)], (-1+0j)), ([(True, 1), (False, 0)], (-1+0j)), ([(True, 2), (True, 2), (False, 2), (False, 2)], (0.5+0j))]
Beberapa method merangkum struktur sebuah operator:
>>> hopping.get_support()
{0, 1, 2}
>>> hopping.min_num_modes()
3
>>> hopping.max_rank()
4
get_support() adalah himpunan mode yang dikenai operator, min_num_modes() adalah register terkecil yang bisa memuatnya, dan max_rank() adalah panjang suku terpanjangnya.
Sebuah mode adalah indeks abstrak yang melabeli satu derajat kebebasan bosonik. Indeks ini tidak punya makna sendiri: kamu yang menentukan situs lattice mana, koordinat vibrasi mana, atau port optik mana yang diwakili sebuah mode. BosonicCircuit memakai indeks yang sama (lihat Bekerja dengan bosonic circuit), sehingga operator dan gate yang membawanya sepakat tentang apa itu mode 2 tanpa salah satunya tahu apa yang diwakilinya. Tidak ada di dalam operator yang menetapkan berapa level yang dimiliki sebuah mode, dan itu juga sebabnya ukuran register hanyalah batas bawah; min_num_modes() adalah register terkecil yang memuat operator, bukan properti operator itu.
Method okupansi melaporkan berapa banyak boson yang bisa ditempatkan satu suku di satu mode. Ini adalah batas bawah untuk cutoff yang dibutuhkan sebuah pemetaan. Ini bukan cutoff yang cukup, karena penerapan operator berulang kali, seperti pada evolusi waktu, bisa mengisi level yang lebih tinggi (lihat Memotong ruang Fock):
>>> hopping.mode_occupations()
[1, 1, 2]
>>> hopping.mode_occupations([2, 0])
[2, 1]
>>> hopping.max_occupation()
2
Dua pemeriksaan lain melihat besaran yang kekal. Sebuah operator mengekalkan jumlah partikel jika setiap suku punya operator penciptaan sebanyak operator pemusnahan. Memberikan daftar ukuran blok menguji kondisi yang sama di dalam setiap blok mode yang berurutan:
>>> hopping.conserves_particle_number()
True
>>> move = BosonOperator.from_dict({(cre(0), ann(2)): 1.0})
>>> move.conserves_sector([3])
True
>>> move.conserves_sector([2, 1])
False
Terakhir, relabel_modes() mengganti nama mode: mode i menjadi permutation[i]. Daftarnya harus cukup panjang untuk mengindeks setiap mode operator dan tidak boleh mengulang entri:
>>> print(format(move.relabel_modes([5, 6, 7])))
1.000000e0 +0.000000e0j * (+5 -7)
Melakukan aritmetika
Operator mendukung penjumlahan, pengurangan, negasi, serta perkalian dan pembagian dengan sebuah angka, dalam bentuk operator biasa atau in place:
>>> total = 2 * b_dag + b_dag - 3j * b
>>> len(total)
3
>>> len(total.simplify())
2
>>> total.simplify().equiv(3 * b_dag - 3j * b)
True
Ketiga suku total tidak pernah digabung, jadi operator memuat b_dag dua kali sampai simplify() menggabungkannya. Method ini menjumlahkan koefisien suku-suku yang identik dan membuang suku yang total magnitudonya di bawah toleransi. Ini adalah operasi struktural: ia membandingkan faktor-faktor seperti yang tertulis dan tidak menerapkan relasi komutasi. Method terkait ichop() menghapus koefisien kecil in place tanpa menggabungkan dulu, jadi panggil simplify() sebelum ichop() ketika operator bisa memuat suku duplikat.
Hasil kali memakai @ atau & seperti dijelaskan di atas, dan ** memangkatkan operator dengan bilangan bulat:
>>> x = b_dag + b
>>> print(format((x ** 2).simplify()))
1.000000e0 +0.000000e0j * (-0 -0)
1.000000e0 +0.000000e0j * (-0 +0)
1.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0)
Hasilnya mencantumkan keempat urutan seperti yang tertulis. Menyederhanakannya ke satu bentuk membutuhkan relasi komutasi, yang diterapkan di bagian berikutnya.
Adjoint membalik urutan faktor di setiap suku, menukar operator penciptaan dan pemusnahan, dan mengonjugasi koefisien. Method is_hermitian() menguji apakah sebuah operator sama dengan adjoint-nya:
>>> hop = BosonOperator.from_dict({(cre(0), ann(1)): 1j})
>>> print(format(hop.adjoint()))
0.000000e0 -1.000000e0j * (+1 -0)
>>> hop.is_hermitian()
False
>>> (hop + hop.adjoint()).is_hermitian()
True
Satu suku hopping saja tidak Hermitian, dan ini penting saat kamu mengelompokkan suku untuk evolusi waktu: setiap kelompok harus Hermitian dengan sendirinya (lihat Mengelompokkan suku operator: memanfaatkan struktur operator).
Urutan normal dan membandingkan operator
Dua hasil kali operator tangga bisa merupakan operator yang sama namun terlihat berbeda, karena dan sama di dalam aljabar. Operator yang sama dalam arti ini belum tentu disimpan dengan cara yang sama, jadi membandingkannya membutuhkan relasi komutasi. Method normal_ordered() menerapkannya: ia memindahkan setiap operator penciptaan ke kiri semua operator pemusnahan dan menambah satu suku setiap kali sepasang operator pada mode yang sama ditukar.
>>> product = BosonOperator.from_dict({(ann(0), cre(0), ann(0), cre(0)): 1.0})
>>> ordered = product.normal_ordered()
>>> len(ordered)
5
>>> print(format(ordered.simplify()))
1.000000e0 +0.000000e0j * ()
3.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0 -0 -0)
Operator yang sudah diurutkan dikembalikan sebagai jumlah yang belum digabung, dengan lima suku tersimpan yang berkurang menjadi tiga setelah simplify() menggabungkannya. Pengurutan normal tidak pernah membuat suku lenyap: dan setiap faktor berulang lainnya tetap ada.
Argumen sandwich menentukan bagaimana mode di dalam kedua kelompok faktor disusun. Secara bawaan kedua kelompok diurutkan menurun. Dengan sandwich=True, operator penciptaan menaik dan operator pemusnahan menurun, sehingga indeks yang lebih besar berada di tengah:
>>> pair = BosonOperator.from_dict({(cre(0), cre(1), ann(0), ann(1)): 1.0})
>>> list(pair.normal_ordered().iter_terms())
[([(True, 1), (True, 0), (False, 1), (False, 0)], (1+0j))]
>>> list(pair.normal_ordered(sandwich=True).iter_terms())
[([(True, 0), (True, 1), (False, 1), (False, 0)], (1+0j))]
Operator == menguji kesamaan eksak dari suku-suku yang tersimpan, tanpa toleransi dan tanpa penyederhanaan, jadi ia bisa melaporkan perbedaan antara dua operator yang secara matematis sama. Method equiv() lebih dulu membawa selisih kedua operator ke urutan normal, lalu menguji bahwa setiap koefisien berada di bawah toleransi. Inilah perbandingan yang hampir selalu kamu inginkan:
>>> reordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0, (): 1.0})
>>> contracted = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> contracted == reordered
False
>>> contracted.simplify() == reordered.simplify()
False
>>> contracted.equiv(reordered)
True
Method simplify saja membiarkan kedua bentuk itu tetap terpisah, karena ia hanya menggabungkan suku yang tertulis identik. Demikian pula, is_hermitian() membandingkan operator dengan adjoint-nya dalam urutan normal, sehingga ia mengenali sebagai Hermitian tanpa usaha tambahan.
Pengurutan normal adalah identitas eksak dalam aljabar yang tidak dipotong, dan equiv bergantung pada hal itu. Ini bukan identitas setelah pemotongan. Operator yang dipetakan bergantung pada bentuk yang kamu petakan, jadi citra terpetakan dari contracted dan reordered di atas berbeda, hanya pada level tertinggi yang dipertahankan. Lihat Memotong ruang Fock untuk besar perbedaannya dan cara memilih bentuk.
Mengurutkan dan memfilter suku
Mengurutkan ulang suku-suku sebuah operator, berbeda dari faktor di dalam satu suku, tidak mengubah operatornya tetapi bisa mengubah perilaku algoritma yang membaca suku secara berurutan, seperti product formula teracak. Fungsi-fungsi di qiskit_bosons.operators.terms mengembalikan operator baru dan mempertahankan indeks kelompok yang melekat pada suku-sukunya.
canonical_order() mengurutkan suku ke dalam urutan tetap yang hanya bergantung pada faktor yang tertulis, sehingga operator terbaca sama bagaimanapun ia dirakit. order_terms() mengurutkan menurut kunci yang kamu berikan, yang menerima setiap suku sebagai tuple yang dihasilkan iter_terms():
>>> from qiskit_bosons.operators.terms import canonical_order, order_terms
>>> mixed = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> list(canonical_order(mixed).iter_terms())
[([], (2+0j)), ([(True, 0), (False, 1)], (3+0j)), ([(True, 1), (False, 0)], (1+0j))]
>>> by_weight = order_terms(mixed, key=lambda term: abs(term[1]), reverse=True)
>>> [coeff for _, coeff in by_weight.iter_terms()]
[(3+0j), (2+0j), (1+0j)]
Jika sebuah operator membawa indeks kelompok (lihat Mengelompokkan suku operator: memanfaatkan struktur operator), fungsi group_order() membuat setiap kelompok menjadi satu rangkaian suku yang berurutan:
>>> from qiskit_bosons.operators.terms import group_order
>>> grouped = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> grouped.groups = [1, 0, 1]
>>> group_order(grouped).groups
[0, 1, 1]
Helper terakhir menghapus suku, bukan mengurutkannya ulang. Suku yang merupakan hasil kali operator bilangan, , hanya mengalikan setiap state Fock dengan fase, jadi filter_diagonal_terms() bisa membuang konstanta, operator bilangan, dan hasil kalinya ketika fase itu tidak relevan untuk apa yang kamu hitung. Fungsi ini mengubah operator in place dan membutuhkan operator yang sudah berurutan normal, yang tidak diperiksanya:
>>> from qiskit_bosons.operators.terms import filter_diagonal_terms
>>> model = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(0), cre(0), ann(0), ann(0)): 0.5,
... (cre(0), ann(0)): 0.2,
... (): 0.1,
... }
... )
>>> filter_diagonal_terms(model)
>>> len(model)
2
>>> model.is_hermitian()
True
Hanya dua suku hopping yang tersisa. Interaksi on-site dan operator bilangan bersifat diagonal pada basis okupansi, jadi keduanya dihapus bersama konstanta. Mode berulang pada interaksi ditangani dengan benar: diagonal, sedangkan tidak.
Langkah berikutnya
-
Mengelompokkan suku operator: memanfaatkan struktur operator menunjukkan cara memberi tag pada suku dengan indeks kelompok.
-
Petakan representasi operator mengubah operator menjadi operator qubit, termasuk pilihan encoding dan efek pemotongan.
-
Bekerja dengan bosonic circuit menempatkan operator ke dalam circuit sebagai gate
Evolution. -
Simulasikan dinamika Bose-Hubbard dengan pilihan encoding membangun Hamiltonian rantai Bose-Hubbard dan mensimulasikannya dari awal sampai akhir.
-
Modul
qiskit_bosons.operatorsdanqiskit_bosons.operators.termsmendokumentasikan antarmuka secara lengkap.