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?

Merotasi mode bosonik

Sebuah ModeRotation mencampur mode bosonik dengan matriks uniter UU, seperti jaringan beamsplitter dan phase shifter. Disebut pasif karena ia mempertahankan jumlah total boson: ia memindahkan boson antar mode dan tidak pernah menciptakan atau menghancurkannya. (Squeezing dan displacement adalah operasi aktif, dan rotasi mode tidak bisa mengungkapkannya.) Ia memetakan operator creation sebagai

bi†↦∑jUji bj† ,b^\dagger_i \mapsto \sum_j U_{ji} \, b^\dagger_j \, ,

yang pada seluruh ruang Fock adalah uniter many-body

exp⁡(∑ijlog⁡(U)ij bi†bj) .\exp\left(\sum_{ij} \log(U)_{ij} \, b^\dagger_i b_j\right) \, .

Ini adalah gate di balik interferometer optik linear: kirim state Fock masuk, rotasikan mode-modenya, lalu baca bagaimana boson terdistribusi ulang. Seperti setiap gate dari BosonicCircuit (lihat Bekerja dengan Circuit bosonik), ia tidak membawa pemotongan maupun encoding. Keduanya masuk saat waktu transpile, dan panduan ini menunjukkan kenapa pemotongan butuh perhatian khusus untuk sebuah rotasi.

Bangun sebuah rotasi​

Konstruktor menerima matriks uniter M×MM \times M. Gate bekerja pada MM mode. Kelas ini tidak mengecek apakah matriksnya uniter, jadi itu tanggung jawabmu.

>>> import numpy as np
>>> from qiskit_bosons.circuit.library import ModeRotation
>>>
>>> unitary = np.array([[1, -1], [1, 1]]) / np.sqrt(2)
>>> rotation = ModeRotation(unitary)
>>> rotation.num_modes # the gate acts on two modes
2

Rotasi umum punya konstruktor khusus. ModeRotation.beamsplitter() menerima sudut pencampuran θ\theta dan fase opsional ϕ\phi, dan θ=π/4\theta = \pi/4 memberi beamsplitter 50:50 yang seimbang. ModeRotation.phase_shift() menerima satu fase per mode.

>>> beamsplitter = ModeRotation.beamsplitter(np.pi / 4)
>>> np.round(beamsplitter.rotation_unitary.real, 4)
array([[ 0.7071, -0.7071],
[ 0.7071, 0.7071]])
>>> bool(np.allclose(beamsplitter.rotation_unitary, unitary))
True
>>> shift = ModeRotation.phase_shift([0.0, np.pi / 2])
>>> np.round(np.diag(shift.rotation_unitary), 4)
array([1.+0.j, 0.+1.j])

Kamu juga bisa mulai dari generator anti-Hermitian KK dan membiarkan gate meng-eksponensialkannya, yang memberi rotasi exp⁡(K)\exp(K). Generatornya didapat kembali dengan generator().

>>> generator = np.array([[0, -0.3], [0.3, 0]], dtype=complex)
>>> small = ModeRotation.from_generator(generator)
>>> np.round(small.rotation_unitary.real, 4)
array([[ 0.9553, -0.2955],
[ 0.2955, 0.9553]])
>>> bool(np.allclose(small.generator(), generator))
True

Jembatan ke aljabar operator adalah to_operator(), yang mengembalikan generator Hermitian H=∑ijilog⁡(U)ij bi†bjH = \sum_{ij} \mathrm{i} \log(U)_{ij} \, b^\dagger_i b_j sebagai BosonOperator, sehingga gate-nya adalah e−iHe^{-\mathrm{i} H}. Untuk beamsplitter 50:50, itu berupa sepasang suku hopping dengan koefisien imajiner berlawanan. Operatornya Hermitian, yang memang dibutuhkan gate Evolution, dan ia mempertahankan jumlah partikel.

