Lewati ke konten utama
Proyek ini masih dalam tahap beta. Menemukan bug? Buat isu GitHub. Punya pertanyaan atau ide? Tanya di Discussions — kami sangat menghargai masukan kamu!
Bagaimana kualitas terjemahan di halaman ini?

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,

[bi,bj†]=δij,[bi,bj]=[bi†,bj†]=0 .[b_i, b_j^\dagger] = \delta_{ij}, \qquad [b_i, b_j] = [b_i^\dagger, b_j^\dagger] = 0 \, .

Dua konsekuensi membentuk antarmukanya. Indeks mode bisa berulang dalam satu suku, karena bi†bi†b_i^\dagger b_i^\dagger 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, bibi†=bi†bi+1b_i b_i^\dagger = b_i^\dagger b_i + 1.

Penyimpanan​

Sebuah operator menyimpan suku-sukunya dalam empat array datar, mirip dengan format compressed sparse row pada matriks jarang:

coeffsSatu koefisien kompleks per suku.
actionsSatu boolean per faktor: True untuk operator penciptaan dan False untuk pemusnahan.
modesSatu indeks mode per faktor.
boundariesPosisi di actions dan modes tempat setiap suku dimulai dan berakhir.

Konstruktor menerima keempat array ini secara langsung. Operator di bawah punya tiga suku, yaitu 2b0†b12 b_0^\dagger b_1, −ib1†b1†-i b_1^\dagger b_1^\dagger, dan konstanta 0.50.5, 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.

Catatan

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 b0†b_0^\dagger dan -0 mewakili b0b_0. 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,

q^=b†+b2,p^=i(b†−b)2 ,\hat{q} = \frac{b^\dagger + b}{\sqrt{2}}, \qquad \hat{p} = \frac{i \left(b^\dagger - b\right)}{\sqrt{2}} \, ,

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 ABAB. Operator & adalah komposisi, yang mengikuti konvensi Qiskit dengan membaca dari kiri ke kanan: a & b menerapkan a lebih dulu lalu b, jadi hasilnya BABA. Fungsi komutator memakai @, jadi commutator(a, b) adalah [A,B][A, B]. 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.

Catatan

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 b b†b\,b^\dagger dan b†b+1b^\dagger b + 1 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: b†b†b^\dagger b^\dagger 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 b b†+b†bb\,b^\dagger + b^\dagger b sebagai Hermitian tanpa usaha tambahan.

Catatan

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, bi†bib_i^\dagger b_i, 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: b0†b0†b0b0b_0^\dagger b_0^\dagger b_0 b_0 diagonal, sedangkan b0†b0†b0b_0^\dagger b_0^\dagger b_0 tidak.

Langkah berikutnya​