Simulasikan dinamika Fermi-Hubbard 1D dengan flow set
Fungsi yang dijelaskan di panduan ini saat ini hanya tersedia di Python API. Fungsionalitas yang setara akan tersedia di C API pada rilis mendatang.
Panduan ini membangun circuit untuk dinamika waktu model Fermi-Hubbard satu dimensi, mulai dari menuliskan Hamiltonian sampai memeriksa densitas site hasil evolusi. Yang membuat jalur ini menarik adalah satu ide dari framework flow set oleh Gandon dkk. [1], karya yang juga melatarbelakangi TransferVertexOperator di paket ini:
Alih-alih mengelompokkan suku Hamiltonian setelah pemetaan fermion-ke-qubit, kelompokkan sebelum pemetaan, ke dalam flow set, yaitu himpunan bagian satu dimensi dari graf interaksi fermionik berarah yang operator transfernya saling komut.
Pilihan itu menentukan semua yang ada di bawah. Ini membutuhkan hal-hal berikut:
-
Encoding fermion-ke-qubit kustom yang disesuaikan dengan flow set, yang di sini memakai satu qubit ancilla sehingga jumlah qubit yang terkait dengan implementasi suku Hamiltonian tertentu tidak lagi bertambah seiring jumlah mode fermionik (seperti pada Jordan-Wigner (JW)).
-
Synthesis kustom yang memanfaatkan sifat komutatif di dalam tiap set. Hasilnya adalah circuit dengan depth dua-qubit yang konstan terhadap ukuran sistem, dibandingkan pertumbuhan linear pada Trotterisasi suku-per-suku berbasis JW.
Mekanisme yang memasukkan encoding buatanmu sendiri ke transpiler (CustomF2QLayout dan MapperFnEvolutionSynthesis) berlaku untuk model apa pun.
Panduan transpilasi untuk stage transpiler yang dirujuk di seluruh panduan ini, dan panduan mapper untuk resep umum menulis mapper kustom.
Operator transfer dan flow set
Sebuah TransferVertexOperator dibangun dari operator vertex dan operator transfer . Dalam bentuk operator kreasi dan anihilasi fermionik,
jadi mengukur okupansi mode (bernilai saat kosong dan saat terisi), sedangkan memindahkan sebuah fermion sepanjang edge berarah . Perhatikan bahwa kedua indeks pada tidak bisa dipertukarkan. Indeks pertama membawa kombinasi minus dan indeks kedua membawa plus, dan inilah yang membuat berbeda dari serta memberi edge arahnya.
Keduanya memenuhi relasi komutasi campuran. Dua operator transfer yang bekerja pada site yang tidak saling beririsan komut secara trivial, karena masing-masing adalah hasil kali genap operator fermionik. Kasus yang menarik adalah site yang dipakai bersama. Di sana, keduanya komut jika panahnya mengalir melalui site itu (satu datang, satu pergi) dan antikomut jika bertabrakan (keduanya datang, atau keduanya pergi).
Aturan ini paling mudah dilihat lewat gambar. Menggambar vertex sebagai node dan tiap sebagai panah dari ke menghasilkan graf interaksi berarah, di sini untuk rantai empat site yang dipakai di seluruh panduan ini, yang membawa kedua orientasi untuk setiap ikatan:
Telusuri aturannya pada gambar. Lihat dan , misalnya. Satu panah tiba di site 2 dan yang lain pergi darinya, jadi fermion mengalir lurus melewatinya dan kedua operator komut. Sekarang lihat dan . Kedua panah menunjuk ke site 2. Mereka bertabrakan, dan keduanya antikomut.
Inilah yang memungkinkan flow set. Pilih sekumpulan panah yang membentuk jalur berarah, dan setiap pasangan entah bertemu kepala-ke-ekor atau tidak bertemu sama sekali, sehingga seluruh set komut, meskipun operator individualnya beririsan di site bersama. Karena itu, evolusi di bawah satu flow set tidak menimbulkan error Trotter, dan seperti ditunjukkan langkah 7, ini juga bisa dilakukan dengan depth circuit yang konstan.
1. Hamiltonian Fermi-Hubbard
Model Fermi-Hubbard 1D pada site (tanpa spin, batas terbuka) adalah
Mengikuti Pers. (9) dari Ref. [1], suku hopping-nya adalah . Interaksinya diagonal, jadi dinyatakan lewat operator vertex dengan :
Kedua bagian dibangun langsung sebagai TransferVertexOperator. Perhatikan bahwa disimpan sebagai entri diagonal (j, j):
>>> from collections import defaultdict
>>> from qiskit_fermions.operators import TransferVertexOperator
>>>
>>> def fermi_hubbard_1d(num_sites, tunneling, interaction):
... data: defaultdict[tuple[tuple[int, int], ...], complex] = defaultdict(complex)
... for j in range(num_sites - 1):
... k = j + 1
... # hopping: -t (a^dag_j a_k + a^dag_k a_j) = t (T_jk + T_kj)
... data[((j, k),)] += tunneling
... data[((k, j),)] += tunneling
... # interaction: U n_j n_k with n_j = (1 - V_j) / 2
... data[()] += interaction / 4
... data[((j, j),)] -= interaction / 4
... data[((k, k),)] -= interaction / 4
... data[((j, j), (k, k))] += interaction / 4
... return TransferVertexOperator.from_dict(data)
>>>
>>> num_sites = 4
>>> hamiltonian = fermi_hubbard_1d(num_sites, tunneling=1.0, interaction=2.0)
>>> print(format(hamiltonian))
1.500000e0 +0.000000e0j * ()
-5.000000e-1 +0.000000e0j * (V(0))
5.000000e-1 +0.000000e0j * (V(0) V(1))
1.000000e0 +0.000000e0j * (T(0,1))
1.000000e0 +0.000000e0j * (T(1,0))
-1.000000e0 +0.000000e0j * (V(1))
5.000000e-1 +0.000000e0j * (V(1) V(2))
1.000000e0 +0.000000e0j * (T(1,2))
1.000000e0 +0.000000e0j * (T(2,1))
-1.000000e0 +0.000000e0j * (V(2))
5.000000e-1 +0.000000e0j * (V(2) V(3))
1.000000e0 +0.000000e0j * (T(2,3))
1.000000e0 +0.000000e0j * (T(3,2))
-5.000000e-1 +0.000000e0j * (V(3))
Site interior mendapat koefisien pada karena muncul di dua ikatan, sedangkan site tepi hanya muncul di satu. Setiap suku yang tercantum di sini muncul di graf yang digambar di atas. Suku T(j,k) adalah panahnya, dan suku V(j) adalah node-nya.
2. Partisi ke dalam flow set
Selanjutnya, beri label pada tiap suku dengan flow set tempatnya berada. Atribut groups ada untuk tujuan ini. Gate Evolution atas operator yang dikelompokkan terdekomposisi menjadi satu Evolution per grup (langkah 6 memanfaatkan ini), sehingga pengelompokan yang diputuskan di sini pada level fermionik bertahan sampai ke circuit.
Untuk rantai 1D, dua flow set hopping adalah panah berorientasi timur () dan panah berorientasi barat (). Suku interaksi diagonal komut dengan semua yang diagonal dan membentuk grup ketiga:
>>> def flow_set_groups(operator):
... groups = []
... for terms, _ in operator.iter_terms():
... match terms:
... case [(left, right)] if right == left + 1:
... groups.append(0) # east-oriented: T_{j,j+1}
... case [(left, right)] if right == left - 1:
... groups.append(1) # west-oriented: T_{j+1,j}
... case _:
... groups.append(2) # diagonal interaction terms
... return groups
>>>
>>> hamiltonian.groups = flow_set_groups(hamiltonian)
>>> hamiltonian.num_groups()
3
>>> for index, group in enumerate(hamiltonian.split_out_groups()):
... print(index, sorted(terms for terms, _ in group.iter_terms()))
0 [[(0, 1)], [(1, 2)], [(2, 3)]]
1 [[(1, 0)], [(2, 1)], [(3, 2)]]
2 [[], [(0, 0)], [(0, 0), (1, 1)], [(1, 1)], [(1, 1), (2, 2)], [(2, 2)], [(2, 2), (3, 3)], [(3, 3)]]
Grup 0 dan 1 adalah dua flow set. Tiap set adalah jalur berarah sepanjang rantai, jadi berdasarkan sifat flow semua suku dalam satu grup komut.
Tiap grup juga harus Hermitian dengan sendirinya, karena product formula mengeksponensiasi grup secara terpisah dan hanya faktor Hermitian yang punya evolusi uniter. groups_are_hermitian() mengonfirmasinya:
>>> from qiskit_fermions.operators.terms.grouping import groups_are_hermitian
>>>
>>> groups_are_hermitian(hamiltonian)
[True, True, True]
Ini patut dicermati, karena dua grup hopping bersifat satu arah: grup 0 hanya berisi suku berorientasi timur dan grup 1 hanya yang berorientasi barat. Memecah Hamiltonian seperti itu biasanya merusak sifat Hermitian, karena membutuhkan pasangan konjugatnya di sampingnya. TransferVertexOperator menghindari masalah ini karena sudah Hermitian dengan sendirinya. Ia adalah hasil kali dua operator Majorana (lihat definisinya di langkah 1), bukan polos, sehingga citra Jordan-Wigner-nya membawa koefisien real, bukan imajiner. Memilih representasi operator ini karena itu yang membuat flow set satu arah menjadi faktor Trotter yang sah.
Label timur/barat yang dipakai di seluruh panduan ini bersifat geometris, dengan rantai digambar dari kiri ke kanan dan indeks mode bertambah ke arah itu; menunjuk ke timur dan menunjuk ke barat. Ref. [1] tidak konsisten secara internal soal penamaan ini (teks dan gambarnya tidak sejalan), jadi panduan ini sengaja tidak mengikuti teksnya, dan sebagai gantinya mempertahankan label dari gambar. Saat membaca silang dengan makalah, cocokkan flow set berdasarkan bobot Pauli-nya (1 dan 3 di bawah), bukan berdasarkan kata arah mata angin.
3. Tulis encoding kustom
Bagian IV B dari Ref. [1] mengklasifikasikan bentuk yang bisa diambil encoding fermion-ke-qubit lokal ketika dibatasi pada sebuah flow set. Jordan-Wigner biasa memakai qubit dan memetakan setiap suku hopping ke Pauli berbobot 2. Klasifikasi itu menunjukkan bahwa mengeluarkan qubit tambahan membeli struktur pada operator yang di-encode.
Encoding yang diimplementasikan di sini adalah konstruksi parity-delocalized dari Appendix B 2 (persamaan B4 dan B5), yang memakai satu qubit ancilla, jadi . Paritas fermionik didelokalisasi di sepasang qubit:
Indeks pada ekspresi terakhir memaksa adanya qubit tambahan. Ikatan terakhir rantai punya , jadi -nya jatuh pada qubit , satu melewati mode fermionik terakhir . Qubit di ujung itulah ancilla, dan itulah sebabnya , bukan . Jangkauan yang sama muncul pada , yang -nya berada di ancilla untuk .
Keuntungannya langsung terlihat; satu flow set utuh dipetakan menjadi jumlah Pauli berbobot 1.
Prefaktor ditentukan oleh normalisasi , dan tanda minus relatif antara kedua orientasi ditentukan oleh identitas hasil kali , yang dipenuhi oleh Pauli di atas. Salah di salah satunya menghasilkan operator yang mematuhi relasi komutasi tetapi tidak lagi merepresentasikan Hamiltonian yang sama, jadi kamu sebaiknya memverifikasi encoding secara numerik, seperti ditunjukkan di langkah 4.
Menulis encoding berarti menulis fungsi yang memetakan satu operator transfer umum ke sebuah string Pauli; map_transfer_vertex_generators() menangani iterasi atas suku dan komposisinya:
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_transfer_vertex_generators
>>>
>>> def flow_set_action(action, num_qubits):
... match action:
... case (j, k) if j == k:
... # vertex operator: V_j = Z_j Z_{j+1}
... return SparseObservable.from_sparse_list(
... [("ZZ", [j, j + 1], 1.0)], num_qubits=num_qubits
... )
... case (j, k) if k == j + 1:
... # east-oriented transfer operator: weight 1
... return SparseObservable.from_sparse_list(
... [("X", [k], -0.5)], num_qubits=num_qubits
... )
... case (j, k) if k == j - 1:
... # west-oriented transfer operator: weight 3
... return SparseObservable.from_sparse_list(
... [("ZXZ", [k, k + 1, k + 2], 0.5)], num_qubits=num_qubits
... )
... case _:
... raise ValueError(f"not a nearest-neighbor transfer operator: {action}")
>>>
>>> def flow_set_encoding(operator, num_qubits):
... return map_transfer_vertex_generators(
... operator,
... lambda action: flow_set_action(action, num_qubits),
... identity=lambda: SparseObservable.identity(num_qubits),
... compose=SparseObservable.compose,
... ).simplify()
MapperFnEvolutionSynthesis.mapper_fn mengharapkan signature (operator, num_qubits), dan tipe kembaliannya harus berupa SparseObservable, sehingga fungsi itu bisa langsung dipakai sebagai plugin transpiler. Lihat langkah 6.
Menerapkannya pada Hamiltonian dari langkah 1 menghasilkan operator ter-encode pada qubit:
>>> num_qubits = num_sites + 1
>>> encoded = flow_set_encoding(hamiltonian, num_qubits)
>>> for label, indices, coeff in sorted(encoded.to_sparse_list()):
... print(f"{label:3s} {str(indices):12s} {coeff.real:+.2f}")
[] +1.50
X [1] -0.50
X [2] -0.50
X [3] -0.50
ZXZ [0, 1, 2] +0.50
ZXZ [1, 2, 3] +0.50
ZXZ [2, 3, 4] +0.50
ZZ [0, 1] -0.50
ZZ [0, 2] +0.50
ZZ [1, 2] -1.00
ZZ [1, 3] +0.50
ZZ [2, 3] -1.00
ZZ [2, 4] +0.50
ZZ [3, 4] -0.50
Ada 14 suku, dalam tiga bentuk: hopping menjadi X dan ZXZ, interaksi menjadi ZZ, dan [] di awal adalah identitas, yang hanya menyumbang fase global. Perhatikan bahwa suku ZZ menjangkau tetangga terdekat dan tetangga terdekat berikutnya; membentang dari qubit sampai , yang merupakan harga dari mendelokalisasi paritas.
Mengelompokkan berdasarkan flow set memperlihatkan asimetri yang dibeli oleh konstruksi ini. Daftarkan bobot Pauli yang ada di tiap grup:
>>> for index, group in enumerate(hamiltonian.split_out_groups()):
... weights = {len(indices) for _, indices, _ in flow_set_encoding(group, num_qubits).to_sparse_list()}
... print(index, sorted(weights))
0 [1]
1 [3]
2 [0, 2]
Flow set berorientasi timur (grup 0) terdiri seluruhnya dari Pauli berbobot 1. Evolusi waktunya karena itu berupa satu lapis rotasi single-qubit dan tidak membutuhkan gate entangling, sedangkan di bawah Jordan-Wigner setiap suku hopping berbobot 2 dan membutuhkannya. Inilah trade-off ruang-waktu dari Ref. [1] dalam bentuk paling sederhana: satu qubit tambahan mengubah separuh Hamiltonian hopping menjadi rotasi single-qubit.
Flow set berorientasi barat (grup 1) membayarnya di bobot tiga, dan interaksi (grup 2) berada di bobot dua, di mana entri berbobot nol adalah identitas. Semua ini belum merupakan klaim tentang circuit; apa yang dihasilkan synthesis dari bobot-bobot ini adalah topik langkah 6 dan 7.
4. Verifikasi encoding
Encoding kustom hanya berguna jika setia (faithful), dan encoding yang ditulis tangan sebaiknya diperiksa. Karena encoding ini memakai qubit, ruang Hilbert qubit dua kali lebih besar daripada ruang fermionik, sehingga Hamiltonian ter-encode tidak bisa sama dengan Hamiltonian Jordan-Wigner suku demi suku. Keduanya dihubungkan oleh sebuah isometri.
Asal isometri: Di bawah Jordan-Wigner, sebuah basis state adalah string okupansi; qubit menyimpan . Encoding ini sebaliknya menyimpan paritas kumulatif. Definisikan
jadi bit mencatat paritas semua okupansi sampai dan termasuk site . Ada bit seperti itu untuk okupansi, dan dari situlah ancilla berasal. Okupansi dipulihkan sebagai selisih antara bit bertetangga, . Site yang terisi karena itu muncul sebagai domain wall di antara dua bit yang berdampingan, makanya disebut encoding domain-wall (atau Kramers-Wannier). Itu juga sebabnya : hasil kali dua bertetangga membaca selisih tersebut.
Dengan menuliskan peta ini untuk beberapa state, strukturnya jadi lebih jelas (perhatikan bahwa itu tetap, jadi hanya setengah dari state qubit yang ada di image, dan karena itu berupa isometri, bukan unitari):
>>> def domain_wall_bits(occupations):
... parity = 0
... bits = [0]
... for occupation in occupations:
... parity ^= occupation
... bits.append(parity)
... return bits
>>>
>>> for occupations in ([0, 0, 0, 0], [1, 0, 0, 0], [1, 1, 0, 0], [1, 0, 1, 0]):
... bits = domain_wall_bits(occupations)
... # little-endian: reversed() prints qubit 0 on the right, as Qiskit labels do
... print("".join(map(str, occupations)), "->", "".join(str(b) for b in reversed(bits)))
0000 -> 00000
1000 -> 11110
1100 -> 00010
1010 -> 00110
Satu fermion di site 0 membalik setiap paritas kumulatif di atasnya, dan inilah delokalisasi yang bekerja. Kalau kolom-kolom ini dikumpulkan jadi satu matriks, kita dapat isometri , dan encoding-nya dikatakan faithful kalau ia mengaitkan (intertwine) kedua Hamiltonian:
Artinya, memetakan state fermionik ke ruang qubit lalu mengevolusikannya di sana harus memberi hasil yang sama dengan mengevolusikannya lebih dulu di ruang fermionik lalu memetakannya sesudahnya.
>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp
>>> from qiskit_fermions.mappers.library import transfer_vertex_jordan_wigner
>>>
>>> def to_matrix(observable):
... # SparseObservable carries no dense-matrix method, so route through
... # SparsePauliOp. This is only ever needed for the checks below --
... # the encoding itself never leaves SparseObservable.
... return SparsePauliOp.from_sparse_observable(observable).to_matrix()
>>>
>>> def domain_wall_isometry(num_sites):
... isometry = np.zeros((2 ** (num_sites + 1), 2**num_sites), dtype=complex)
... for index in range(2**num_sites):
... occupations = [(index >> j) & 1 for j in range(num_sites)]
... bits = domain_wall_bits(occupations)
... isometry[sum(b << j for j, b in enumerate(bits)), index] = 1.0
... return isometry
>>>
>>> isometry = domain_wall_isometry(num_sites)
>>> np.allclose(isometry.conj().T @ isometry, np.eye(2**num_sites))
True
>>>
>>> def intertwines(operator):
... """Whether both mappings of `operator` agree through the isometry."""
... flow = to_matrix(flow_set_encoding(operator, num_qubits))
... reference = to_matrix(transfer_vertex_jordan_wigner(operator, num_sites).simplify())
... return np.allclose(flow @ isometry, isometry @ reference)
>>>
>>> intertwines(hamiltonian)
True
>>> # and, more strongly, once per flow set: a product formula evolves the groups
>>> # separately, so each one has to be intertwined on its own
>>> all(intertwines(group) for group in hamiltonian.split_out_groups())
True
Relasi intertwining ini berlaku persis, dan berlaku terpisah untuk tiap flow set. Pernyataan kedua yang lebih kuat itulah yang dipakai langkah-langkah berikutnya: product formula mengevolusikan grup satu per satu, dan hasil kali faktor-faktor yang intertwined itu sendiri juga intertwined, jadi aproksimasi apa pun yang dibangun dari grup-grup ini akan sama di antara kedua encoding, meski encoding-nya berbeda. Sebagai akibatnya, spektrumnya harus sama, dengan setiap eigenvalue Jordan-Wigner muncul dua kali di ruang yang lebih besar, sekali untuk tiap nilai paritas global yang tidak dibatasi:
>>> jw_matrix = to_matrix(transfer_vertex_jordan_wigner(hamiltonian, num_sites).simplify())
>>> flow_matrix = to_matrix(flow_set_encoding(hamiltonian, num_qubits))
>>>
>>> jw_spectrum = np.linalg.eigvalsh(jw_matrix)
>>> flow_spectrum = np.linalg.eigvalsh(flow_matrix)
>>> np.allclose(
... np.sort(flow_spectrum),
... np.sort(np.concatenate([jw_spectrum, jw_spectrum])),
... )
True
>>> print(f"{jw_spectrum[0]:.10f} {flow_spectrum[0]:.10f}")
-1.8557725066 -1.8557725066
5. Bangun fermionic circuit
Sebelum melibatkan qubit apa pun, dinamikanya dinyatakan di level fermionik. Sebuah FermionicCircuit bekerja pada mode fermionik, bukan qubit, dan sebuah gate Evolution atas Hamiltonian itu sudah menjadi seluruh circuit-nya:
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import FermionicLieTrotter
>>>
>>> total_time = 1.0
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(
... Evolution(
... num_sites, hamiltonian, time=total_time, synthesis=FermionicLieTrotter()
... ),
... circuit.modes,
... )
>>> circuit.draw("mpl")
<Figure size ... with 1 Axes>
Ada satu kotak buram untuk keempat mode, dan bagian menariknya adalah apa yang diuraikan darinya. Argumen synthesis menentukan caranya: FermionicLieTrotter adalah product formula orde pertama, yang memecah operator berdasarkan groups-nya dan mengevolusikan tiap grup secara bergantian. Jadi partisi flow set dari langkah 2 adalah Trotterisasinya, dan satu panggilan decompose() langsung melakukannya, menghasilkan satu Evolution per flow set sesuai urutan grup: east, west, lalu interaksi diagonal, masing-masing membawa 3, 3, dan 8 suku.
>>> flow_set_circuit = circuit.decompose()
>>> flow_set_circuit.draw("mpl")
<Figure size ... with 1 Axes>
Perhatikan bahwa semua ini terjadi sepenuhnya di ruang fermionik: belum ada qubit yang disebut, dan aproksimasinya sudah dibuat. Jumlah langkah Trotter karena itu juga ada di sini, sebagai reps, yang membagi waktu dan mengulang seluruh sapuan:
>>> def trotter_steps(reps):
... step = FermionicCircuit(num_sites)
... step.append(
... Evolution(
... num_sites,
... hamiltonian,
... time=total_time,
... synthesis=FermionicLieTrotter(reps=reps),
... ),
... step.modes,
... )
... return step.decompose()
>>>
>>> trotter_steps(3).count_ops()["Evolution"]
9
Sembilan gate: tiga flow set, diputar tiga kali dengan sepertiga waktu untuk masing-masing. Perputaran itulah yang menjadikannya Trotterisasi, bukan sekadar memotong ulang satu grup. Mengulang satu grup saja tidak akan memberi apa-apa, karena eksponensial berurutan dari operator yang sama saling komut, jadi A(t/3) A(t/3) A(t/3) sama saja dengan A(t). Formula yang membaca grup bisa menyisipkannya bergantian; formula yang diterapkan setelah pemetaan tidak bisa, karena pengelompokannya sudah hilang saat itu.
Inilah langkah yang membuat pengelompokan jadi penting. Setiap gate yang dihasilkan dipetakan dan disintesis secara independen, jadi semua yang ada di hilir bekerja berdasarkan partisi yang diputuskan di level fermionik. Kalau dekomposisi dilewati, seluruh operator diserahkan begitu saja ke tahap berikutnya, dan pengelompokannya diabaikan.
6. Transpile dengan encoding kustom
Sekarang encoding-nya siap dipakai oleh transpiler. Dua bagian lagi dibutuhkan:
-
Sebuah layout.
TrivialF2QLayoutyang default mengasumsikan satu qubit per mode, dan itu keliru untuk kasus ini.CustomF2QLayoutmengaitkan register fermionik dengan sebuahQuantumRegisterberukuran berbeda, dan lewat cara inilah qubit ancilla masuk ke pipeline. -
Sebuah synthesis plugin.
MapperFnEvolutionSynthesismenerima fungsi mapper secara langsung, memetakan Hamiltonian dari gateEvolutiondengannya, lalu menghasilkan sebuahPauliEvolutionGate, dengan konvensi tetap terjaga.product_formula-nya menentukan bagaimana gate itu diubah menjadi rotasi. Perhatikan bahwa ini adalah product formula kedua yang independen, bekerja pada qubit setelah pemetaan:LieTrottermilik Qiskit dipakai di sini, dan langkah 7 menggantinya dengan yang paham flow set.
Karena setiap perbandingan di bawah menjalankan ulang pipeline ini, ada baiknya dibungkus sekali saja:
>>> from qiskit.circuit import QuantumRegister
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG, QuantumDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import (
... CustomF2QLayout,
... F2QSynthesis,
... MapperFnEvolutionSynthesis,
... )
>>>
>>> def flow_set_pass_manager(mode_register, product_formula=None):
... '''Build the pipeline mapping `mode_register` with the flow-set encoding.'''
... synthesis = F2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... flow_set_encoding, product_formula=product_formula
... )
... # this encoding spends one ancilla on top of the fermionic modes
... qubit_register = QuantumRegister(mode_register.size + 1, "q")
... return MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... layout=CustomF2QLayout({mode_register: qubit_register}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>>
>>> from qiskit.synthesis import LieTrotter
>>>
>>> pass_manager = flow_set_pass_manager(flow_set_circuit.register, LieTrotter())
>>> qubit_circuit = pass_manager.run(flow_set_circuit)
>>> qubit_circuit.num_qubits
5
Empat mode fermionik telah menjadi circuit lima qubit, dengan ancilla yang disediakan oleh layout kustom.
Sekarang lihat apa isi circuit hasilnya. Dekomposisi berulang menurunkan gate evolusi sampai ke U dan CX, sehingga biaya dua-qubit dari kedua strategi sintesis yang dibandingkan di bawah terukur dengan satuan yang sama: count_ops() lalu melaporkan jumlah CX, dan depth() menerima fungsi filter untuk mengukur depth dua-qubit. Perhatikan bahwa reps ini adalah kedalaman dekomposisi, tidak ada hubungannya dengan jumlah langkah Trotter yang diambil product formula, yang di bagian selanjutnya panduan ini juga disebut reps:
>>> decomposed = qubit_circuit.decompose(reps=6)
>>> decomposed.count_ops()["cx"]
26
>>> decomposed.depth(lambda instruction: len(instruction.qubits) == 2)
23
Dua puluh enam gate CX dalam dua puluh tiga layer, jadi hampir tidak ada yang berjalan paralel. Menggambar circuit-nya menunjukkan alasannya:
>>> decomposed.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Setiap suku dikompilasi secara terpisah: perubahan basis, tangga CX turun ke satu rotasi, lalu tangganya dibatalkan. Flow set berbobot 1 itu gratis; suku-sukunya hanya rotasi tunggal tanpa tangga di sekitarnya, tetapi setiap suku berbobot 3 membayar empat gate CX, dan karena tangga-tangganya dihasilkan secara berurutan, tidak ada yang tumpang tindih.
Jadi pengelompokan bertahan sepanjang pipeline (tiga flow set, tiga PauliEvolutionGate yang dipetakan) dan tetap hampir tidak memberi keuntungan. Trotterisasi fermionik di langkah 5 sudah menjalankan tugasnya; yang menyia-nyiakannya adalah formula di sisi qubit. LieTrotter menerima sekumpulan suku tanpa diberi tahu apa-apa tentangnya, dan mengompilasi masing-masing secara terpisah. Ia tidak bisa tahu bahwa suku-suku itu saling komut, jadi ia menyerialkan apa yang sebenarnya bisa berbagi satu perubahan basis.
Itulah celah yang ingin ditutup encoding ini: ada dua product formula yang bekerja, dan sejauh ini hanya yang fermionik yang diberi tahu strukturnya. Langkah berikutnya menyediakan sintesis di sisi qubit yang juga memanfaatkannya.
7. Sintesis yang sadar flow set
Memulihkan keuntungan berarti memberi tahu sintesis di sisi qubit tentang strukturnya. Ini adalah Bagian IV C dari Ref. [1]: karena semua suku dalam satu flow set saling komut, suku-suku itu bisa didiagonalisasi sekaligus oleh satu circuit Clifford, dirotasi dalam satu basis bersama lalu dirotasi balik, bukan sekali per suku.
Untuk encoding ini, Clifford-nya sederhana. Melakukan konjugasi dengan rantai gate CZ pada tetangga terdekat memetakan
secara bersamaan untuk setiap . Satu rantai CZ mengubah seluruh flow set berbobot 3 (west) menjadi satu layer rotasi satu-qubit yang independen. Antarmuka EvolutionSynthesis hanya meminta metode synthesize(), dan hanya itu yang diimplementasikan kelas ini. Kelas ini tidak membawa jumlah langkah Trotter, karena ia menerima satu flow set pada satu waktu sehingga tidak bisa membagi evolusi ke seluruh set. Pembagian itu ada satu level di atas, di ruang fermionik, tempat sebuah F2FSynthesisPlugin melihat semuanya sekaligus (lihat langkah 9):
>>> from qiskit.circuit import QuantumCircuit
>>> from qiskit.synthesis.evolution import EvolutionSynthesis
>>>
>>> def cz_brickwork(circuit):
... # emit even and odd bonds as two layers, so the CZs are scheduled in
... # parallel: a chain of overlapping CZs has depth 2, not num_qubits - 1
... #
... # NOTE: CZ gates are diagonal, so they all commute and each is its own inverse.
... # This whole layer is therefore self-inverse, which is why the conjugation below
... # calls it twice rather than calling it and then its inverse.
... for offset in (0, 1):
... for j in range(offset, circuit.num_qubits - 1, 2):
... circuit.cz(j, j + 1)
>>>
>>> # The interaction reaches nearest (distance 1) and next-nearest (distance 2)
>>> # neighbors, so every interior qubit carries four Rzz gates and can only do one at
>>> # a time: four layers is the best any schedule can do. These offsets achieve it.
>>> def rzz_layers(circuit, angles):
... """Emit the diagonal Rzz gates in four layers of disjoint qubit pairs."""
... near = [(j, k) for j, k in angles if k - j == 1]
... far = [(j, k) for j, k in angles if k - j == 2]
... schedule = [
... [(j, k) for j, k in near if j % 2 == 0],
... [(j, k) for j, k in far if j % 4 in (0, 1)],
... [(j, k) for j, k in near if j % 2 == 1],
... [(j, k) for j, k in far if j % 4 in (2, 3)],
... ]
... # emit layer by layer, since Qiskit's depth follows the order gates were added
... for layer in schedule:
... for j, k in layer:
... circuit.rzz(angles[j, k], j, k)
>>>
>>> class FlowSetSynthesis(EvolutionSynthesis):
... '''Synthesize one flow set of the encoded Hamiltonian at a time.'''
...
... preserve_order = True
...
... def synthesize(self, evolution):
... circuit = QuantumCircuit(evolution.operator.num_qubits)
... east, west, diagonal = [], [], {}
...
... for label, indices, coeff in evolution.operator.to_sparse_list():
... angle = 2 * evolution.time * coeff.real
... match label, indices:
... case "", _: # the identity contributes only a global phase
... circuit.global_phase -= angle / 2
... case "X", [qubit]: # east flow set: already weight-1
... east.append((qubit, angle))
... case "ZXZ", [j, k, l] if (k, l) == (j + 1, j + 2):
... west.append((k, angle)) # needs the CZ conjugation below
... case "ZZ", [j, k]: # interaction: diagonal, see rzz_layers above
... diagonal[min(j, k), max(j, k)] = angle
... case _:
... raise ValueError(f"unexpected Pauli term: {label} on {indices}")
...
... # east flow set: bare rotations, no entangling gates at all
... for qubit, angle in east:
... circuit.rx(angle, qubit)
...
... # west flow set: one CZ chain diagonalizes the WHOLE set at once
... if west:
... cz_brickwork(circuit)
... for qubit, angle in west:
... circuit.rx(angle, qubit)
... cz_brickwork(circuit)
...
... if diagonal:
... rzz_layers(circuit, diagonal)
...
... return circuit
Dua detail gampang terlewat. Sebuah EvolutionSynthesis kustom harus menyediakan atribut preserve_order, yang dibaca oleh pass high-level-synthesis.
Kedua helper menghasilkan gate-nya dalam layer eksplisit, bukan sesuai urutan suku, karena depth() menjadwalkan gate secepat mungkin, sesuai urutan penambahannya. Karena semua gate dalam satu set ini saling komut, urutannya bebas dipilih. Sintesis suku demi suku memilih urutan yang buruk. Menghasilkan rantai CZ yang tumpang tindih secara berurutan akan melaporkan depth yang bertambah seiring jumlah qubit, bukan 2 konstan yang bisa dicapai hardware.
Inilah sebabnya sintesis di atas tidak membawa jumlah langkah Trotter. Ia menerima satu flow set per gate, sehingga pengulangan di dalamnya akan mengulang di dalam satu set, bukan berputar di antara set-set itu:
repeating within one set: A(t/2) A(t/2) B(t/2) B(t/2)
what a Trotterization needs: A(t/2) B(t/2) A(t/2) B(t/2)
Bentuk pertama tidak memberi apa-apa: eksponensial berurutan dari operator yang sama saling komut, jadi A(t/2) A(t/2) sama saja dengan A(t) dan errornya tetap di nilai satu langkah, berapa pun langkah yang diminta.
Jumlah langkah karena itu milik FermionicLieTrotter fermionik dari langkah 5, yang membaca groups dan bisa menyisipkannya bergantian. Pembagian tugas itulah intinya: formula fermionik menentukan berapa langkah dan dalam urutan apa, sedangkan kelas ini menentukan seberapa murah tiap set diwujudkan.
Untuk memilih urutannya, berikan ke MapperFnEvolutionSynthesis.product_formula, yang merupakan argumen yang diterima flow_set_pass_manager. Tidak ada hal lain dari pipeline (atau fermionic circuit tempat ia berjalan) yang berubah:
>>> pass_manager = flow_set_pass_manager(flow_set_circuit.register, FlowSetSynthesis())
>>>
>>> flow_circuit = pass_manager.run(flow_set_circuit).decompose(reps=6)
>>> flow_circuit.count_ops()["cx"]
22
>>> flow_circuit.depth(lambda instruction: len(instruction.qubits) == 2)
12
>>> flow_circuit.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Dari depth 23 dengan 26 gate CX turun ke depth 12 dengan 22 gate CX. Perhatikan bahwa ini berarti gate entangling lebih sedikit dan depth hampir setengahnya. Kalau membandingkan kedua gambar, bedanya bukan karena gate-nya lebih murah, tetapi karena gate-nya bertumpuk. Tangga serial sudah hilang; gate entangling sekarang berada di atas satu sama lain dalam layer bersama karena seluruh flow set west dirotasi dalam satu basis bersama dan interaksinya dihasilkan dalam layer pasangan yang saling lepas.
FlowSetSynthesis melakukan pencocokan pola pada indeks qubit absolut, yang aman di sini hanya karena setiap flow set dari Hamiltonian ini mencakup seluruh register. Product formula fermionik mempersempit tiap faktor ke mode yang benar-benar disentuhnya, jadi pengelompokan yang set-setnya hanya mencakup sebagian rantai akan datang dengan label yang sudah dinomori ulang dan pola di atas tidak akan cocok.
Kedua gambar menunjukkan circuit setelah decompose(reps=6), yang menulis ulang semuanya ke basis U dan CX, sehingga gate CZ, Rx, dan Rzz yang dihasilkan oleh FlowSetSynthesis tidak terlihat sebagai gate-gate itu. Ini disengaja: cara ini menempatkan kedua circuit dalam basis yang sama, satu-satunya cara agar jumlah dan depth dua-qubit di atas bisa dibandingkan. Yang patut diperhatikan dalam gambar-gambar ini adalah struktur layer-nya, bukan label gate-nya.
8. Depth konstan pada skala besar
Pertanyaan menariknya bukan jumlah di empat mode, tetapi bagaimana ia berskala. Dua layer brickwork CZ tidak bertambah seiring panjang rantai, jadi depth dua-qubit satu langkah Trotter seharusnya konstan. Mengukurnya langsung pada Hamiltonian Fermi-Hubbard penuh, jauh melampaui apa yang bisa disimulasikan dengan brute force:
>>> def evolution_stats(num_sites, product_formula):
... operator = fermi_hubbard_1d(num_sites, tunneling=1.0, interaction=2.0)
... operator.groups = flow_set_groups(operator)
...
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(
... num_sites, operator, time=total_time, synthesis=FermionicLieTrotter()
... ),
... circuit.modes,
... )
... circuit = circuit.decompose() # one Evolution per flow set, as in step 5
...
... pass_manager = flow_set_pass_manager(circuit.register, product_formula)
... decomposed = pass_manager.run(circuit).decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return decomposed.count_ops()["cx"], decomposed.depth(two_qubit)
>>>
>>> sites = [4, 10, 20, 50, 100]
>>> flow = [evolution_stats(n, FlowSetSynthesis()) for n in sites]
>>> lie_trotter = [evolution_stats(n, LieTrotter()) for n in sites]
>>>
>>> [depth for _, depth in flow]
[12, 12, 12, 12, 12]
>>> [depth for _, depth in lie_trotter]
[23, 47, 87, 207, 407]
Depth dua-qubit tetap 12 dari empat mode sampai 100, sedangkan LieTrotter tumbuh linear sampai 407. Flow set membuatnya bisa sedikit diparalelkan di dalam pipeline, tetapi tiap set tetap di-Trotterisasi suku demi suku, jadi pertumbuhannya hanya diperlambat, bukan dihilangkan:
>>> import matplotlib.pyplot as plt
>>>
>>> figure, axes = plt.subplots(1, 2, figsize=(9, 3.5), layout="constrained")
>>> for panel, index, title in zip(axes, (1, 0), ("two-qubit depth", "CX count")):
... _ = panel.plot(sites, [stat[index] for stat in lie_trotter], "o-", label="qubit-level")
... _ = panel.plot(sites, [stat[index] for stat in flow], "s-", label="flow set")
... _ = panel.set(xlabel="modes", title=title)
... _ = panel.legend()
>>> figure
<Figure size ... with 2 Axes>
Jumlah gate tumbuh linear pada keduanya (ada suku yang harus diterapkan, dan itu tak terhindarkan), tetapi sintesis flow set menerapkannya dalam jumlah layer paralel yang konstan. Inilah hasil depth konstan dari Ref. [1].
Depth 12 terbagi ke tiga flow set sebagai 0 + 4 + 8, pada setiap panjang rantai. Flow set east menyumbang nol; isinya hanya rotasi Rx murni. Flow set west berbiaya 4, dari dua layer brickwork CZ yang masuk dan dua yang keluar; kalau interaksinya dibuang, depth dua-qubit-nya empat pada panjang rantai berapa pun. Sisa delapan adalah interaksi yang suku Rzz-nya bekerja pada tetangga terdekat maupun tetangga terdekat kedua sehingga butuh empat layer yang saling lepas, masing-masing berbiaya dua gate CX. Empat itu optimal di sini: setiap qubit interior membawa empat gate Rzz (dua ke tiap tetangga dan dua ke tiap tetangga kedua) dan hanya bisa ikut serta dalam satu pada satu waktu, jadi tidak ada jadwal yang bisa lebih baik.
Dua pertiga depth adalah interaksi, yang menunjukkan bahwa encoding ini unggul pada hopping, dan karena itu pada dinamika rantai fermion bebas. Namun, perhatikan bahwa interaksi tidak komut dengan hopping. Membuangnya bukan aproksimasi untuk Fermi-Hubbard, melainkan model yang berbeda. Di mana keseimbangannya jatuh untuk suatu masalah patut dieksplorasi.
9. Periksa dinamikanya
Di bawah peta domain-wall, pola okupansi menjadi string paritas kumulatif, jadi state awalnya disiapkan secara berbeda dari di Jordan-Wigner. Ini konsekuensi praktis yang mudah terlewat dari delokalisasi paritas. Helper dari langkah 4 memberikan label yang benar:
>>> occupations = [1, 0, 1, 0]
>>> initial_label = "".join(str(b) for b in reversed(domain_wall_bits(occupations)))
>>> initial_label
'00110'
Dengan state yang sudah disiapkan, tersisa dua pertanyaan, dan paling baik ditanyakan terpisah: apakah sintesis flow set menghitung hal yang benar, dan apakah ia menghitungnya dengan lebih murah?
Mari mulai dari kebenaran. FlowSetSynthesis dan LieTrotter milik Qiskit menerima flow set ter-encode yang sama dari Trotterisasi fermionik yang sama, jadi keduanya harus mewujudkan unitari yang sama hingga fase global. Clifford kustom hanya menata ulang pekerjaannya; hasilnya tidak berubah. Statevector acak mendeteksi ketidaksesuaian apa pun jauh lebih murah daripada membangun unitari penuh:
>>> from qiskit.quantum_info import Statevector
>>>
>>> rng = np.random.default_rng(1234)
>>>
>>> def agrees_up_to_one_phase(reps, samples=3):
... """Compare both qubit-side syntheses of the same fermionic Trotterization."""
... step = trotter_steps(reps)
... trotter = flow_set_pass_manager(step.register, LieTrotter()).run(step)
... flow = flow_set_pass_manager(step.register, FlowSetSynthesis()).run(step)
... trotter, flow = trotter.decompose(reps=6), flow.decompose(reps=6)
... dimension = 2**trotter.num_qubits
... residuals, phases = [], []
... for _ in range(samples):
... vector = rng.normal(size=dimension) + 1j * rng.normal(size=dimension)
... state = Statevector(vector / np.linalg.norm(vector))
... a = state.evolve(trotter).data
... b = state.evolve(flow).data
... overlap = np.vdot(a, b)
... phases.append(overlap / abs(overlap))
... residuals.append(np.linalg.norm(b - a * overlap / abs(overlap)))
... # the SAME global phase must work for every input state
... spread = max(abs(phase - phases[0]) for phase in phases)
... return bool(max(residuals) < 1e-9), bool(spread < 1e-9)
>>>
>>> [agrees_up_to_one_phase(reps) for reps in (1, 3)]
[(True, True), (True, True)]
Kedua bagian dari pemeriksaan itu penting. Overlap bermodulus satu pada satu state itu perlu tetapi belum cukup: sintesis yang salah di fase yang bergantung pada state tetap akan terlihat. Mensyaratkan fase yang sama di beberapa state acak itulah yang menutup celahnya. Memeriksa lebih dari satu reps juga penting, karena itu memastikan kesesuaiannya bertahan melewati penyisipan flow set, bukan hanya berlaku untuk satu sapuan.
Jadi pilihan sintesis di sisi qubit tidak berpengaruh pada akurasi, dan itulah yang membuat perbandingan biaya bermakna. Pertanyaan yang tersisa adalah apa yang didapat dari encoding secara keseluruhan dibanding alternatif yang jelas: Jordan-Wigner biasa pada num_sites qubit, tanpa ancilla dan tanpa Clifford kustom. Baik encoding maupun sintesis di sisi qubit berbeda di antara kedua kolom, tetapi pemeriksaan di atas menunjukkan yang terakhir tidak bisa memengaruhi hasil, jadi yang diuji fidelity itu adalah encoding-nya. Kedua pipeline juga mulai dari Trotterisasi fermionik yang sama, jadi perbedaan fidelity apa pun akan menandakan bug, bukan trade-off. Kedua kolom itu adalah encoding, flow-set melawan Jordan-Wigner:
>>> def jordan_wigner_pass_manager(mode_register, product_formula):
... """The same pipeline under plain Jordan-Wigner: one qubit per mode."""
... synthesis = F2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... transfer_vertex_jordan_wigner, product_formula=product_formula
... )
... qubit_register = QuantumRegister(mode_register.size, "q")
... return MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... layout=CustomF2QLayout({mode_register: qubit_register}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>>
>>> from scipy.linalg import expm
>>>
>>> jw_state = np.zeros(2**num_sites, dtype=complex)
>>> jw_state[sum(o << j for j, o in enumerate(occupations))] = 1.0
>>> reference_jw = expm(-1j * total_time * jw_matrix) @ jw_state
>>> reference_flow = expm(-1j * total_time * flow_matrix) @ (isometry @ jw_state)
>>>
>>> def cost_and_fidelity(reps):
... step = trotter_steps(reps)
... flow = flow_set_pass_manager(step.register, FlowSetSynthesis()).run(step)
... jw = jordan_wigner_pass_manager(step.register, LieTrotter()).run(step)
... flow, jw = flow.decompose(reps=6), jw.decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... evolved_flow = Statevector.from_label(initial_label).evolve(flow).data
... evolved_jw = Statevector(jw_state).evolve(jw).data
... return (
... abs(np.vdot(evolved_flow, reference_flow)) ** 2,
... flow.count_ops()["cx"],
... flow.depth(two_qubit),
... abs(np.vdot(evolved_jw, reference_jw)) ** 2,
... jw.count_ops()["cx"],
... jw.depth(two_qubit),
... )
>>>
>>> for reps in (1, 2, 4, 8):
... flow_f, flow_cx, flow_d, jw_f, jw_cx, jw_d = cost_and_fidelity(reps)
... print(
... f"{reps} step(s): flow set {flow_f:.6f}"
... f" ({flow_cx:3d} CX, depth {flow_d:3d})"
... f" JW {jw_f:.6f} ({jw_cx:3d} CX, depth {jw_d:3d})"
... )
1 step(s): flow set 0.590661 ( 22 CX, depth 12) JW 0.590661 ( 18 CX, depth 14)
2 step(s): flow set 0.926656 ( 44 CX, depth 24) JW 0.926656 ( 36 CX, depth 26)
4 step(s): flow set 0.982053 ( 88 CX, depth 48) JW 0.982053 ( 72 CX, depth 50)
8 step(s): flow set 0.995395 (176 CX, depth 96) JW 0.995395 (144 CX, depth 98)
Baca dulu kolom fidelity: identik, sampai setiap digit yang dicetak, di dua ruang Hilbert berbeda dengan dua encoding berbeda. Itulah Trotterisasi fermionik yang terlihat. Aproksimasinya ditetapkan di langkah 5, sebelum salah satu encoding dipilih, jadi kedua circuit membawa error yang persis sama dari formula orde pertama atas tiga flow set, dan error itu turun sekitar empat kali lipat untuk setiap penggandaan jumlah langkah.
Yang berbeda antara kedua encoding adalah biayanya, dan trade-off-nya layak dinyatakan terus terang. Flow set mengeluarkan gate entangling lebih banyak: 22 lawan 18 pada satu langkah. Ancilla bukan penyebabnya, karena ia tidak membawa gate dua-qubit sendiri. Penyebabnya adalah interaksi, persis seperti yang diperingatkan langkah 3: delokalisasi paritas mengubah tiga suku ZZ tetangga terdekat menjadi tujuh suku yang mencakup tetangga terdekat dan tetangga terdekat kedua, dan masing-masing berbiaya dua gate CX. Itulah harga yang dibayar agar hopping jadi murah.
Keunggulan di depth juga hanya tipis pada ukuran ini (12 lawan 14), karena empat mode terlalu kecil untuk memperlihatkan asimtotiknya. Intinya ada pada skala yang diukur di langkah 8: depth 12 itu konstan, sementara depth Jordan-Wigner tumbuh seiring rantai. Pada 14 mode sudah 12 lawan 34.
Panggilan decompose() di atas itu penting, karena dua alasan yang independen.
evolve() dan Operator memakai definisi eksak dari sebuah PauliEvolutionGate, melewati sintesis kustom di sisi qubit. Memverifikasi terhadap circuit yang tidak didekomposisi akan melaporkan fidelity sempurna bahkan untuk sintesis yang sengaja dibuat salah.
Hal yang sama berlaku satu level di atas, untuk sintesis fermionik. Sebuah gate Evolution yang tidak pernah didekomposisi masuk utuh ke tahap fermion-ke-qubit, yang memetakannya tanpa merujuk Evolution.synthesis sama sekali, sehingga reps diam-diam tidak akan berpengaruh. trotter_steps dari langkah 5 melakukan dekomposisi itu; pipeline yang menetapkan metodenya dengan F2FSynthesis mendapat ekspansi yang sama dari pass tersebut.
Sebagai pemeriksaan silang terakhir, densitas site yang dihitung di ruang ter-encode harus mereproduksi hasil Jordan-Wigner. Pemeriksaan ini menyangkut encoding, bukan circuit, jadi tidak ada aproksimasi Trotter yang masuk. Kedua evolusi waktu dihitung secara eksak, dengan eksponensiasi matriks dari masing-masing Hamiltonian. Ketidaksesuaian apa pun karena itu akan menunjuk ke encoding, bukan ke product formula:
>>> time = 0.8
>>> state_jw = np.zeros(2**num_sites, dtype=complex)
>>> state_jw[sum(o << j for j, o in enumerate(occupations))] = 1.0
>>> evolved_jw = expm(-1j * time * jw_matrix) @ state_jw
>>> evolved_flow = expm(-1j * time * flow_matrix) @ (isometry @ state_jw)
>>>
>>> for j in range(num_sites):
... v_flow = to_matrix(
... SparseObservable.from_sparse_list([("ZZ", [j, j + 1], 1.0)], num_qubits)
... )
... v_jw = to_matrix(SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_sites))
... density_flow = (1 - np.vdot(evolved_flow, v_flow @ evolved_flow).real) / 2
... density_jw = (1 - np.vdot(evolved_jw, v_jw @ evolved_jw).real) / 2
... print(f"site {j}: {density_flow:.10f} {density_jw:.10f}")
site 0: 0.6507935456 0.6507935456
site 1: 0.5963201678 0.5963201678
site 2: 0.3365016619 0.3365016619
site 3: 0.4163846247 0.4163846247
Densitasnya cocok sampai sepuluh angka desimal.
Yang perlu dibawa pulang
-
Kelompokkan sebelum memetakan. Flow set didefinisikan pada graf interaksi fermionik berarah, jadi pengelompokan dipilih selagi struktur masalah masih tersedia. Atribut
groupsmembawanya ke dalam circuit, tempat satudecompose()mengubahnya menjadi satu gateEvolutionper flow set. -
Ancilla membeli struktur. Mengeluarkan satu qubit tambahan mengubah seluruh flow set menjadi Pauli berbobot 1, yang evolusinya tidak butuh gate entangling. Ref. [1] mengamati trade-off ini secara umum. Rasio qubit-ke-fermion yang lebih besar memungkinkan circuit evolusi yang lebih dangkal.
-
Encoding kustom itu satu fungsi. Petakan satu operator transfer umum ke satu string Pauli;
map_transfer_vertex_generators()danMapperFnEvolutionSynthesismengerjakan sisanya. -
Pemetaan dan pengelompokan saja tidak cukup. Memecah evolusi per flow set hanya memperlambat pertumbuhan linear depth; sintesis suku demi suku untuk tiap set tetap membuang fakta bahwa suku-sukunya saling komut. Hanya dengan menyediakan
EvolutionSynthesisyang sadar flow set, depth menjadi konstan. Encoding kustom, pengelompokan, dan sintesis kustom saling melengkapi: ketiganya dibutuhkan. -
Dua product formula, dua tugas.
FermionicLieTrottermenetapkan aproksimasi di ruang fermionik, tempat pengelompokan masih terlihat, jadi hanya ia yang menentukan error Trotter dan jumlah langkah.EvolutionSynthesisdi sisi qubit lalu hanya menentukan seberapa murah tiap set diwujudkan: langkah 9 mengonfirmasi bahwa kedua pilihan memberi unitari yang sama. Memisahkan keduanya membuat akurasi dan biaya bisa diatur secara independen. Lihat Sintesis gate Evolution di ruang fermionik. -
Verifikasi. Encoding yang memenuhi relasi komutasi mungkin tetap merepresentasikan Hamiltonian yang berbeda kalau ada prefaktor atau tanda yang meleset. Memeriksa relasi intertwining (atau, yang lebih murah, spektrumnya) akan menangkap hal ini.
FlowSetSynthesis di atas sengaja ditulis untuk encoding ini pada rantai 1D. Ia mencocokkan pola label Pauli spesifik yang dihasilkan encoding dan memunculkan error untuk yang lain, bukan diam-diam kembali ke product formula generik. Implementasi umum punya dua bagian. Membaca struktur flow set dari groups milik operator adalah hal yang sudah dilakukan sebuah F2FSynthesisPlugin untuk sebuah evolusi. Menurunkan Clifford yang mendiagonalisasi dari grup stabilizer, seperti dijelaskan di Bagian IV C Ref. [1], adalah bagian yang tetap spesifik untuk encoding.
Selain itu, delokalisasi paritas membuat suku interaksi berbobot 2, bukan diagonal pada qubit tunggal, yang merupakan biaya pasangan dari suku hopping yang lebih murah.
Berikutnya: dua dimensi
Semua di atas bersifat satu dimensi, dan memang disengaja. Pada sebuah rantai, flow set hanyalah dua orientasi panah, dan encoding-nya butuh satu ancilla saja. Kedua penyederhanaan itu tidak bertahan di dua dimensi, tempat encoding lokal dibutuhkan.
Pada kisi planar, sebuah flow set adalah lintasan berarah (atau gabungan lintasan berarah yang saling lepas titik-sudutnya) melalui graf interaksi, dan mencari himpunan kecil lintasan semacam itu yang menutupi setiap sisi menjadi masalah kombinatorial tersendiri, bukan klasifikasi dua baris. Encoding-nya juga berubah karakter; alih-alih satu ancilla untuk seluruh rantai, mereka menambah satu per site atau sel kisi. Verstraete-Cirac memasangkan setiap site fermionik dengan qubit bantu miliknya sendiri, rasio qubit-ke-mode sebesar , sedangkan Derby-Klassen menempatkan satu di pusat setiap plaquette ganjil, dengan rasio . Tata letak register lalu punya struktur dua dimensi yang sesungguhnya, dan grup stabilizer yang harus diproyeksikan tidak lagi trivial. Hamiltonian ter-encode hanya faithful pada sebuah subruang, yang mengubah persiapan state maupun resep verifikasi dari langkah 4.
Bagian-bagian yang dibangun panduan ini adalah yang terbawa ke sana: CustomF2QLayout untuk register yang ukurannya berbeda dari jumlah mode, map_transfer_vertex_generators() untuk encoding itu sendiri, MapperFnEvolutionSynthesis untuk menyambungkannya ke transpiler, dan EvolutionSynthesis kustom agar struktur flow set membuahkan hasil. Hanya encoding dan dekomposisi flow set yang berbeda.
Panduan flow set 2D melanjutkan dari sini, mengerjakan encoding Verstraete-Cirac pada kisi persegi: empat flow set, Clifford berdepth 2 untuk masing-masing, dan proyeksi stabilizer yang kemudian dibutuhkan verifikasi.
A. Gandon, S. Piccinelli, M. Rossmannek, F. Tacchino, A. Baiardi, J. Nys, and I. Tavernelli, Stabilizer-based quantum simulation of fermion dynamics with local qubit encodings (2026), arXiv:2512.11418v2.

