Mensintesis Evolution gate di ruang bosonik
Evolusi waktu dari Hamiltonian yang merupakan jumlah dari bagian-bagian yang tidak komut tidak bisa diimplementasikan secara eksak gate demi gate. Product formula memperkirakannya dengan serangkaian evolusi singkat di bawah masing-masing bagian, dan error aproksimasinya disebut error Trotter.
Kamu bisa menerapkan product formula di dua tempat, dan keduanya tidak bisa saling menggantikan:
-
Di ruang bosonik, sebelum truncation atau encoding dipilih. Formula ini melihat grup dari operator (lihat Mengelompokkan suku operator: manfaatkan struktur operator) dan bisa menyisipkannya secara berselang-seling.
-
Di ruang qubit, di dalam
PauliEvolutionGatemilik Qiskit, setelah operator dipetakan. Formula ini melihat satu operator Pauli per grup dan hanya bisa membagi suku-suku di dalamnya.
Hanya yang pertama yang bisa mengurangi error antar grup yang tidak komut, yang biasanya paling dominan. Panduan ini menunjukkan cara memakainya.
Kedua langkah ini opsional. Sebuah gate Evolution yang tidak pernah didekomposisi di ruang bosonik sampai ke tahap boson-ke-qubit secara utuh, lalu dipetakan dan disintesis di sana. Itu tidak menimbulkan error di level bosonik, hanya apa pun yang diperkenalkan formula level qubit.
Persiapan
Rantai Bose-Hubbard dua situs, contoh terkecil di mana grup-grupnya tidak komut (lihat Simulasi dinamika Bose-Hubbard dengan pilihan encoding untuk model lengkapnya). Rantai ini dikelompokkan menjadi satu grup Hermitian untuk bond dan satu grup diagonal untuk interaksi on-site:
>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp, Statevector
>>> from qiskit.transpiler import PassManager
>>> from scipy.sparse import csc_matrix
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> num_sites, max_occupation, total_time = 2, 2, 0.8
>>> bonds = [(0, 1)]
>>> terms = {}
>>> for left, right in bonds:
... terms[(cre(left), ann(right))] = -1.0
... terms[(cre(right), ann(left))] = -1.0
>>> for site in range(num_sites):
... terms[(cre(site), cre(site), ann(site), ann(site))] = 1.0
>>> hamiltonian = BosonOperator.from_dict(terms)
>>> hamiltonian.groups = [
... len(bonds)
... if len({mode for _, mode in term}) == 1
... else bonds.index(tuple(sorted({mode for _, mode in term})))
... for term, _ in hamiltonian.iter_terms()
... ]
>>> hamiltonian.num_groups()
2
Untuk mengukur error, kamu butuh referensi. Evolusi eksak dari Hamiltonian yang dipetakan pada truncation yang sama adalah pilihan yang tepat, karena truncation adalah aproksimasi yang terpisah:
>>> spec = LevelSpec(max_occupation, "unary_projected")
>>> mapped = occupation_encoding(hamiltonian, spec).simplify()
>>> matrix = csc_matrix(SparsePauliOp.from_sparse_observable(mapped).to_matrix(sparse=True))
>>> pm = generate_preset_occupation_pass_manager(spec)
>>> pm.qubit = PassManager() # keep each group as one exact PauliEvolutionGate
>>>
>>> def build(**evolution_kwargs):
... circuit = BosonicCircuit(num_sites)
... circuit.append(InitializeModes([1, 1]), circuit.modes)
... circuit.append(
... Evolution(num_sites, hamiltonian, time=total_time, **evolution_kwargs), circuit.modes
... )
... return circuit
>>>
>>> # Under the unary encodings, occupation ``n`` of mode ``m`` sets qubit ``m * d + n``, with
>>> # ``d = max_occupation + 1`` levels per mode. That places |1, 1> on the basis state below.
>>> levels = max_occupation + 1
>>> index = sum(1 << (mode * levels + n) for mode, n in enumerate([1, 1]))
>>> initial = Statevector.from_int(index, 2**(num_sites * levels)).data
>>> exact = expm_multiply(-1j * total_time * matrix, initial)
>>> def error(circuit):
... """Distance to the exact evolution, of the circuit's product of group exponentials."""
... lowered = pm.run(circuit)
... state = Statevector.from_int(0, 2**lowered.num_qubits).evolve(lowered).data
... return float(np.max(np.abs(state - exact)))
error sengaja mensimulasikan circuit hasil lowering tanpa mendekomposisi instance PauliEvolutionGate-nya. Statevector menerapkan masing-masing sebagai eksponensial eksak dari grupnya, jadi satu-satunya yang tersisa adalah hasil kali eksponensial grup yang dipilih oleh sintesis bosonik.
Default
Tanpa argumen synthesis, gate Evolution didekomposisi dengan BosonicLieTrotter, formula orde pertama: satu faktor per grup, masing-masing untuk seluruh waktu.
>>> dict(build().decompose().count_ops())
{'Evolution': 2, 'InitializeModes': 1}
Tanpa dekomposisi, seluruh gate diserahkan ke tahap qubit dan sama sekali tidak ada error Trotter bosonik:
>>> error(build()) < 1e-6
True
Setelah didekomposisi, muncul error yang nyata, karena kedua grup tidak komut:
>>> error(build().decompose()) > 0.1
True
Kurangi error
Berikan metode sintesis yang berbeda ke gate. Orde yang lebih tinggi menyusun faktor-faktor secara simetris sehingga suku error utamanya saling meniadakan; lebih banyak repetisi membagi waktu menjadi langkah-langkah yang lebih pendek.
>>> from qiskit_bosons.circuit.library.synthesis import BosonicLieTrotter, BosonicSuzukiTrotter
>>>
>>> for label, synthesis in [
... ("Lie-Trotter", BosonicLieTrotter()),
... ("Suzuki, order 2", BosonicSuzukiTrotter(order=2)),
... ("Suzuki, order 4", BosonicSuzukiTrotter(order=4)),
... ("Suzuki, order 4*", BosonicSuzukiTrotter(order=4, merge_adjacent=False)),
... ]:
... circuit = build(synthesis=synthesis).decompose()
... print(f"{label:16s} {circuit.count_ops()['Evolution']:3d} factors")
Lie-Trotter 2 factors
Suzuki, order 2 3 factors
Suzuki, order 4 11 factors
Suzuki, order 4* 15 factors
Setiap kenaikan orde sebesar dua menyusun lima sapuan dari orde di bawahnya (pertumbuhan dari rekursi Suzuki, diterapkan pada faktor dari orde 2), yaitu jumlah pada baris terakhir, yang ditandai bintang. Sapuan-sapuan itu bertemu di faktor yang sama, sehingga gate bertetangga berevolusi di bawah satu operator dan digabung menjadi satu gate dengan waktu yang dijumlahkan. Itu menghemat satu gate per sambungan dan bersifat eksak. Berikan merge_adjacent=False untuk mempertahankan urutan versi buku teks. Bagaimanapun, orde yang lebih tinggi memberi error yang lebih kecil:
>>> lie = error(build(synthesis=BosonicLieTrotter()).decompose())
>>> second = error(build(synthesis=BosonicSuzukiTrotter(order=2)).decompose())
>>> fourth = error(build(synthesis=BosonicSuzukiTrotter(order=4)).decompose())
>>> lie > second > fourth
True
Repetisi menukar depth dengan akurasi pada laju yang berbeda. Error orde pertama turun secara linear terhadap ukuran langkah, sedangkan orde kedua secara kuadratik:
>>> lie_errors = [error(build(synthesis=BosonicLieTrotter(reps=r)).decompose()) for r in (1, 2, 4, 8)]
>>> bool(all(a > b for a, b in zip(lie_errors, lie_errors[1:])))
True
>>> bool(lie_errors[0] / lie_errors[-1] > 6) # about 8, i.e. linear in 1 / reps
True
>>> suzuki_errors = [
... error(build(synthesis=BosonicSuzukiTrotter(order=2, reps=r)).decompose()) for r in (1, 2, 4)
... ]
>>> bool(suzuki_errors[0] / suzuki_errors[-1] > 10) # about 16, i.e. quadratic in 1 / reps
True
Mana yang lebih baik tergantung pada operatornya. Perhatikan bahwa operator dengan satu grup saja eksak pada orde berapa pun, karena satu faktor tunggal komut dengan dirinya sendiri, dan formula itu lalu menyusut menjadi satu gate alih-alih membaginya ulang.
Ukur biayanya
Error yang lebih kecil butuh lebih banyak faktor, dan setiap faktor memakan gate entangling setelah tahap qubit mensintesisnya. Berapa banyaknya tergantung pada encoding, jadi hitung lewat pipeline preset, dengan basis tetap dan tanpa optimisasi level qubit (optimization_level=0), untuk melihat harga tiap metode:
>>> basis = ["rz", "sx", "x", "cx"]
>>>
>>> def cx_count(circuit, encoding):
... spec = LevelSpec(max_occupation, encoding)
... lowered = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=basis
... ).run(circuit)
... return lowered.count_ops().get("cx", 0)
>>>
>>> methods = [
... ("Lie-Trotter", BosonicLieTrotter()),
... ("Suzuki, order 2", BosonicSuzukiTrotter(order=2)),
... ("Suzuki, order 4", BosonicSuzukiTrotter(order=4)),
... ]
>>> encodings = ("gray", "unary_unprojected")
>>> counts = {
... encoding: [
... cx_count(build(synthesis=method).decompose(), encoding) for _, method in methods
... ]
... for encoding in encodings
... }
>>> # the order buys accuracy at a price that grows with it
>>> bool(counts["gray"][0] < counts["gray"][1] < counts["gray"][2])
True
Jumlah persisnya bergantung pada cara Qiskit mensintesis evolution gate, yang berubah antar rilis, jadi plot menampilkannya sebagai ganti tabel:
Setiap kelompok batang adalah satu metode: Lie-Trotter, Suzuki orde 2, dan Suzuki orde 4. Orde membeli akurasi dengan harga yang tumbuh sesuai faktor rekursi, dan encoding unary membayar lebih mahal daripada kode Gray untuk setiap metode karena tiap faktornya dipetakan ke lebih banyak gate entangling. Pilih metode sesuai akurasi yang kamu butuhkan dan encoding sesuai hardware, dan pisahkan kedua pilihan itu: circuit bosonik di atas tidak pernah berubah.
Terapkan satu metode ke seluruh circuit
Untuk mengatur synthesis pada setiap gate, kamu harus meneruskan pilihan itu ke semua yang membangunnya. Pass B2BSynthesis membuat pilihan itu sekali untuk seluruh pipeline. config-nya menamai metode untuk setiap jenis gate, dan pass ini sekaligus memilih metode dan mengekspansi gate:
>>> from qiskit_bosons.transpiler import BosonicPassManager
>>> from qiskit_bosons.transpiler.passes import B2BSynthesis
>>>
>>> trotterize = BosonicPassManager([B2BSynthesis({"Evolution": BosonicSuzukiTrotter(order=2)})])
>>> dict(trotterize.run(build()).count_ops())
{'Evolution': 3, 'InitializeModes': 1}
Sebuah metode juga bisa disebut namanya, dengan argumen untuk __init__-nya, alih-alih dibangun: B2BSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}). Nama yang tersedia adalah milik B2BSynthesisPluginManager, dan metode yang tidak terdaftar di mana pun bisa diserahkan sebagai instance, seperti di atas.
Dengan apply=False, pass hanya memilih metode dan menunda ekspansi sampai saat dibutuhkan, misalnya untuk memeriksa circuit terlebih dahulu. Gate atomik tidak pernah disentuh, bahkan tidak diberi tag:
>>> selected = BosonicPassManager(
... [B2BSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}, apply=False)]
... ).run(build())
>>> selected.count_ops()["Evolution"] # selected, not yet expanded
1
>>> selected.decompose().count_ops()["Evolution"] # expanded on demand
3
Gate yang belum diekspansi sampai ke tahap boson-ke-qubit secara utuh, di mana ia dipetakan tanpa synthesis-nya dipertimbangkan, sehingga setiap metode menghasilkan output yang identik.
filter opsional membatasinya pada gate yang diuntungkan. Evolusi yang semua grupnya komut, seperti Hamiltonian yang hanya terdiri dari operator bilangan, disintesis secara eksak pada orde berapa pun, jadi orde yang lebih tinggi hanya menambah depth di sana.
Batasi pass pada gate yang diuntungkan
Operator yang murni diagonal membuat poin ini konkret, karena operator bilangan saling komut. Grup-grupnya eksak pada orde pertama, dan orde kedua menghabiskan gate entangling tanpa guna:
>>> # one group per term, each Hermitian; the order is fixed because order two evolves the first
>>> # group twice, so which group comes first changes the cost
>>> diagonal = BosonOperator.from_terms_with_groups(
... [([cre(0), ann(0), cre(1), ann(1)], 1.0, 0), ([cre(0), ann(0)], 0.5, 1)]
... )
>>>
>>> def diagonal_cost(synthesis):
... circuit = BosonicCircuit(num_sites)
... circuit.append(Evolution(num_sites, diagonal, time=total_time, synthesis=synthesis), circuit.modes)
... return cx_count(circuit.decompose(), "gray")
>>>
>>> bool(diagonal_cost(BosonicSuzukiTrotter(order=2)) > diagonal_cost(BosonicLieTrotter()))
True
Kedua circuit eksak, jadi yang kedua memakai lebih banyak gate entangling tanpa keuntungan. (Formula simetris orde dua menerapkan grup pertama dua kali dengan setengah waktu masing-masing, sehingga biayanya bergantung pada grup mana yang datang lebih dulu.) filter menerima predikat atas DAGOpNode dan membiarkan gate yang ditolak tidak tersentuh, sehingga satu pass bisa menaikkan orde pada gate yang diuntungkan dan mempertahankan sisanya di orde pertama. Predikat di bawah menerima gate hanya jika operatornya memindahkan boson antar mode, yaitu saat grup-grupnya tidak komut:
>>> def is_diagonal(operator):
... """Whether every term only counts bosons, rather than moving them between modes."""
... return all(
... sorted(mode for created, mode in actions if created)
... == sorted(mode for created, mode in actions if not created)
... for actions, _ in operator.iter_terms()
... )
>>>
>>> def mixed_circuit():
... circuit = BosonicCircuit(num_sites)
... circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
... circuit.append(Evolution(num_sites, diagonal, time=total_time), circuit.modes)
... return circuit
>>>
>>> order_2 = BosonicSuzukiTrotter(order=2)
>>> everywhere = cx_count(
... BosonicPassManager([B2BSynthesis({"Evolution": order_2})]).run(mixed_circuit()), "gray"
... )
>>> restricted = cx_count(
... BosonicPassManager(
... [
... B2BSynthesis(
... {"Evolution": order_2},
... filter=lambda node: not is_diagonal(node.op.operator),
... )
... ]
... ).run(mixed_circuit()),
... "gray",
... )
>>> bool(everywhere > restricted) # the filter saves entangling gates
True
Kalau kamu menjalankan pass itu lagi, tidak ada yang berubah: faktor yang dihasilkannya bersifat atomik.
Faktor atomik
Sebuah Evolution yang ditandai atomic tidak punya definisi, sehingga decompose() membiarkannya di tempat sebanyak apa pun ia diterapkan. Sebuah sintesis menandai faktor yang dihasilkannya dengan cara ini, begitu pula BosonicQDrift untuk suku yang disampelnya.
Ini lebih dari sekadar kerapian. Membagi grup Hermitian lebih lanjut, suku demi suku, menghasilkan faktor yang masing-masing tidak Hermitian, dan eksponensial dari operator non-Hermitian tidak unitari:
>>> circuit = build()
>>> once = circuit.decompose()
>>> dict(circuit.decompose(reps=5).count_ops()) == dict(once.count_ops())
True
Setiap faktor harus Hermitian dengan sendirinya. Operator Hermitian yang dibagi menjadi grup bisa menghasilkan grup non-Hermitian, seperti suku hopping yang dipisahkan dari adjoint-nya. Periksa dengan groups_are_hermitian(). Operator tanpa grup dibagi suku demi suku, dan tahap qubit lalu menolak faktornya dengan Operator contains complex coefficients. Simetrisasi pada orde genap sebagian meniadakan error dari faktor non-Hermitian, sehingga operator yang pengelompokannya salah bisa tampak lebih baik pada orde 2 daripada orde 1 padahal tetap salah.
Ulangi langkah pendek
Sebagai alternatif di level circuit, bangun satu langkah Trotter pendek lalu ulangi. repeat() meratakan repetisi menjadi satu circuit dan membiarkan batasnya bersebelahan, sehingga pass bisa bekerja melintasinya:
>>> step = BosonicCircuit(num_sites)
>>> step.append(Evolution(num_sites, hamiltonian, time=total_time / 4), step.modes)
>>> dict(step.repeat(4).count_ops())
{'Evolution': 4}
Berikan insert_barriers=True untuk memisahkan repetisi. Barrier dipetakan ke setiap qubit dari register yang disentuhnya, karena sebuah encoding butuh beberapa qubit per mode dan mode tidak punya citra per qubit sendiri:
>>> [inst.operation.name for inst in step.repeat(3, insert_barriers=True)._inner.data]
['Evolution', 'barrier', 'Evolution', 'barrier', 'Evolution']
repeat tidak berkaitan dengan reps pada metode sintesis. Sebuah sintesis membagi satu evolusi menjadi lebih banyak faktor yang lebih pendek. Mengulang circuit yang berisi satu evolusi hanya melipatgandakan total waktunya.
Gabungkan dengan formula level qubit
Kedua level ini bisa dikomposisikan, dan jika keduanya mengaproksimasi, akurasi hasil ditentukan oleh yang lebih lemah. Menaikkan orde bosonik sementara formula sisi qubit tetap orde pertama tidak banyak membantu. Lihat MapperFnEvolutionSynthesis untuk formula sisi qubit. Ia mempertahankan urutan suku Pauli yang dipilih mapper, sehingga group_wise() bisa mengeluarkan suku-suku tiap grup secara bersebelahan, dan simplify() bisa mengunci urutan kanonik sebagai gantinya.
Langkah berikutnya
-
Transpile circuit bosonik menjelaskan tahap-tahap setelah sintesis bosonik, dan di mana
B2BSynthesisberada di antaranya. -
Mengelompokkan suku operator: manfaatkan struktur operator menjelaskan cara menetapkan dan memeriksa grup yang disisipkan oleh product formula.
-
Simulasi dinamika Bose-Hubbard dengan pilihan encoding menerapkan skema repetisi pada contoh lengkap.