>>> operator = beamsplitter.to_operator()
>>> # The stored order of the terms is not specified, so sort them before printing.
>>> sorted((term[0], round(term[1].imag, 4)) for term in operator.iter_terms())
[([(True, 0), (False, 1)], -0.7854), ([(True, 1), (False, 0)], 0.7854)]
>>> operator.is_hermitian()
True
>>> operator.conserves_particle_number()
True

Setiap suku dibaca sebagai (is_creation, mode) dari kiri ke kanan, jadi yang kedua adalah iπ4 b1†b0\mathrm{i} \tfrac{\pi}{4} \, b^\dagger_1 b_0.

Bekerja pada state Fock​

Sebuah rotasi memindahkan boson antar mode, jadi efeknya paling mudah dilihat pada state Fock. Efek Hong-Ou-Mandel adalah contoh standarnya. Kirim satu boson ke tiap mode input dari beamsplitter seimbang. Dua jalur yang meninggalkan boson di mode output berbeda berinterferensi destruktif, sehingga state ∣1,1⟩\lvert 1, 1 \rangle lenyap dan boson selalu keluar bersama, di ∣2,0⟩\lvert 2, 0 \rangle atau ∣0,2⟩\lvert 0, 2 \rangle, masing-masing dengan probabilitas setengah.

Bangun Circuit pada mode bosonik dan turunkan dengan preset pass manager. LevelSpec menetapkan pemotongan dan encoding pada titik ini. Cutoff max_occupation=2 mempertahankan level 0, 1, dan 2 dari setiap mode.

>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import InitializeModes
>>> from qiskit_bosons.mappers import LevelEncoding, LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> circuit = BosonicCircuit(2)
>>> circuit.append(InitializeModes([1, 1]), circuit.modes)
>>> circuit.append(ModeRotation.beamsplitter(np.pi / 4), circuit.modes)

Pipeline mengembalikan Circuit qubit biasa. Baca probabilitas hasil dari state akhir, di sini dengan Statevector milik Qiskit, yang eksak pada ukuran ini. Simulator apa pun yang mengembalikan amplitudo atau counts bisa dipakai. State Fock ∣n0,n1⟩\lvert n_0, n_1 \rangle adalah satu computational basis state, yang indeksnya diberikan oleh pola bit dari LevelEncoding.level_to_bits(); qubit mode kedua mengikuti qubit mode pertama.

>>> from qiskit.quantum_info import Statevector
>>> from qiskit.transpiler import PassManager
>>>
>>> def fock_probabilities(circuit, max_occupation):
... """Maps each pair of occupations to the probability of that outcome."""
... encoding = LevelEncoding.UNARY_UNPROJECTED
... spec = LevelSpec(max_occupation, encoding)
... pass_manager = generate_preset_occupation_pass_manager(spec)
... pass_manager.qubit = PassManager() # keep the evolutions as the pipeline emitted them
... qubit_circuit = pass_manager.run(circuit)
... state = Statevector(qubit_circuit)
... width = encoding.num_qubits(max_occupation + 1)
... probabilities = {}
... for n0 in range(max_occupation + 1):
... for n1 in range(max_occupation + 1):
... index = encoding.level_to_bits(n0) | (encoding.level_to_bits(n1) << width)
... probability = round(float(abs(state.data[index]) ** 2), 4)
... if probability > 0:
... probabilities[(n0, n1)] = probability
... return probabilities
>>>
>>> fock_probabilities(circuit, max_occupation=2)
{(0, 2): 0.5, (2, 0): 0.5}

Hasil ∣1,1⟩\lvert 1, 1 \rangle punya probabilitas nol, dan dua hasil bunched berbagi probabilitas secara merata.

Pilih cutoff​

Hasil sebelumnya membutuhkan max_occupation=2. Jalankan Circuit yang sama dengan cutoff 1. Itu cukup untuk menampung state input ∣1,1⟩\lvert 1, 1 \rangle, tapi tidak untuk state yang dihasilkan rotasi darinya, yang punya bobot pada ∣2,0⟩\lvert 2, 0 \rangle dan ∣0,2⟩\lvert 0, 2 \rangle:

>>> fock_probabilities(circuit, max_occupation=1)
{(1, 1): 1.0}

