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?

Memetakan representasi operator

Sebuah mapper mengubah BosonOperator menjadi operator qubit, di sini berupa SparseObservable. Operatornya sendiri tidak tahu apa-apa soal qubit: ia menyimpan operator ladder pada mode, dan sebuah mode bosonik bisa menampung partikel dalam jumlah berapa pun. Semua yang membuat operator itu berhingga (berapa banyak level okupansi yang kamu pertahankan, dan bagaimana level-level itu ditata di qubit) ditentukan saat kamu memetakannya.

Mapper pustaka occupation_encoding() bekerja di basis okupansi (Fock): level nn dari sebuah mode adalah state dengan nn boson. Ia menerima operator dan sebuah LevelSpec, yang menyimpan pemotongan (truncation) dan encoding. Panduan ini menjelaskan apa arti kedua pilihan itu dan berapa biaya masing-masing. Untuk alur end-to-end yang memakainya, lihat Simulasikan dinamika Bose-Hubbard dengan pilihan encoding; untuk cara Transpiler menerapkan pilihan yang sama pada seluruh Circuit, lihat Transpile Circuit bosonik.

Potong ruang Fock​

Sebuah mode yang dipertahankan sampai d=nmax+1d = n_\text{max} + 1 level, dengan nmaxn_\text{max} adalah max_occupation-nya, tidak lagi memenuhi relasi komutasi kanonik. Dengan menulis bˉ\bar{b} untuk operator annihilation yang dipotong ke level-level itu,

[bˉ,bˉ†]=1−d ∣d−1⟩⟨d−1∣ ,[\bar{b}, \bar{b}^\dagger] = 1 - d \, \lvert d-1 \rangle\langle d-1 \rvert \, ,

jadi relasi tersebut berlaku di setiap level kecuali yang tertinggi [1]. Kamu bisa melihatnya dengan memetakan komutator kedua operator ladder dan membaca diagonalnya pada level-level satu mode. Helper berikut membangun matriks padat dari operator yang dipetakan untuk satu mode, yang hanya masuk akal untuk register yang sangat kecil:

>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp
>>> from qiskit_bosons.mappers import LevelEncoding, LevelSpec
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> def level_diagonal(operator, max_occupation, encoding):
... """The diagonal of a mapped one-mode operator on the codewords of the levels."""
... spec = LevelSpec(max_occupation, encoding)
... matrix = SparsePauliOp.from_sparse_observable(occupation_encoding(operator, spec)).to_matrix()
... codewords = [spec.encoding.level_to_bits(level) for level in range(max_occupation + 1)]
... return [round(float(matrix[c, c].real), 6) for c in codewords]
>>>
>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dagger = BosonOperator.from_dict({(cre(0),): 1.0})
>>> commutator = (b @ b_dagger) - (b_dagger @ b)
>>> level_diagonal(commutator, 2, "unary_unprojected") # d = 3
[1.0, 1.0, -2.0]

Entri terakhir adalah 1−d1 - d. Karena ini berarti rentang {bˉ†,bˉ}\{\bar{b}^\dagger, \bar{b}\} tidak tertutup di bawah komutator, tidak ada encoding yang merupakan isomorfisme aljabar. Sebuah encoding memetakan state, bersama operator yang bekerja di dalam subruang terpotong. Dua konsekuensi berikut penting dalam praktik.

Cutoff harus membatasi okupansi yang bisa dicapai. Tidak cukup hanya membatasi okupansi awal. Sebuah beamsplitter yang bekerja pada ∣1,1⟩\lvert 1, 1 \rangle mengisi ∣2,0⟩\lvert 2, 0 \rangle dan ∣0,2⟩\lvert 0, 2 \rangle, jadi cutoff satu diam-diam menghasilkan state yang salah:

>>> from scipy.linalg import expm
>>>
>>> beamsplitter = BosonOperator.from_dict({(cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0})
>>>
>>> def stays_in_11(max_occupation):
... """Probability to remain in |1, 1> after a 50:50 beamsplitter."""
... spec = LevelSpec(max_occupation, "gray").resolve(2)
... observable = occupation_encoding(beamsplitter, spec, spec.total_width)
... matrix = SparsePauliOp.from_sparse_observable(observable).to_matrix()
... unitary = expm(-1j * (np.pi / 4) * matrix)
... index = sum(spec.encoding.level_to_bits(1) << offset for offset in spec.offsets)
... return round(float(abs(unitary[index, index]) ** 2), 6)
>>>
>>> stays_in_11(1), stays_in_11(2)
(1.0, 0.0)

Dengan cutoff satu, probabilitas tetap di ∣1,1⟩\lvert 1, 1 \rangle adalah satu, karena dua state yang akan diisi oleh dinamika itu tidak ada. Dengan cutoff dua, probabilitasnya lenyap, yaitu efek Hong-Ou-Mandel. Inilah kenapa max_occupation adalah argumen wajib yang tidak pernah punya default: ini keputusan pemodelan sekaligus parameter konvergensi yang memang seharusnya kamu sapu (sweep).

Mapping tidak komut dengan normal ordering. Operator bb†b b^\dagger dan b†b+1b^\dagger b + 1 sama sebelum pemotongan, tapi keduanya dipetakan ke operator qubit yang berbeda:

>>> product = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> ordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0}) + BosonOperator.one()
>>> bool(product.equiv(ordered))
True
>>> level_diagonal(product, 3, "unary_unprojected") # d = 4
[1.0, 2.0, 3.0, 0.0]
>>> level_diagonal(ordered, 3, "unary_unprojected")
[1.0, 2.0, 3.0, 4.0]

Keduanya sama di setiap level kecuali yang tertinggi. Saat diterapkan pada level teratas, b†b^\dagger tidak punya tujuan dan amplitudonya terpotong, jadi bb†b b^\dagger bernilai nol di situ, sedangkan b†b+1b^\dagger b + 1 memberi dd. Putuskan sekali apakah Hamiltonian-mu didefinisikan dalam bentuk normal-ordered, lalu petakan bentuk itu secara konsisten.

Tiga metode membantu kamu memilih cutoff, dan semuanya adalah batas bawah. Operator melaporkan jumlah terbesar operator creation yang ia terapkan pada tiap mode dalam satu suku, dan sebuah spec membandingkannya dengan cutoff-nya:

>>> quartic = BosonOperator.from_dict({(cre(0), cre(0), ann(0), ann(0)): 1.0})
>>> quartic.mode_occupations(), quartic.max_occupation()
([2], 2)
>>> LevelSpec(1, "gray").truncation(quartic) # one level short
[1]
>>> LevelSpec(2, "gray").truncation(quartic)
[0]

Entri bukan nol berarti cutoff berada di bawah batas, dan nilainya adalah seberapa jauh. Helper ini tidak tahu soal dinamikanya, jadi pengecekan yang lolos tidak membuat sebuah cutoff cukup; beamsplitter di atas punya batas satu tapi butuh cutoff dua. Helper ini juga bisa melaporkan kekurangan untuk suku yang bernilai nol pada level yang dipertahankan, seperti b†b†bbb^\dagger b^\dagger b b untuk cutoff satu.

Deskripsikan pemotongan dengan LevelSpec​

Sebuah LevelSpec menggabungkan cutoff tiap mode dengan encoding, dan dari keduanya ia menurunkan berapa banyak qubit yang dipakai tiap mode dan di mana qubit-qubitnya dimulai. Semua yang harus sepakat soal itu (mapper, layout Transpiler, state preparation) diberi spec yang sama. Encoding adalah argumen wajib, begitu juga cutoff: keduanya tidak punya default.

Cutoff bisa berupa satu bilangan bulat yang dipakai bersama oleh semua mode, atau satu bilangan bulat per mode:

>>> shared = LevelSpec(2, "gray")
>>> per_mode = LevelSpec([4, 2, 2], "gray")
>>> shared.num_modes is None, per_mode.num_modes
(True, 3)

Spec yang dibangun dari satu bilangan bulat membiarkan jumlah mode terbuka, jadi kamu harus meresolusinya sebelum meminta lebar. Sebuah urutan tidak pernah di-broadcast, berapa pun panjangnya, dan ia menetapkan jumlah mode:

>>> per_mode.widths, per_mode.offsets, per_mode.total_width
([3, 2, 2], [0, 3, 5], 7)
>>> shared.resolve(3).widths
[2, 2, 2]
>>> try:
... shared.widths
... except ValueError as error:
... print(error)
this spec was built from a single cutoff, so its number of modes is open; call `resolve(num_modes)` first

Mode mm menempati qubit offsets[m] sampai offsets[m] + widths[m] - 1, jadi mode dengan cutoff berbeda bisa memakai jumlah qubit yang berbeda, dan offset adalah jumlah kumulatif dari lebar. Kamu tidak perlu memberikan jumlah mode ke mapper. Ia mengambilnya dari spec, atau, untuk spec dengan satu cutoff, dari operator lewat min_num_modes(). Register boleh lebih lebar dari yang dibutuhkan mode; qubit sisanya menganggur dan berada di ujung:

>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> number.min_num_modes()
1
>>> occupation_encoding(number, LevelSpec(1, "gray")).num_qubits
1
>>> occupation_encoding(number, LevelSpec(1, "gray"), 4).num_qubits
4

Operator yang bekerja pada mode di luar mode terakhir dari spec per-mode akan ditolak:

>>> far = BosonOperator.from_dict({(cre(5), ann(5)): 1.0})
>>> try:
... occupation_encoding(far, LevelSpec([1, 1], "gray"))
... except ValueError as error:
... print(error)
the operator acts on 6 modes, but the spec covers only 2

Beberapa batasan muncul dari cara encoding menghitung qubit. Di bawah encoding compact, mode dengan satu level (max_occupation = 0) tidak memakai qubit sama sekali, karena satu levelnya tidak butuh apa-apa untuk dibedakan dari yang lain. Mode seperti itu diperbolehkan dan tidak menggeser qubit mode-mode sesudahnya, tapi spec yang setiap modenya tidak memakai qubit akan ditolak. Encoding unary menampung paling banyak 128 level per mode, dan cutoff yang lebih besar memunculkan error yang menyarankan encoding compact:

>>> LevelSpec([0, 1], "gray").resolve(2).widths
[0, 1]
>>> try:
... LevelSpec(0, "gray")
... except ValueError as error:
... print(str(error).split(":")[0])
the spec would span no qubits
>>> try:
... LevelSpec(128, "unary_projected")
... except ValueError as error:
... print(str(error).split(" (")[0])
max_occupation = 128 is too large for the unary encodings, which hold at most 128 levels per mode

Pilih sebuah encoding​

Sebuah LevelEncoding menentukan basis state mana dari sebuah blok qubit yang mewakili tiap level. Ada empat, dan tidak ada satu pun yang terbaik secara umum [2]. Jumlah qubit per mode dan pola bit tiap level mengikuti dari encoding-nya:

>>> encodings = (
... LevelEncoding.UNARY_PROJECTED,
... LevelEncoding.UNARY_UNPROJECTED,
... LevelEncoding.STANDARD_BINARY,
... LevelEncoding.GRAY,
... )
>>> rows = ["encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns"]
>>> for encoding in encodings:
... widths = [encoding.num_qubits(levels) for levels in (1, 2, 3, 4, 5, 8)]
... patterns = [f"{encoding.level_to_bits(level):05b}" for level in range(5)]
... rows.append(f"{encoding!s:18s} {widths!s:34s} {patterns}")
>>> print("\n".join(rows))
encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns
unary_projected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
unary_unprojected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
standard_binary [0, 1, 2, 2, 3, 3] ['00000', '00001', '00010', '00011', '00100']
gray [0, 1, 2, 2, 3, 3] ['00000', '00001', '00011', '00010', '00110']

Pola bit adalah bilangan bulat di mana bit qq adalah nilai qubit qq. Encoding unary memakai satu qubit per level dan menyetel salah satunya. Encoding compact memakai ⌈log⁡2d⌉\lceil \log_2 d \rceil qubit dan menyimpan nomor level, dalam biner atau sebagai kode Gray di mana level bertetangga berbeda satu bit. Nama string dari sebuah encoding diterima di mana pun encoding-nya diterima.