Circuit berjalan tanpa error apa pun, dan jawabannya salah: state tetap di ∣1,1⟩\lvert 1, 1 \rangle dengan probabilitas satu. Rotasi mencampur sektor okupansi di dalam satu mode. Dua boson bisa berakhir di mode output yang sama, jadi state eksak punya bobot pada okupansi 2 di mode yang awalnya berisi satu boson. Dengan cutoff 1 level itu tidak ada, dan generator terpotong tidak bisa memindahkan state ke sana.

Batas pada okupansi state awal karena itu tidak cukup. Cutoff harus mencakup jumlah total boson yang bisa sampai ke satu mode mana pun dari rotasi. Untuk total NN boson, pilihan aman adalah cutoff NN.

Penalaran yang sama berlaku untuk tiap mode secara terpisah, jadi campuran cutoff berbeda per mode (lihat Deskripsikan pemotongan dengan LevelSpec) membuat rotasi mudah dipetakan secara keliru. Mode yang bisa menerima boson butuh cutoff yang mencakup apa yang bisa diterimanya, bukan hanya apa yang dipegangnya di awal. Pengecekan LevelSpec.truncation() membandingkan sebuah spec dengan batas bawah okupansi dari sebuah operator. Ia tidak bisa menandai kasus di atas. Setiap suku generator beamsplitter memegang satu operator creation, jadi batasnya satu dan pengecekan tidak melaporkan kekurangan pada cutoff 1, padahal hasilnya di sana salah:

>>> LevelSpec(1, "unary_unprojected").truncation(beamsplitter.to_operator())
[0, 0]

Peringatan lebih lanjut muncul dari cara Transpiler membangun rotasi (bagian berikutnya). Ia menerapkan rotasi sebagai urutan faktor dua-mode, dan dokumentasi plugin memperingatkan bahwa satu faktor bisa mengisi level yang tidak pernah dicapai rotasi lengkapnya. Urutan terpotong dan evolusi tunggal terpotong itu kemudian menjadi aproksimasi berbeda dari uniter yang sama. Pencarian atas 1.080 rotasi tiga-mode acak, dengan satu boson di masing-masing dari dua mode input dan tanpa okupansi ganda di output, tidak menemukan kasus seperti itu, jadi anggap ini sebagai tindakan pencegahan, bukan kegagalan yang umum. Konvergenkan cutoff seperti untuk Circuit lain, dengan menaikkannya sampai hasilnya berhenti berubah.

Cara Transpiler mensintesis sebuah rotasi​

Tahap optimization dari preset pipeline menguraikan ModeRotation di ruang bosonik terlebih dahulu, dengan pass B2BSynthesis dan metode BosonicClementsDecomposition (terdaftar sebagai ModeRotation.Clements). Itu adalah pilihan preset, bukan persyaratan framework: bagian berikutnya menurunkan gate yang sama tanpa dekomposisi apa pun. Dekomposisi ini tidak meng-eksponensialkan generator penuh dalam satu langkah. Ia pertama memfaktorkan uniter menjadi paling banyak M(M−1)/2M(M-1)/2 rotasi antara mode yang bersebelahan ditambah MM fase single-mode, mengikuti mesh persegi panjang dari Clements dkk. [1]. Setiap faktor menjadi gate Evolution atomik, yang dipetakan tahap boson-ke-qubit seperti yang lain.

Faktorisasi itu sendiri adalah givens_decomposition(), yang dipanggil oleh dekomposisi tersebut. Pemetaan dari sebuah unitary ke rotasi many-body adalah homomorfisme grup, jadi hasil kali rotasi-rotasi faktor persis sama dengan rotasi penuh pada ruang yang tidak dipotong. Dengan begitu, mesh ini tidak menambah galat apa pun. Keuntungannya ada pada lokalitas. Setiap evolusi yang dihasilkan hanya melibatkan suku Pauli dari dua mode yang dicampurnya (atau dari satu mode untuk sebuah fase), bukan suku-suku dari semua mode.

Transpile rotasi acak tiga mode lalu periksa apa yang dihasilkan pipeline. Unitary 3×33 \times 3 yang generik membutuhkan tiga rotasi maksimum, ditambah tiga fase, dan setiap faktor adalah PauliEvolutionGate tersendiri. Semuanya dideklarasikan pada seluruh register, tetapi operator di dalamnya hanya bekerja pada beberapa qubit saja:

>>> from qiskit.quantum_info import random_unitary
>>>
>>> random = np.asarray(random_unitary(3, seed=7).data)
>>> spec = LevelSpec(2, "unary_unprojected")
>>> rotation_circuit = BosonicCircuit(3)
>>> rotation_circuit.append(InitializeModes([1, 0, 0]), rotation_circuit.modes)
>>> rotation_circuit.append(ModeRotation(random), rotation_circuit.modes)
>>> pass_manager = generate_preset_occupation_pass_manager(spec)
>>> pass_manager.qubit = PassManager()
>>> qubit_circuit = pass_manager.run(rotation_circuit)
>>> dict(qubit_circuit.count_ops())
{'PauliEvolution': 6, 'x': 3}
>>> for instruction in qubit_circuit.data:
... if instruction.operation.name == "PauliEvolution":
... terms = instruction.operation.operator.to_sparse_list()
... print(len(terms), sorted({qubit for _, indices, _ in terms for qubit in indices}))
64 [0, 1, 2, 3, 4, 5]
64 [3, 4, 5, 6, 7, 8]
64 [0, 1, 2, 3, 4, 5]
2 [1, 2]
2 [4, 5]
2 [7, 8]

Dengan tiga level per mode, setiap mode menempati tiga qubit. Sebuah beamsplitter di antara dua mode menyentuh enam qubit dari kedua mode itu, dan sebuah fase menyentuh qubit dari satu mode.

Faktor-faktor ini baru eksak kalau setiap evolusi disintesis dengan baik. Suku-suku dari generator beamsplitter tidak saling komut, jadi product formula satu langkah bawaan menyisakan galat di setiap faktor. Berikan product formula dengan repetisi lebih banyak ke plugin Evolution yang memetakan faktor-faktor tersebut untuk mengurangi galat itu:

>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.synthesis import LieTrotter
>>> from qiskit_bosons.circuit.library.synthesis import BosonicClementsDecomposition
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.transpiler import (
... BosonicCircuitToDAG,
... BosonicPassManager,
... QuantumDAGToCircuit,
... )
>>> from qiskit_bosons.transpiler.passes import (
... B2BSynthesis,
... B2QSynthesis,
... MapperFnEvolutionSynthesis,
... OccupationB2QLayout,
... OccupationInitializeModesSynthesis,
... )
>>>
>>> def with_product_formula(product_formula):
... synthesis = B2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... lambda operator, num_qubits: occupation_encoding(operator, spec, num_qubits),
... product_formula,
... )
... synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
... return MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=BosonicPassManager(
... B2BSynthesis({"ModeRotation": BosonicClementsDecomposition()})
... ),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )

Satu boson yang masuk ke mode 0 keluar di mode jj dengan probabilitas ∣Uj0∣2|U_{j0}|^2. Bandingkan circuit hasil sintesis dengan hasil eksak itu untuk jumlah repetisi yang meningkat. Dekomposisi circuit-nya dulu supaya product formula diterapkan; tanpa itu, simulator eksak seperti Statevector menerapkan gate sebagai eksponensial penuhnya dan menyembunyikan galatnya.

>>> encoding = LevelEncoding.UNARY_UNPROJECTED
>>> width = encoding.num_qubits(3)
>>> exact = np.abs(random[:, 0]) ** 2
>>>
>>> def one_boson_probabilities(qubit_circuit):
... """Probability that the single boson sits in mode j, for each j."""
... state = Statevector(qubit_circuit)
... probabilities = []
... for j in range(3):
... occupations = [1 if mode == j else 0 for mode in range(3)]
... index = sum(
... encoding.level_to_bits(n) << (width * mode) for mode, n in enumerate(occupations)
... )
... probabilities.append(abs(state.data[index]) ** 2)
... return np.array(probabilities)
>>>
>>> for repetitions in (1, 4, 16):
... lowered = with_product_formula(LieTrotter(reps=repetitions)).run(rotation_circuit)
... error = np.max(np.abs(one_boson_probabilities(lowered.decompose(reps=2)) - exact))
... print(f"{repetitions:2d} repetitions: error {error:.1e}")
1 repetitions: error 1.2e-03
4 repetitions: error 7.6e-05
16 repetitions: error 4.7e-06