Trade-off-nya adalah qubit melawan bobot operator. Encoding compact membutuhkan qubit yang jauh lebih sedikit secara eksponensial, tapi operator ladder menghubungkan level yang pola-polanya bisa berbeda di beberapa bit, sehingga suku-sukunya bekerja pada lebih banyak qubit. Bagian-bagian berikut melihat berapa biayanya, dimulai dari dua encoding unary, yang tata letaknya identik dan hanya berbeda di operatornya.

Bandingkan dua encoding unary​

Di encoding unary, operator ladder memindahkan satu eksitasi di antara dua qubit bertetangga. Untuk menuliskannya sebagai operator pada seluruh mode, kamu harus memilih apa yang dilakukan pada qubit lainnya. Kedua varian memilih secara berbeda:

  • unary_projected menaruh proyektor ∣0⟩⟨0∣\lvert 0 \rangle\langle 0 \rvert pada setiap qubit lain dari mode. Itu menegakkan batasan one-hot di dalam operator itu sendiri: suku hanya bekerja kalau sisa mode kosong.

  • unary_unprojected membiarkan qubit-qubit itu apa adanya, jadi suku bekerja sebagai identitas pada qubit tersebut.

Lihat satu b†b^\dagger pada d=4d = 4 di bawah keduanya. Mapper mengembalikan SparseObservable, yang suku-sukunya berupa string bit term single-qubit. Berikut empat suku pertama dari masing-masing, beserta qubit yang dikenai:

>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(b_dagger, LevelSpec(3, encoding))
... terms = observable.to_sparse_list()
... print(encoding, "->", len(terms), "terms on", observable.num_qubits, "qubits")
... for label, qubits, coefficient in terms[:4]:
... print(f" {label:5s} on qubits {qubits} coefficient {complex(coefficient):.4f}")
unary_projected -> 12 terms on 4 qubits
XX00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
XY00 on qubits [0, 1, 2, 3] coefficient 0.0000-0.2500j
YX00 on qubits [0, 1, 2, 3] coefficient 0.0000+0.2500j
YY00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
unary_unprojected -> 12 terms on 4 qubits
XX on qubits [0, 1] coefficient 0.2500+0.0000j
XY on qubits [0, 1] coefficient 0.0000-0.2500j
YX on qubits [0, 1] coefficient 0.0000+0.2500j
YY on qubits [0, 1] coefficient 0.2500+0.0000j

Keduanya punya dua belas suku dengan koefisien yang sama. Suku yang diproyeksikan membawa bit term 0 pada dua qubit yang tidak disebut oleh suku yang tidak diproyeksikan, jadi setiap suku terproyeksi menyentuh keempat qubit dan setiap suku tak terproyeksi menyentuh dua. Secara umum, suku terproyeksi menyentuh semua dd qubit dari mode, karena ia membawa dua faktor, masing-masing XX atau YY, dan sebuah proyektor pada tiap qubit lainnya.

Kedua operator sama pada state yang dituju encoding-nya, yaitu basis state one-hot (codeword), dan berbeda di luarnya. Untuk mengeceknya, ambil operator kecil pada dua mode di d=3d = 3 dan bandingkan matriksnya pada sembilan codeword:

>>> hamiltonian = BosonOperator.from_dict({
... (cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0,
... (cre(0), ann(0)): 0.5, (cre(1), cre(1), ann(1), ann(1)): 0.25,
... })
>>> matrices = {}
>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(hamiltonian, LevelSpec(2, encoding))
... matrices[encoding] = SparsePauliOp.from_sparse_observable(observable).to_matrix()
>>> unary = LevelEncoding.UNARY_PROJECTED
>>> codewords = sorted(
... unary.level_to_bits(first) | (unary.level_to_bits(second) << 3)
... for first in range(3) for second in range(3)
... )
>>> projected, unprojected = matrices["unary_projected"], matrices["unary_unprojected"]
>>> bool(np.allclose(projected[np.ix_(codewords, codewords)], unprojected[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(projected, unprojected))
False

Keduanya juga membiarkan subruang codeword tetap invarian, jadi state yang disiapkan pada sebuah codeword tetap berada di codeword. Pilihan ini hanya berpengaruh untuk state yang keluar dari subruang itu, misalnya lewat noise. Di sana operator terproyeksi menekan evolusi, karena proyektornya gagal, sedangkan yang tak terproyeksi terus bekerja. Tidak ada perilaku yang merupakan persyaratan fisik. Operator tak terproyeksi hanya terdefinisi pada subruang one-hot, jadi memilihnya adalah keputusan pemodelan, bukan optimasi gratis.

Harga dari proyektor adalah gate. Evolusikan b†+bb^\dagger + b yang Hermitian pada satu mode untuk waktu singkat, lalu hitung gate dua-qubit setelah transpilasi Qiskit:

>>> from qiskit import QuantumCircuit, transpile
>>> from qiskit.circuit.library import PauliEvolutionGate
>>>
>>> def cx_count(observable):
... circuit = QuantumCircuit(observable.num_qubits)
... circuit.append(PauliEvolutionGate(observable, time=0.1), range(observable.num_qubits))
... basis = ["cx", "rz", "sx", "x"]
... return transpile(circuit, basis_gates=basis, optimization_level=1).count_ops().get("cx", 0)
>>>
>>> hermitian = b + b_dagger
>>> levels = (3, 4, 5, 6)
>>> counts = {
... str(encoding): [
... cx_count(occupation_encoding(hermitian, LevelSpec(d - 1, encoding))) for d in levels
... ]
... for encoding in encodings
... }

Jumlah persisnya bergantung pada cara Qiskit mensintesis evolution gate, yang berubah antar rilis, jadi plot saja:

CX count against the number of levels d for the four encodings. The unary_projected count grows fastest, the unary_unprojected count grows linearly, and the Gray code is the cheapest.

Jumlah tak terproyeksi tumbuh secara linear terhadap dd, karena tiap dari d−1d - 1 transisi adalah rotasi dua-qubit. Jumlah terproyeksi tumbuh jauh lebih cepat, karena setiap suku bekerja pada semua dd qubit. Encoding compact butuh lebih sedikit qubit, tapi bobot suku-sukunya bergantung pada bagaimana pola bit level bertetangga berbeda: kode Gray, yang beda satu bit, adalah yang lebih murah dari dua encoding compact, dan biner standar melonjak saat carry membalik beberapa bit sekaligus (d=5d = 5). Apakah encoding compact juga mengalahkan unary tak terproyeksi bergantung pada dd dan cara Qiskit mensintesis evolusinya, jadi bacalah tren-nya, bukan peringkat pada satu ukuran.

Proyektor juga penting saat kamu mengembangkan operator terpetakan menjadi Pauli, yang dilakukan oleh from_sparse_observable() dengan mengganti tiap proyektor dengan jumlah dua operator Pauli. Suku dengan kk proyektor menjadi 2k2^k suku Pauli. Berikut satu b†b^\dagger pada dd kecil, yang merupakan satu-satunya ukuran di mana kamu sebaiknya mencoba ini:

>>> rows = [f"{'d':>2s} {'stored (both)':>14s} {'expanded, projected':>20s} {'expanded, unprojected':>22s}"]
>>> for levels in (3, 4, 5):
... stored = len(occupation_encoding(b_dagger, LevelSpec(levels - 1, "unary_projected")))
... expanded = [
... len(SparsePauliOp.from_sparse_observable(
... occupation_encoding(b_dagger, LevelSpec(levels - 1, encoding))).simplify())
... for encoding in ("unary_projected", "unary_unprojected")
... ]
... rows.append(f"{levels:2d} {stored:14d} {expanded[0]:20d} {expanded[1]:22d}")
>>> print("\n".join(rows))
d stored (both) expanded, projected expanded, unprojected
3 8 16 8
4 12 48 12
5 16 128 16

Kedua varian menyimpan jumlah suku yang sama, tapi ekspansi varian terproyeksi tumbuh secara eksponensial terhadap dd. Untuk apa pun yang lebih besar dari contoh mainan, baca suku-suku SparseObservable secara langsung, seperti yang dilakukan listing di atas, daripada mengonversinya.

Encoding compact tidak bisa membuang proyektornya. Di sana bit nol adalah bagian dari nomor level, jadi menghapus ∣0⟩⟨0∣\lvert 0 \rangle\langle 0 \rvert mengubah level mana yang dihubungkan suku, dan operatornya tidak lagi sama pada codeword. Inilah kenapa hanya encoding unary yang punya dua varian.

Perkalian ladder dipotong dulu​

Satu pilihan desain lagi terlihat pada biaya suku diagonal. Mapper tidak memetakan tiap operator ladder lalu mengalikan hasilnya. Ia mengumpulkan operator ladder yang diterapkan suku pada satu mode, mengalikannya sebagai matriks d×dd \times d di basis Fock, memotong hasil kali itu sekali, dan meng-encode hasilnya [2]. Hasil kali seperti operator bilangan b†bb^\dagger b dengan begitu tetap diagonal:

>>> number_observable = occupation_encoding(number, LevelSpec(3, "unary_unprojected"))
>>> [(label, qubits) for label, qubits, _ in number_observable.to_sparse_list()]
[('1', [1]), ('1', [2]), ('1', [3])]
>>> cx_count(number_observable)
0

Suku-sukunya adalah proyektor ∣1⟩⟨1∣\lvert 1 \rangle\langle 1 \rvert pada qubit tiap level di atas nol, dibobot dengan nomor level (level nol berbobot nol sehingga tidak punya suku). Evolusi waktu darinya berupa sekumpulan rotasi single-qubit dan tidak butuh gate dua-qubit. Mengalikan dua operator ladder terpetakan sebagai gantinya memberi operator yang sama pada codeword, tapi berbeda di luarnya, dengan string off-diagonal yang memakan biaya gate:

>>> unprojected = LevelSpec(3, "unary_unprojected")
>>> as_pauli = lambda operator: SparsePauliOp.from_sparse_observable(
... occupation_encoding(operator, unprojected, 4)).simplify()
>>> composed = as_pauli(b_dagger).dot(as_pauli(b)).simplify()
>>> direct = as_pauli(number)
>>> len(direct), len(composed)
(4, 16)
>>> cx_count(direct)
0
>>> bool(cx_count(composed) > 0)
True
>>> codewords = [LevelEncoding.UNARY_UNPROJECTED.level_to_bits(level) for level in range(4)]
>>> bool(np.allclose(direct.to_matrix()[np.ix_(codewords, codewords)],
... composed.to_matrix()[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(direct.to_matrix(), composed.to_matrix()))
False

Suku diagonal seperti interaksi on-site pada model Bose-Hubbard adalah hasil kali semacam ini, dan di situlah pilihan ini berpengaruh.

Tulis mapper kustom​

Kalau kamu menginginkan encoding yang tidak disediakan pustaka, bangun dari citra satu operator ladder. map_boson_action_generators() menerima sebuah fungsi yang memetakan satu BosonAction, sebuah fungsi yang mengembalikan identitas, lalu membangun mapper untuk operator utuh dari keduanya. Ia mengalikan citra aksi tiap suku secara berurutan, mengalikan dengan koefisien, dan menjumlahkan suku-sukunya. Outputnya bisa tipe apa pun yang mendukung perkalian skalar dan komposisi.

Sebagai contoh, ambil encoding untuk d=2d = 2 yang menyimpan satu mode dalam satu qubit. Operator creation adalah ∣1⟩⟨0∣\lvert 1 \rangle\langle 0 \rvert, yaitu (X−iY)/2(X - iY)/2 dalam konvensi Qiskit, dan operator annihilation adalah adjoin-nya:

>>> from qiskit_bosons.mappers import map_boson_action_generators
>>> from qiskit_bosons.operators import BosonAction
>>>
>>> num_qubits = 2
>>>
>>> def map_action(action: BosonAction) -> SparsePauliOp:
... is_creation, mode = action
... sign = -1j if is_creation else 1j
... return SparsePauliOp.from_sparse_list(
... [("X", [mode], 0.5), ("Y", [mode], 0.5 * sign)], num_qubits=num_qubits
... )
>>>
>>> def identity() -> SparsePauliOp:
... return SparsePauliOp.from_sparse_list([("", [], 1.0)], num_qubits)
>>>
>>> model = BosonOperator.from_dict({
... (cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0, (ann(0), cre(0)): 0.7, (cre(1), ann(1)): 0.3,
... })
>>> custom = map_boson_action_generators(model, map_action, identity)
>>> library = SparsePauliOp.from_sparse_observable(occupation_encoding(model, LevelSpec(1, "gray")))
>>> bool(np.allclose(custom.to_matrix(), library.to_matrix()))
True

Mapper tulisan tangan ini mengalikan operator ladder yang dipetakan, dan hasilnya sesuai dengan mapper pustaka di sini karena setiap mode punya dua level dan satu qubit menampung seluruh mode. Pada dd yang lebih besar, seperti yang ditunjukkan bagian sebelumnya, menyusun ladder terpetakan dan memotong hasil kali terlebih dahulu bukanlah operator yang sama di luar codeword.

Untuk memakai mapper seperti itu dalam Circuit, bungkus dalam fungsi dengan signature (operator, num_qubits) -> SparseObservable dan berikan ke MapperFnEvolutionSynthesis, yang memetakan operator dari setiap gate Evolution dengannya. Pipeline Transpiler dijelaskan di Transpile Circuit bosonik.

>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_bosons.transpiler.passes import MapperFnEvolutionSynthesis
>>>
>>> def mapper_fn(operator, num_qubits):
... return SparseObservable.from_sparse_pauli_op(
... map_boson_action_generators(operator, map_action, identity)
... )
>>>
>>> synthesis = MapperFnEvolutionSynthesis(mapper_fn)

Dua detail layak diketahui. Pertama, operator qubit harus punya koefisien riil, karena PauliEvolutionGate menolak bagian imajiner. Operator Hermitian dipetakan ke operator riil, tapi hanya setelah suku-suku yang saling meniadakan digabungkan. Mapper tulisan tangan ini tidak menggabungkannya, jadi suku hopping dan adjoin-nya meninggalkan koefisien imajiner sampai kamu menyederhanakan hasilnya. Mapper pustaka menggabungkan hasilnya sendiri; mapper kustom harus melakukannya, misalnya dengan membungkusnya dalam simplify():

>>> hopping = BosonOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> hermitian_hopping = hopping + hopping.adjoint()
>>> raw = mapper_fn(hermitian_hopping, 2)
>>> bool(np.allclose(np.asarray(raw.coeffs).imag, 0))
False
>>> merged = raw.simplify()
>>> bool(np.allclose(np.asarray(merged.coeffs).imag, 0))
True
>>> sorted(label for label, _, _ in merged.to_sparse_list())
['XX', 'YY']

Kedua, product formula mensintesis suku-suku sesuai urutan yang diterimanya, dan urutan yang dihasilkan mapper tidak dijamin stabil. Helper simplify() dan group_wise() membungkus fungsi mapper untuk mengendalikannya: simplify menggabungkan string duplikat dan mengurutkan suku ke urutan kanonik, sedangkan group_wise memetakan satu grup operator sekali waktu sehingga suku-suku sebuah grup tetap berdekatan. Jangan gabungkan keduanya, karena pengurutan membuang pengelompokan. Panduan pengelompokan menunjukkan cara menetapkan grup, dan panduan sintesis menunjukkan bagaimana urutan memengaruhi Circuit.

Langkah selanjutnya​

[1]

R. Somma, G. Ortiz, E. Knill, and J. Gubernatis, Quantum Simulations of Physics Problems (2003), arXiv:quant-ph/0304063, Sec. 4, Eq. 22.

[2] (1,2)

N. P. D. Sawaya, T. Menke, T. H. Kyaw, S. Johri, A. Aspuru-Guzik, and G. G. Guerreschi, Resource-efficient digital quantum simulation of d-level systems for photonic, vibrational, and spin-s Hamiltonians, npj Quantum Information 6, 49 (2020), arXiv:1909.12847.