Galat turun sekitar empat kali lipat setiap kali repetisi naik empat kali lipat, sesuai harapan untuk formula orde pertama. Biayanya naik secara proporsional, karena setiap faktor diulang.

Menurunkan rotasi tanpa mendekomposisinya​

Dekomposisi di ruang bosonik adalah salah satu cara untuk menurunkan rotasi. (Library ini tidak menyertakan cara lain, tetapi tahap boson-ke-qubit tidak bergantung padanya.) B2QSynthesis mencari plugin berdasarkan nama gate, dan ModeRotation yang dibiarkan utuh sampai ke sana seperti gate lainnya. Plugin untuknya memetakan generator dari ModeRotation.to_operator() sekaligus dan menghasilkan satu PauliEvolutionGate pada waktu satuan. Hanya itu saja:

>>> from qiskit.circuit.library import PauliEvolutionGate
>>> from qiskit_bosons.transpiler.passes.utils import map_node_single_register
>>>
>>> class DirectModeRotationSynthesis:
... """Maps the generator of the whole rotation, with no bosonic decomposition."""
...
... def __init__(self, mapper_fn):
... self.mapper_fn = mapper_fn
...
... def run(self, in_node, out_dag, *, b2q_layout):
... modes, qreg = map_node_single_register(in_node, b2q_layout)
... generator = in_node.op.to_operator().relabel_modes(list(modes))
... observable = self.mapper_fn(generator, len(qreg))
... out_dag.apply_operation_back(PauliEvolutionGate(observable, time=1.0), tuple(qreg))

Daftarkan plugin itu untuk nama gate di pass manager yang tidak mendekomposisinya:

>>> def mapper_fn(operator, num_qubits):
... return occupation_encoding(operator, spec, num_qubits)
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(mapper_fn)
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> synthesis.methods["ModeRotation"] = DirectModeRotationSynthesis(mapper_fn)
>>> direct = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> direct_circuit = direct.run(rotation_circuit)
>>> dict(direct_circuit.count_ops())
{'x': 3, 'PauliEvolution': 1}
>>> terms = direct_circuit.data[-1].operation.operator.to_sparse_list()
>>> len(terms), sorted({qubit for _, indices, _ in terms for qubit in indices})
(198, [0, 1, 2, 3, 4, 5, 6, 7, 8])

Sekarang rotasinya adalah satu evolusi atas kesembilan qubit, sedangkan mesh punya enam, yang tidak satu pun mencakup lebih dari enam. Keduanya bukan pendekatan yang sama. Pemotongan di sini diterapkan sekali pada generator seluruh rotasi, dan pada setiap faktor secara terpisah di mesh (karena itu ada peringatan tentang cutoff di atas). Jalur langsung sama dengan occupation_encoding() dari operator pada cutoff berapa pun, jadi ini adalah acuan untuk membandingkan sebuah mesh. Harganya adalah lokalitas: suku-sukunya mencakup semua mode, dan product formula punya lebih banyak suku untuk dibagi.

Tidak ada yang lebih baik secara umum, dan itulah alasan untuk membiarkan pilihannya terbuka. Tulis plugin seperti ini untuk membandingkan cara penurunan, untuk memakai rotasi khusus encoding, atau untuk menyerahkan gate ke sesuatu yang memahaminya secara native.

Langkah berikutnya​

[1]

W. R. Clements, P. C. Humphreys, B. J. Metcalf, W. S. Kolthammer, and I. A. Walmsley, Optimal design for universal multiport interferometers, Optica 3, 1460 (2016), arXiv:1603.08788.