Simulasikan dinamika Fermi-Hubbard 2D dengan flow set
Konsep dalam panduan ini saat ini hanya tersedia di Python API. Fungsionalitas yang setara akan tersedia di C API pada rilis mendatang.
Baca dulu panduan flow set 1D. Panduan ini adalah lanjutan dua dimensinya dan tidak mengulang informasi yang sudah dikembangkan di sana: operator transfer dan vertex, apa itu flow set, bagaimana groups bertahan sampai ke circuit, dan bagaimana encoding kustom disambungkan ke transpiler lewat MapperFnEvolutionSynthesis.
Lihat panduan transpilation untuk tahap-tahap transpiler yang dirujuk sepanjang panduan ini, dan panduan mapper untuk resep umum menulis mapper kustom.
Panduan 1D diakhiri dengan menyebut bahwa encoding lokal dibutuhkan saat bekerja dengan dua dimensi. Panduan ini mengambil langkah itu, untuk model Fermi-Hubbard dua dimensi pada kisi persegi, memakai encoding Verstraete-Cirac (VC) [2].
Ada tiga perbedaan dari masalah 1D, yang menjadi dasar pekerjaan ini:
-
Flow set bertambah banyak. Sebuah rantai punya dua orientasi panah; kisi persegi punya empat (east, west, north, south), dan masing-masing adalah gabungan lintasan berarah yang saling lepas titik-sudutnya (satu per baris atau kolom kisi), bukan satu lintasan tunggal.
-
Encoding mendapat satu ancilla per site, bukan satu untuk seluruh sistem, sehingga menghasilkan
rasio qubit-ke-mode sebesar . Operator transfer menjadi Pauli berbobot 3 (horizontal) dan berbobot 4 (vertikal), dan tiap dari empat flow set dipetakan ke bentuk Pauli-nya sendiri, jadi masing-masing butuh Clifford pendiagonal sendiri.
-
Hamiltonian ter-encode hanya faithful pada sebuah subruang: eigenspace gabungan
dari batasan plaquette yang dibawa setiap encoding lokal 2D [2] [3]. Di 1D, encoding-nya adalah isometri biasa dan verifikasinya berupa pemeriksaan spektrum. Di sini, perbandingannya harus diproyeksikan, seperti diilustrasikan langkah 9.
Hasilnya berbentuk sama seperti di 1D: depth dua-qubit satu langkah Trotter menjadi konstan terhadap ukuran kisi, di ( untuk hopping, sesuai Ref. [1], ditambah untuk interaksi), sementara Trotterisasi suku demi suku dari Hamiltonian ter-encode yang sama tumbuh linear. Jumlah gate tentu saja tetap tumbuh seiring jumlah ikatan; yang menjadi tidak bergantung pada ukuran adalah berapa banyak layer yang ditempati gate-gate itu. Depth di seluruh bagian dihitung pada register abstrak yang terhubung penuh; memetakan encoding ke coupling map perangkat yang terbatas adalah masalah tersendiri, yang dibahas di bagian akhir.
Kisi dan empat orientasinya
Perubahan pertama terlihat pada graf interaksi berarah. Site dinomori dalam urutan row-major, site(r, c) = r * cols + c, dan setiap ikatan membawa kedua orientasi, seperti pada rantai di 1D, tetapi sekarang ikatannya berjalan sepanjang dua sumbu:

Aturan komutasinya sama dengan di 1D, tidak berubah: dua operator transfer yang berbagi satu site komut ketika panah mengalir melaluinya dan antikomut ketika bertabrakan. Namun, sebuah site sekarang punya hingga empat ikatan, bukan dua, jadi panah yang melaluinya bisa bertabrakan dengan lebih banyak cara, dan himpunan panah yang tidak pernah bertabrakan tidak lagi berupa satu lintasan. Membaca panah horizontal dari kiri ke kanan memberi satu himpunan semacam itu, panah vertikal dari atas ke bawah memberi satu lagi, dan membalik masing-masing memberi dua lagi. Inilah flow set east, west, north, dan south yang mengorganisasi semua yang menyusul.
Encoding Verstraete-Cirac
Verstraete-Cirac (VC) memasangkan setiap mode fermionik dengan qubit bantunya sendiri. Dengan site kisi, mode berada di qubit dan ancilla-nya di qubit , sehingga register berisi qubit. Operator vertex tetap sesederhana mungkin,
yang membuat okupansi bisa dibaca langsung nanti. Operator transfer bergantung pada orientasi sisi. Untuk sisi horizontal dengan sebagai site barat, dan sisi vertikal dengan sebagai site utara,
sebuah “segitiga” berbobot 3 dan sebuah “persegi” berbobot 4. Mengidentifikasi setiap panah itu penting: dengan sebagai site barat, menunjuk ke timur, tetapi dengan sebagai site utara, ia menunjuk ke selatan. Orientasi sebaliknya mengikuti dari identitas , yang mengubah segitiga XXZ menjadi YYZ, dan persegi XYXY menjadi YXXY. Keempat label berbeda itulah keempat flow set, dan sisa panduan ini melacaknya.
Ini tidak persis sama dengan Pers. (A1) dari Ref. [1], yang menyatakan “hingga rotasi single-qubit” dan berbeda dalam tiga hal yang perlu disebutkan, karena pembaca yang mencocokkan dengan makalahnya pasti akan menyadarinya:
-
Prefaktor: Pers. (A1) tidak punya prefaktor. di sini membuat , sesuai dengan normalisasi yang dipakai oleh
TransferVertexOperator. Makalah itu hanya membutuhkan operator transfernya di dalam , di mana faktor skala terserap ke dalam sudut rotasi; di sini operator tersebut harus berupa operator yang memenuhi suatu identitas. -
Orientasi: Pers. (A1) memilih huruf berdasarkan paritas baris , yang untuk sisi horizontal sama untuk kedua ujungnya, jadi tidak membedakan dari . Makalah itu memulihkan orientasi secara terpisah, dengan melakukan konjugasi memakai satu layer . Panduan ini justru memasukkannya ke dalam encoding, dengan menurunkan kebalikannya dari , yang membuat kedua orientasi jatuh ke Pauli yang berbeda di langkah 3.
-
Bentuk vertikal: Pers. (A1) bergantian antara
XXXXdanYYYYmenurut paritas baris, sedangkan panduan ini memakai satuXYXY. Keduanya adalah dua cara untuk mendapatkan hal yang sama: asimetri huruf yang membuat sisi vertikal berbenturan dengan benar terhadap sisi horizontal yang bertemu dengannya. Jangan anggap perbedaan ini sebagai pilihan basis yang bebas. Menghilangkan pergantian paritas dan memakai satuXXXXsaja langsung merusak aljabarnya, makanya huruf-huruf yang berbeda itu diperiksa, bukan sekadar diasumsikan.
Ancilla dipasang pada sebuah site, sehingga rasionya ; encoding Derby-Klassen-lah yang menaruh satu qubit tambahan per plakuet (ganjil), untuk rasio yang lebih kecil, . VC dipakai di sini karena operator transfernya adalah operator yang Clifford pendiagonalnya diberikan dalam bentuk tertutup oleh Ref. [1].
1. Hamiltonian pada kisi persegi
Konstruksinya sama seperti di 1D, tetapi dengan daftar sisi dua dimensi. Site tetap memakai penomoran row-major seperti pada gambar di atas, site(r, c) = r * cols + c, sehingga ikatan horizontal menghubungkan indeks berurutan, dan ikatan vertikal menghubungkan indeks yang selisihnya cols:
>>> from collections import defaultdict
>>> from qiskit_fermions.operators import TransferVertexOperator
>>>
>>> def fermi_hubbard_2d(rows, cols, tunneling, interaction):
... site = lambda r, c: r * cols + c
... edges = [(site(r, c), site(r, c + 1)) for r in range(rows) for c in range(cols - 1)]
... edges += [(site(r, c), site(r + 1, c)) for r in range(rows - 1) for c in range(cols)]
... data: defaultdict[tuple[tuple[int, int], ...], complex] = defaultdict(complex)
... for j, k in edges:
... # 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)
>>>
>>> rows, cols = 3, 3
>>> num_sites = rows * cols
>>> hamiltonian = fermi_hubbard_2d(rows, cols, tunneling=1.0, interaction=2.0)
>>> print(f"Number of terms: {len(hamiltonian)}")
Number of terms: 46
2. Empat flow set, bukan dua
Setiap suku hopping diklasifikasikan menurut arah panah-nya pada kisi. Membandingkan koordinat baris/kolom divmod dari kedua ujungnya memisahkan sisi horizontal dari vertikal, dan tanda selisihnya menentukan orientasi. Semua yang diagonal (identitas, suku dan ) masuk ke kelompok kelima:
Konvensi orientasi. Nama-namanya diambil begitu saja dari gambar di atas: sebuah panah disebut east atau west kalau mengarah ke kanan atau kiri sepanjang baris, dan north atau south kalau mengarah ke atas atau bawah sepanjang kolom. Karena penomoran berjalan row-major, east dan south adalah arah dengan indeks yang bertambah. Tidak ada yang bergantung pada nama-nama ini di langkah berikutnya (hanya label kelompok), tetapi gampang tertukar, jadi sebaiknya dipastikan lewat gambar. Ref. [1] menamai flow set kisi persegi miliknya dengan cara yang sama.
>>> EAST, WEST, NORTH, SOUTH, DIAGONAL = range(5)
>>>
>>> def flow_set_groups(operator, cols):
... groups = []
... for terms, _ in operator.iter_terms():
... if len(terms) == 1 and terms[0][0] != terms[0][1]:
... j, k = terms[0]
... row_j, col_j = divmod(j, cols)
... row_k, col_k = divmod(k, cols)
... if row_j == row_k:
... groups.append(EAST if col_k > col_j else WEST)
... else:
... groups.append(SOUTH if row_k > row_j else NORTH)
... else:
... groups.append(DIAGONAL)
... return groups
>>>
>>> hamiltonian.groups = flow_set_groups(hamiltonian, cols)
>>>
>>> names = ["east", "west", "north", "south", "diagonal"]
>>> for index, name in enumerate(names):
... count = sum(1 for group in hamiltonian.groups if group == index)
... print(f"{name:9s} {count:2d}")
east 6
west 6
north 6
south 6
diagonal 22
Dalam satu flow set, setiap panah entah bertemu panah lain secara head-to-tail atau tidak bertemu sama sekali, seperti di 1D, jadi flow set tetap komut suku demi suku dan evolusinya tidak membawa error Trotter. Yang baru adalah bahwa flow set sekarang tidak terhubung: set east pada kisi rows x cols terdiri dari rows lintasan kiri-ke-kanan yang saling independen. Keterputusan itu justru keuntungan, bukan komplikasi, seperti yang ditunjukkan langkah 6.
3. Meng-encode operator
Encoding-nya berupa satu fungsi yang memetakan satu operator transfer umum menjadi sebuah string Pauli, yang lalu diteruskan ke map_transfer_vertex_generators(). Dibandingkan versi 1D, isinya punya lebih banyak kasus: diagonal, horizontal, vertikal, dan orientasi kebalikan lewat .
Ada satu masalah. map_transfer_vertex_generators() memanggil balik hanya dengan sebuah generator dan tidak ada yang lain, padahal menentukan string Pauli sebuah ikatan juga membutuhkan cols untuk memulihkan koordinat baris dan kolom dari indeks mode. Daripada menyarangkan fungsi untuk menangkapnya, functools.partial() mengikatnya, sehingga kedua fungsi tetap berada di level teratas dan bisa dipanggil secara independen.
>>> from functools import partial
>>>
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_transfer_vertex_generators
>>>
>>> def ancilla_qubit(mode, num_sites):
... """The auxiliary qubit paired with fermionic mode `mode`."""
... return num_sites + mode
>>>
>>> def vc_action(generator, width, cols):
... """Map one generalized transfer operator to its Verstraete-Cirac Pauli."""
... num_sites = width // 2
... ancilla = partial(ancilla_qubit, num_sites=num_sites)
... j, k = generator
... if j == k: # vertex operator V_j = Z_j
... return SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... row_j, col_j = divmod(j, cols)
... row_k, col_k = divmod(k, cols)
... if row_j == row_k: # horizontal: weight-3 triangle
... left, right = (j, k) if col_j < col_k else (k, j)
... operator = SparseObservable.from_sparse_list(
... [("XXZ", [left, right, ancilla(left)], 0.5)], num_qubits=width
... )
... forward = (j, k) == (left, right)
... else: # vertical: weight-4 square
... up, down = (j, k) if row_j < row_k else (k, j)
... operator = SparseObservable.from_sparse_list(
... [("XYXY", [up, down, ancilla(up), ancilla(down)], 0.5)], num_qubits=width
... )
... forward = (j, k) == (up, down)
... if forward:
... return operator
... # reverse orientation: T_kj = -V_j V_k T_jk
... v_j = SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... v_k = SparseObservable.from_sparse_list([("Z", [k], 1.0)], num_qubits=width)
... return (-1.0 * v_j.compose(v_k).compose(operator)).simplify()
>>>
>>> def vc_encoding(operator, width, cols):
... """Map a TransferVertexOperator to a qubit observable, Verstraete-Cirac style."""
... return map_transfer_vertex_generators(
... operator,
... partial(vc_action, width=width, cols=cols),
... identity=lambda: SparseObservable.identity(width),
... compose=SparseObservable.compose,
... ).simplify()
>>>
>>> # bind the lattice width once; the transpiler only ever passes (operator, width)
>>> encoding = partial(vc_encoding, cols=cols)
>>> num_qubits = 2 * num_sites
>>> num_qubits
18
Manfaat mengelompokkan sebelum memetakan langsung terlihat: setiap flow set di-encode menjadi satu bentuk Pauli, seragam di seluruh set.
>>> for index, name in enumerate(names):
... observable = encoding(hamiltonian.split_out_groups()[index], num_qubits)
... shapes = sorted({label for label, _, _ in observable.to_sparse_list()})
... print(f"{name:9s} {shapes}")
east ['XXZ']
west ['YYZ']
north ['YXXY']
south ['XYXY']
diagonal ['', 'Z', 'ZZ']
Keempat bentuk hopping itu berbeda, jadi setiap flow set mendapat Clifford-nya sendiri di langkah 5. Orientasi kebalikan adalah Pauli yang berbeda, bukan Pauli yang sama dengan label lain; inilah sebabnya sintesis flow set 2D membutuhkan empat circuit, sedangkan sintesis 1D hanya butuh satu.
4. Transpile dengan encoding kustom
Tahap ini sama seperti yang 1D dengan register qubit yang lebih lebar: CustomF2QLayout memetakan register mode ke qubit, MapperFnEvolutionSynthesis membawa encoding-nya, dan argumen product_formula-nya memilih bagaimana PauliEvolutionGate yang dihasilkan didekomposisi. Bandingkan dengan langkah 6 pada panduan 1D, yang membangun pipeline yang sama untuk satu ancilla.
>>> from qiskit.circuit import QuantumRegister
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.synthesis import LieTrotter
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import FermionicLieTrotter
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG, QuantumDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import (
... CustomF2QLayout,
... F2QSynthesis,
... MapperFnEvolutionSynthesis,
... )
>>>
>>> total_time = 1.0
>>>
>>> def flow_set_pass_manager(mode_register, encoding, product_formula=None):
... """Build the pipeline mapping `mode_register` with the VC `encoding`."""
... synthesis = F2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... encoding, product_formula=product_formula
... )
... # this encoding spends one ancilla per fermionic mode
... qubit_register = QuantumRegister(2 * mode_register.size, "q")
... return MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... layout=CustomF2QLayout({mode_register: qubit_register}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>>
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(
... Evolution(
... num_sites, hamiltonian, time=total_time, synthesis=FermionicLieTrotter()
... ),
... circuit.modes,
... )
>>>
>>> # as in 1D, one decompose() splits the Evolution group by group (five gates here,
>>> # the four hopping flow sets in group order, then the diagonal interaction)
>>> flow_set_circuit = circuit.decompose()
>>> flow_set_circuit.count_ops()["Evolution"]
5
Lima gate, yang membawa 6, 6, 6, 6, dan 22 suku seperti dihitung di langkah 2. Masing-masing dipetakan dan disintesis secara independen, jadi partisi yang ditentukan di level fermionik bertahan sampai ke dalam circuit:
>>> pass_manager = flow_set_pass_manager(flow_set_circuit.register, encoding, LieTrotter())
>>> qubit_circuit = pass_manager.run(flow_set_circuit)
>>> qubit_circuit.num_qubits
18
>>>
>>> def two_qubit_stats(circuit):
... decomposed = circuit.decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return decomposed.count_ops().get("cx", 0), decomposed.depth(two_qubit)
>>>
>>> two_qubit_stats(qubit_circuit)
(144, 47)
Itulah baseline-nya: encoding dan pengelompokan sudah terpasang, tetapi LieTrotter mensintesis setiap flow set suku demi suku, dan depth-nya tumbuh seiring ukuran kisi.
5. Clifford berdepth 2 per flow set
Mekanismenya sama seperti di 1D: lakukan konjugasi satu flow set komut penuh dengan sebuah Clifford yang memetakan setiap sukunya ke Pauli berbobot 1 yang berbeda, sehingga evolusi seluruh set hanya memerlukan rotasi single-qubit. Di 1D, satu brickwork gate CZ melakukan ini.
Di 2D, setiap flow set butuh circuit-nya sendiri, mengikuti Bagian IV C dan Gambar 7 dari Ref. [1]. Keempatnya bekerja pada satu garis kisi sekali waktu (satu baris untuk east/west, satu kolom untuk north/south) bersama ancilla garis itu, dan keempatnya punya bentuk tiga bagian yang sama:
-
Satu layer rotasi single-qubit, yang menempatkan setiap qubit pada basis yang dibutuhkan flow set-nya.
-
Layer
CXpertama, selalu dari site ke ancilla pada site yang sama. -
Layer
CXkedua, yang mengaitkan tetangga di sepanjang garis.
Langkah 2 dan 3 adalah satu-satunya layer dua-qubit, jadi depth dua-qubit-nya , berapa pun panjang garisnya. Semuanya ditulis satu fungsi per flow set di bawah ini. Rotasinya seperti itu karena ditemukan dengan melakukan konjugasi pada suku-suku tiap flow set dan mensyaratkan citra berbobot 1 yang berbeda, jadi anggap saja sebagai yang sudah diberikan dan andalkan pemeriksaan yang menyusul.
Dua set horizontal adalah yang paling sederhana. Suku-sukunya berbobot 3, XXZ dan YYZ, dan hanya berbeda oleh ekstra yang mengubah menjadi pada site:
>>> from qiskit.circuit import QuantumCircuit
>>>
>>> def east_clifford(circuit, line, ancilla):
... """Diagonalize the east flow set (XXZ) on one lattice row."""
... for j in line:
... circuit.h(ancilla(j))
... for j in line[:-1]: # layer 1: each site with its own ancilla
... circuit.cx(ancilla(j), j)
... for j, k in zip(line, line[1:]): # layer 2: ancilla with the next site along
... circuit.cx(k, ancilla(j))
>>>
>>> def west_clifford(circuit, line, ancilla):
... """Diagonalize the west flow set (YYZ) on one lattice row."""
... for j in line:
... circuit.s(j) # the only difference from east: X -> Y on the sites
... circuit.h(ancilla(j))
... for j in line[:-1]:
... circuit.cx(ancilla(j), j)
... for j, k in zip(line, line[1:]):
... circuit.cx(k, ancilla(j))
Dua set vertikal berbobot 4, XYXY dan YXXY, dan sesuai dengan itu lebih ramai: rotasinya berganti-ganti menurut posisi di sepanjang garis, dan layer CX kedua juga berganti-ganti, mengaitkan site pada posisi genap dan ancilla pada posisi ganjil (atau sebaliknya):
>>> def south_clifford(circuit, line, ancilla):
... """Diagonalize the south flow set (XYXY) on one lattice column."""
... for position, j in enumerate(line):
... if position % 2 == 0:
... circuit.sdg(ancilla(j))
... circuit.h(ancilla(j))
... else:
... circuit.h(j)
... circuit.s(j)
... circuit.h(ancilla(j))
... circuit.s(ancilla(j))
... for j in line: # layer 1
... circuit.cx(ancilla(j), j)
... for position, (j, k) in enumerate(zip(line, line[1:])): # layer 2
... if position % 2 == 0:
... circuit.cx(ancilla(j), ancilla(k)) # ancilla to ancilla
... else:
... circuit.cx(j, k) # site to site
>>>
>>> def north_clifford(circuit, line, ancilla):
... """Diagonalize the north flow set (YXXY) on one lattice column."""
... for position, j in enumerate(line):
... if position % 2 == 0:
... circuit.h(ancilla(j))
... circuit.s(ancilla(j))
... else:
... circuit.h(j)
... circuit.s(j)
... circuit.sdg(ancilla(j))
... circuit.h(ancilla(j))
... for j in line: # layer 1
... circuit.cx(ancilla(j), j)
... for position, (j, k) in enumerate(zip(line, line[1:])): # layer 2
... if position % 2 == 0:
... circuit.cx(k, j) # site to site, and the other way round from south
... else:
... circuit.cx(ancilla(j), ancilla(k))
Digambar pada satu garis tiga site (site-nya dulu, lalu ancilla-nya) seluruh mekanismenya muat dalam satu gambar. Bentuk segitiga adalah yang lebih jelas dari keduanya: perubahan basis pada ancilla, lalu dua layer CX, yang pertama memasangkan setiap site dengan ancilla-nya sendiri dan yang kedua menjangkau site berikutnya di sepanjang garis.
>>> line = [0, 1, 2] # three sites, with ancillas on qubits 3, 4, 5
>>> triangle = QuantumCircuit(6)
>>> east_clifford(triangle, line, lambda mode: 3 + mode)
>>> triangle.draw("mpl")
<Figure size ... with 1 Axes>
Bentuk persegi membutuhkan lebih banyak kerja single-qubit (rotasinya berganti-ganti di sepanjang garis) dan layer CX keduanya berganti antara mengaitkan site dan mengaitkan ancilla:
>>> square = QuantumCircuit(6)
>>> south_clifford(square, line, lambda mode: 3 + mode)
>>> square.draw("mpl")
<Figure size ... with 1 Axes>
Keduanya sedalam dua layer CX, dan menambah site pada garis hanya melebarkannya tanpa memperdalamnya; itulah seluruh klaim depth konstan, terlihat langsung dengan pemeriksaan. Clifford west dan north hanya berbeda pada layer single-qubit-nya, jadi tidak digambar terpisah.
Yang masih dibutuhkan sintesis adalah menentukan ke mana setiap suku berakhir. Sebuah garis dengan site punya ikatan, masing-masing menyumbang satu suku, jadi helper di bawah mengembalikan satu entri per ikatan: qubit tempat suku itu mendarat, dan Pauli single-qubit yang ia jadi. Yang penting, ini dibaca dari konjugasi, bukan ditebak. Tidak seperti 1D, di mana satu brickwork CZ memetakan dalam bentuk tertutup, di sini tidak ada rumus seperti itu, dan ancilla pada garis kolom tidak berurutan. Satu-satunya yang berbeda antar flow set adalah string Pauli yang masuk, yang sudah dicetak langkah 3:
>>> from qiskit.quantum_info import Clifford, SparsePauliOp
>>>
>>> def clifford_supports(clifford, pauli, line, ancilla, num_qubits):
... """Conjugate a flow set's terms to find which qubit each one lands on.
...
... `pauli` is the flow set's Pauli string, as printed in step 3. Its letters apply
... to the i-th term's qubits in the order below: the two sites of the bond, then
... their ancillas.
...
... This is the one check that needs SparsePauliOp: Clifford conjugation wants a
... `Pauli` and its symplectic bits, and SparseObservable exposes neither -- its own
... `evolve` conjugates by a Pauli, not by a Clifford.
... """
... supports = []
... for i in range(len(line) - 1):
... qubits = [line[i], line[i + 1], ancilla(line[i]), ancilla(line[i + 1])]
... term = SparsePauliOp.from_sparse_list(
... [(pauli, qubits[: len(pauli)], 1.0)], num_qubits=num_qubits
... )
... image = term.paulis[0].evolve(clifford, frame="s")
... # a weight-1 image has exactly one qubit with an X and/or Z bit set
... (support,) = np.nonzero(image.x | image.z)
... assert len(support) == 1, f"term {i} is not weight-1"
... supports.append((int(support[0]), image[support].to_label()))
... return supports
Citra-citranya harus keluar berbeda dan berbobot 1, supaya setiap suku bisa dirotasi secara independen pada qubit-nya sendiri; depth dua-qubit sudah terlihat dalam empat konstruksi di atas, masing-masing menghasilkan dua layer CX terlepas dari panjang garis. Di bawah, kedua properti itu diperiksa pada setiap flow set, setiap garis, dan beberapa ukuran kisi. Langkah 6 lalu memanggil clifford_supports, jadi ini bagian kerja dari sintesis, bukan sekadar demonstrasi.
6. Sintesis yang sadar flow set
Sintesis mendapat awalan yang menguntungkan. Karena Evolution memecah dirinya sendiri kelompok demi kelompok, synthesize dipanggil sekali per flow set (lima kali di sini, empat set hopping plus kelompok diagonal) sehingga setiap suku hopping yang dilihatnya punya satu bentuk Pauli yang sama, dan satu Clifford mendiagonalkan semuanya. Ia hanya perlu membaca bentuk itu dari suku pertama dan mengurutkan sisanya menurut garis tempat mereka berada. Lalu muncul pengamatan yang membuat dua dimensi lebih murah daripada yang dikhawatirkan:
Komponen-komponen dari satu flow set saling vertex-disjoint: baris-baris set west tidak berbagi qubit. Jadi semua Clifford sebuah flow set bisa dikeluarkan bersamaan dan berbagi dua layer dua-qubit yang sama, bukan mengantre satu baris demi satu baris. Karena itu sebuah flow set memakan jumlah layer yang tetap (dua untuk Clifford dan dua untuk inversnya) berapa pun banyaknya baris atau kolom pada kisi, dan itulah sebabnya depth totalnya akhirnya konstan.
>>> from qiskit.synthesis.evolution import EvolutionSynthesis
>>>
>>> # The interaction couples nearest neighbors only, so every interior site carries four
>>> # Rzz gates -- one per bond -- and can only do one at a time: four layers is the best
>>> # any schedule can achieve. Horizontal bonds split by column parity, vertical ones by
>>> # row parity, which achieves it.
>>> def rzz_layers(circuit, angles, cols):
... """Emit the diagonal Rzz gates in four layers of disjoint qubit pairs."""
... horizontal = [(j, k) for j, k in angles if k - j == 1]
... vertical = [(j, k) for j, k in angles if k - j == cols]
... schedule = [
... [(j, k) for j, k in horizontal if (j % cols) % 2 == 0],
... [(j, k) for j, k in horizontal if (j % cols) % 2 == 1],
... [(j, k) for j, k in vertical if (j // cols) % 2 == 0],
... [(j, k) for j, k in vertical if (j // cols) % 2 == 1],
... ]
... # 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 2D line flow set at a time, via its depth-2 Clifford."""
...
... preserve_order = True
...
... def __init__(self, rows, cols):
... self.rows, self.cols = rows, cols
...
... def rows_of_lattice(self):
... return [[r * self.cols + c for c in range(self.cols)] for r in range(self.rows)]
...
... def columns_of_lattice(self):
... return [[r * self.cols + c for r in range(self.rows)] for c in range(self.cols)]
...
... def synthesize(self, evolution):
... num_qubits = 2 * self.rows * self.cols
... ancilla = partial(ancilla_qubit, num_sites=self.rows * self.cols)
... circuit = QuantumCircuit(evolution.operator.num_qubits)
... terms = [
... (label, indices, 2 * evolution.time * coeff.real)
... for label, indices, coeff in evolution.operator.to_sparse_list()
... ]
...
... # `Evolution.decompose()` hands us one gate per flow set, so every hopping term
... # here shares a single Pauli shape and one Clifford diagonalizes all of them
... shapes = tuple({label for label, _, _ in terms} - {"", "Z", "ZZ"})
... match shapes:
... case ("XXZ",):
... clifford_function, lines = east_clifford, self.rows_of_lattice()
... along_rows = True
... case ("YYZ",):
... clifford_function, lines = west_clifford, self.rows_of_lattice()
... along_rows = True
... case ("XYXY",):
... clifford_function, lines = south_clifford, self.columns_of_lattice()
... along_rows = False
... case ("YXXY",):
... clifford_function, lines = north_clifford, self.columns_of_lattice()
... along_rows = False
... case (): # the diagonal group: no conjugation needed
... self.diagonal(circuit, terms)
... return circuit
... case _:
... raise ValueError(f"expected one known flow set per gate, got {shapes}")
...
... # a term's line is the row it sits in and its slot the column, or vice versa
... rotations: dict[int, list] = defaultdict(list)
... for _, indices, angle in terms:
... row, col = divmod(indices[0], self.cols)
... line, slot = (row, col) if along_rows else (col, row)
... rotations[line].append((slot, angle))
...
... active = sorted(rotations)
... # the lines of one flow set are vertex-disjoint, so all of their Cliffords
... # are emitted together and share the same two two-qubit layers
... conjugation = QuantumCircuit(num_qubits)
... for line in active:
... clifford_function(conjugation, lines[line], ancilla)
... circuit.compose(conjugation, inplace=True)
... for line in active:
... supports = clifford_supports(
... Clifford(conjugation), shapes[0], lines[line], ancilla, num_qubits
... )
... for slot, angle in rotations[line]:
... match supports[slot]:
... case qubit, "X":
... circuit.rx(angle, qubit)
... case qubit, "Y":
... circuit.ry(angle, qubit)
... case qubit, "Z":
... circuit.rz(angle, qubit)
... circuit.compose(conjugation.inverse(), inplace=True)
... return circuit
...
... def diagonal(self, circuit, terms):
... """Synthesize the diagonal group: a phase, single-qubit Z and paired Rzz."""
... rzz = {}
... for label, indices, angle in terms:
... match label, indices:
... case "", _: # the identity contributes only a global phase
... circuit.global_phase -= angle / 2
... case "Z", [qubit]: # a lone vertex operator
... circuit.rz(angle, qubit)
... case "ZZ", [j, k]: # interaction, see rzz_layers above
... rzz[min(j, k), max(j, k)] = angle
... case _:
... raise ValueError(f"unexpected Pauli term: {label} on {indices}")
... rzz_layers(circuit, rzz, self.cols)
Seperti di 1D, ini sengaja ditulis khusus untuk encoding ini: ia mengenali keempat bentuk Pauli dan melempar error pada yang lain, alih-alih diam-diam beralih ke cara lain. Ia juga mengasumsikan pemisahan satu-flow-set-per-gate di atas, dan menyatakannya; lebih dari satu bentuk hopping dalam satu gate berarti pengelompokan tidak diterapkan, yang merupakan error dan bukan sesuatu yang perlu disiasati.
Seperti padanan 1D-nya, sintesis ini tidak menerima jumlah langkah Trotter. Satu langkah Trotter harus bersiklus di antara flow set, tetapi transpile_trotter_step di bawah mendekomposisi circuit fermionik menjadi satu gate Evolution per set, sehingga method ini hanya melihat satu set sekali waktu dan hanya bisa mengulang di dalamnya, yang tidak mengubah apa-apa: mengulang satu operator hanya membagi evolusinya sendiri menjadi bagian-bagian yang sama besar.
Trotterisasi multi-langkah karena itu membutuhkan product formula yang menghormati groups milik operator di dalam satu gate, sebelum dekomposisi. FermionicLieTrotter melakukannya, dan langkah 10 memakainya untuk mengukur konvergensi.
Perhatikan dua kelas bernama mirip yang terlibat. LieTrotter milik Qiskit, yang dipakai sebagai baseline di atas, bekerja pada Hamiltonian yang sudah dipetakan, di mana pengelompokan sudah hilang. FermionicLieTrotter adalah padanan fermionik-nya, dan ia yang masih bisa melihat flow set.
7. Hasilnya: depth konstan
Semua yang sejauh ini dijalankan memakai satu kisi yang dibangun di langkah 1. Membandingkan kedua jalur sintesis pada kisi yang makin besar membutuhkan seluruh urutan itu (operator, pengelompokan, encoding, circuit, pass manager) diulang per ukuran, jadi sebaiknya dibungkus sekali saja:
>>> def transpile_trotter_step(rows, cols, product_formula, steps=1):
... """Transpile `steps` Trotter steps on a `rows` x `cols` lattice."""
... num_sites = rows * cols
... operator = fermi_hubbard_2d(rows, cols, 1.0, 2.0)
... operator.groups = flow_set_groups(operator, cols)
... encoding = partial(vc_encoding, cols=cols)
...
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(
... num_sites,
... operator,
... time=total_time,
... synthesis=FermionicLieTrotter(reps=steps),
... ),
... circuit.modes,
... )
... circuit = circuit.decompose() # split into one Evolution per flow set
...
... pass_manager = flow_set_pass_manager(circuit.register, encoding, product_formula)
... return pass_manager.run(circuit)
Mengumpulkan kedua metrik pada berbagai ukuran kisi dan memplotnya terhadap sisi kisi membuat perbedaannya jelas:
>>> sizes = [3, 4, 5, 6, 7, 8]
>>> lie_trotter = [
... two_qubit_stats(transpile_trotter_step(n, n, LieTrotter())) for n in sizes
... ]
>>> flow = [
... two_qubit_stats(transpile_trotter_step(n, n, FlowSetSynthesis(n, n))) for n in sizes
... ]
>>>
>>> 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(sizes, [stat[index] for stat in lie_trotter], "o-", label="qubit-level")
... _ = panel.plot(sizes, [stat[index] for stat in flow], "s-", label="flow set")
... _ = panel.set(xlabel="lattice edge $L$ (an $L \\times L$ lattice)", title=title)
... _ = panel.legend()
>>> figure
<Figure size ... with 2 Axes>
Depth dua-qubit-nya 24 dan konstan dari sampai (yaitu 18 qubit sampai 128), sedangkan LieTrotter yang diterapkan pada Hamiltonian dengan encoding identik tumbuh linear terhadap sisi kisi, dari 47 sampai 92. Jumlah gate tumbuh pada keduanya, karena ada suku yang harus diterapkan bagaimanapun; yang dihilangkan flow set adalah pertumbuhan jumlah layer yang ditempati gate-gate itu.
Anggarannya terbagi menjadi 16 untuk hopping dan delapan untuk interaksi:
-
Masing-masing dari empat flow set memakan layer dua-qubit: Clifford-nya saat masuk dan inversnya saat keluar, keduanya berdepth dua menurut konstruksi, dengan hanya rotasi single-qubit di antaranya. Itu , angka yang dikutip Ref. [1], karena ia hanya menghitung suku hopping. Mengatur pada kode di atas mereproduksinya.
-
Kelompok diagonal menambah delapan sisanya. Gate
Rzz-nya terkemas dalam empat layer, dan setiapRzzterdekomposisi menjadi dua gateCXmengapit satu rotasi single-qubit, jadi . Empat layer adalah yang optimal: pada kisi persegi, interaksi hanya menjangkau tetangga terdekat, jadi setiap site interior membawa empat gateRzz(satu per ikatan) dan hanya bisa ikut serta dalam satu pada satu waktu.
Tidak ada dalam kedua penjumlahan itu yang merujuk ukuran kisi, dan itulah intinya.
8. Verifikasi circuit
Sintesis flow set dan LieTrotter menggambarkan langkah Trotter yang sama dari Hamiltonian ter-encode yang sama, jadi unitary keduanya harus sama hingga fase global. Statevector acak mendeteksi ketidakcocokan apa pun jauh lebih murah daripada membangun unitary penuh:
Kedua circuit didekomposisi menjadi gate satu- dan dua-qubit sebelum diterapkan ke sebuah state. Kalau dibiarkan tak terdekomposisi, setiap instruksi Evolution tingkat tinggi memaksa Statevector membangun operator dense penuhnya yang berukuran ; dengan mendekomposisi dulu, ia bisa menerapkan gate satu per satu, yang di sini beberapa ratus kali lebih cepat.
>>> import numpy as np
>>> from qiskit.quantum_info import Statevector
>>>
>>> rng = np.random.default_rng(1234)
>>>
>>> def agrees_up_to_one_phase(rows, cols, samples=3):
... """Compare both syntheses on random states.
...
... Returns whether they agree up to a phase, and whether it is the *same* phase
... for every input state.
... """
... lie_trotter = transpile_trotter_step(
... rows, cols, LieTrotter()
... ).decompose(reps=6)
... flow = transpile_trotter_step(
... rows, cols, FlowSetSynthesis(rows, cols)
... ).decompose(reps=6)
... dimension = 2**lie_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(lie_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(rows, cols)
(True, True)
Kedua bagian pemeriksaan itu penting. Overlap bermodulus satu pada satu state perlu tetapi belum cukup: sintesis yang salah pada fase yang bergantung pada state tetap akan terlihat. Mensyaratkan fase yang sama pada beberapa state acak-lah yang menutup celahnya.
9. Verifikasi fisikanya pada subruang stabilizer
Inilah kerumitan dua dimensinya. Di 1D, encoding-nya adalah isometri, dan membandingkan spektrum terhadap referensi Jordan-Wigner sudah cukup. VC bukan isometri: ia memakai qubit untuk mode, sehingga ruang berdimensi memuat ruang fisik berdimensi . Ini umum terjadi pada encoding lokal di dua dimensi (qubit berlebih dibatasi oleh simetri komut lokal, satu per plakuet [2] [3]) dan artinya Hamiltonian ter-encode mewakili yang fermionik secara setia hanya pada eigenspace gabungan dari kendala-kendala itu.
Kata stabilizer punya dua fungsi dalam topik ini, dan kedua maknanya tidak berhubungan. Ref. [1] memakainya untuk operator transfer komut dari sebuah flow set, yang membentuk grup stabilizer yang didiagonalkan oleh Clifford dari langkah 5 (sebuah alat kompilasi, dan makna yang dimaksud di seluruh bagian atas). Bagian ini memakainya dalam makna koreksi error: operator yang eigenspace -nya adalah subruang fisik. Ref. [1] secara eksplisit menandai tabrakan ini dan menyisihkan makna kedua; kendala plakuet di bawah adalah properti encoding VC [2], bukan hasil dari kerangka flow set.
Generator-generatornya dibangun di sini, bukan dikutip: satu per muka persegi, dan masing-masing tidak lain adalah loop operator sisi ter-encode di sekeliling muka itu.
Operator sisi adalah objek yang alami untuk ini. Loop tertutup darinya adalah simetri dari Hamiltonian apa pun yang menjaga jumlah partikel, sedangkan loop operator transfer yang bersesuaian bukan: keduanya berbeda oleh operator vertex pada site sumber setiap ikatan,
Di sekeliling loop tertutup, setiap site menjadi sumber tepat satu ikatan, jadi loop mengumpulkan satu per site, yaitu paritas lokal dari empat site plakuet. Paritas lokal itulah yang membuat perbedaan: Hamiltonian memindahkan partikel melintasi batas plakuet, jadi ia hanya menjaga paritas global. Loop transfer polos karena itu tidak komut dengannya, sedangkan loop sisi (yang membawa faktor paritas) komut.
Karena encoding didefinisikan pada operator transfer, memetakan operator sisi membutuhkan relasi di atas. Itu callback lima baris, dan ia masuk ke map_edge_vertex_generators() persis seperti vc_action masuk ke map_transfer_vertex_generators():
>>> from qiskit_fermions.mappers import map_edge_vertex_generators
>>> from qiskit_fermions.operators import EdgeVertexOperator
>>>
>>> def vc_edge_action(generator, width, cols):
... """Map one generalized *edge* operator to its Verstraete-Cirac Pauli."""
... j, k = generator
... if j == k: # a vertex operator is the same object in both algebras
... return SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... # The library decomposes T_jk = (i/2) V_j E_jk, so E_jk = -2i V_j T_jk. Reuse the
... # transfer encoding for T_jk rather than repeating its four cases here. Note that
... # `compose` left-multiplies its argument, so this composes to V_j T_jk; the other
... # order carries the opposite sign, since a vertex and a transfer operator sharing a
... # mode anticommute.
... vertex = SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... return (-2.0j * vc_action(generator, width, cols).compose(vertex)).simplify()
>>>
>>> def vc_edge_encoding(operator, width, cols):
... """Map an EdgeVertexOperator to a qubit observable, Verstraete-Cirac style."""
... return map_edge_vertex_generators(
... operator,
... partial(vc_edge_action, width=width, cols=cols),
... identity=lambda: SparseObservable.identity(width),
... compose=SparseObservable.compose,
... ).simplify()
>>>
>>> def plaquette_loops(rows, cols):
... """The directed edge loop around each square face."""
... at = lambda r, c: r * cols + c
... return [
... [(at(r, c), at(r, c + 1)), (at(r, c + 1), at(r + 1, c + 1)),
... (at(r + 1, c + 1), at(r + 1, c)), (at(r + 1, c), at(r, c))]
... for r in range(rows - 1)
... for c in range(cols - 1)
... ]
>>>
>>> def stabilizers(rows, cols):
... """One plaquette stabilizer per square face of the lattice."""
... num_qubits = 2 * rows * cols
... out = []
... for loop in plaquette_loops(rows, cols):
... loop_operator = EdgeVertexOperator.from_dict({tuple(loop): 1.0})
... encoded_loop = vc_edge_encoding(loop_operator, num_qubits, cols)
... out.append(encoded_loop / complex(encoded_loop.coeffs[0]))
... return out
>>>
>>> plaquette_stabilizers = stabilizers(rows, cols)
>>> for stabilizer in plaquette_stabilizers:
... print(stabilizer.to_sparse_list())
[('ZZYXXY', [1, 4, 9, 10, 12, 13], (1+0j))]
[('ZZYXXY', [2, 5, 10, 11, 13, 14], (1+0j))]
[('ZZYXXY', [4, 7, 12, 13, 15, 16], (1+0j))]
[('ZZYXXY', [5, 8, 13, 14, 16, 17], (1+0j))]
Setiap generator berbobot , tersebar pada dua qubit fisik plakuetnya dan empat ancilla. Mereka involutif dan komut dengan Hamiltonian ter-encode, jadi subruang fisik terjaga oleh dinamikanya. Kedua properti ini adalah pernyataan tentang operator, jadi diperiksa secara simbolik pada aljabar SparseObservable, tanpa perlu matriks :
>>> encoded = encoding(hamiltonian, num_qubits)
>>> identity = SparseObservable.identity(num_qubits)
>>> for stabilizer in plaquette_stabilizers:
... square = stabilizer.compose(stabilizer).simplify()
... print(square == identity, stabilizer.commutes(encoded))
True True
True True
True True
True True
Dengan stabilizer di tangan, perbandingan tidak butuh korespondensi basis, karena berarti okupansi berada pada qubit fisik yang sama di kedua gambaran. Jadi: tulis state produk langsung pada qubit fisik dengan ancilla di , proyeksikan ke subruang stabilizer, evolusikan dengan Hamiltonian ter-encode, dan baca densitas site dari kedua sisi.
Setiap langkah di sini tetap di level vektor. Proyektor diterapkan satu faktor sekali waktu alih-alih dikalikan penuh, dan evolusi waktu memakai expm_multiply(), yang menghitung tanpa pernah membentuk . Karena Hamiltonian ter-encode adalah operator sparse berdimensi , perbedaan itulah yang membuat bagian ini tetap murah.
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_fermions.mappers.library import transfer_vertex_jordan_wigner
>>>
>>> # an exact evolution time, unrelated to the Trotter step above
>>> evolution_time = 0.6
>>> reference = transfer_vertex_jordan_wigner(hamiltonian, num_sites).simplify()
>>>
>>> def to_sparse_matrix(observable):
... # SparseObservable carries no dense- or sparse-matrix method, so route through
... # SparsePauliOp. This is only ever needed for the numerical checks below --
... # the encoding itself never leaves SparseObservable.
... return SparsePauliOp.from_sparse_observable(observable).to_matrix(sparse=True)
>>>
>>> def basis_state(occupation, num_qubits):
... """The computational basis state with `occupation` on the first qubits.
...
... Qubit `q` carries bit `2**q`, and the ancillas are the *high* qubits `N..2N-1`,
... so the same integer indexes the same site occupations on either register and
... leaves every ancilla in |0>.
... """
... index = int("".join(str(bit) for bit in reversed(occupation)), 2)
... state = np.zeros(2**num_qubits, dtype=complex)
... state[index] = 1.0
... return state
>>>
>>> def project_onto_codespace(state, stabilizers):
... """Apply the projector one (1 + S) factor at a time, then renormalize.
...
... The factors commute, so their product is itself a projector and the order does
... not matter. Multiplying it out would build a dense 2^2N x 2^2N operator; applied
... factor by factor this stays a handful of sparse matrix-vector products.
...
... Each factor is really (1 + S)/2, but every one contributes the same scalar, so the
... halvings only rescale the result and the final normalization absorbs them.
... """
... for stabilizer in stabilizers:
... state += to_sparse_matrix(stabilizer) @ state
... return state / np.linalg.norm(state)
>>>
>>> occupation = [1, 0, 1, 0, 1, 0, 0, 1, 0] # site occupations of the initial state
>>> initial_reference = basis_state(occupation, num_sites)
>>> initial_encoded = project_onto_codespace(
... basis_state(occupation, num_qubits), plaquette_stabilizers
... )
>>>
>>> evolved_encoded = expm_multiply(
... -1j * evolution_time * to_sparse_matrix(encoded), initial_encoded
... )
>>> evolved_reference = expm_multiply(
... -1j * evolution_time * to_sparse_matrix(reference), initial_reference
... )
>>>
>>> def density(state, qubit, width):
... """Site density <n_j> = (1 - <Z_j>) / 2 of a statevector."""
... z = to_sparse_matrix(
... SparseObservable.from_sparse_list([("Z", [qubit], 1.0)], num_qubits=width)
... )
... return (1 - (state.conj() @ (z @ state)).real) / 2
>>>
>>> encoded_densities = [density(evolved_encoded, j, num_qubits) for j in range(num_sites)]
>>> reference_densities = [density(evolved_reference, j, num_sites) for j in range(num_sites)]
>>>
>>> for j, density_value in enumerate(encoded_densities):
... print(f"site {j}: {density_value:.10f}")
site 0: 0.6750993356
site 1: 0.3689791258
site 2: 0.6750993356
site 3: 0.3818130396
site 4: 0.4868585996
site 5: 0.3818130396
site 6: 0.2693544696
site 7: 0.4916285849
site 8: 0.2693544696
>>> np.allclose(encoded_densities, reference_densities, atol=1e-12)
True
Itulah densitas ter-encode; cocok dengan referensi Jordan-Wigner hingga presisi mesin, pada kisi yang encoding-nya setia hanya setelah proyeksi.
10. Mengukur konvergensi Trotter
Langkah 8 menetapkan bahwa kedua sintesis sepakat satu sama lain, yang merupakan pernyataan tentang ekuivalensi, bukan akurasi: keduanya menggambarkan langkah Trotter tunggal yang sama, jadi keduanya membawa error Trotter yang sama. Mengukur error itu membutuhkan referensi eksak, dan langkah 9 baru saja membangunnya.
Jumlah langkah berasal dari FermionicLieTrotter, padanan fermionik dari rumus level qubit yang dipakai sebagai baseline tadi. Diterapkan pada seluruh operator berkelompok sebelum dekomposisi, pengulangannya bersiklus di antara empat flow set dan kelompok diagonal, bukan di dalam salah satunya:
>>> initial_state = project_onto_codespace(
... basis_state(occupation, num_qubits), plaquette_stabilizers
... )
>>> exact = expm_multiply(
... -1j * total_time * to_sparse_matrix(encoded), initial_state
... )
>>>
>>> def trotter_fidelity(steps):
... # `steps` goes to the fermionic formula inside transpile_trotter_step, so the
... # repetition cycles between the flow sets rather than within any one of them
... circuit = transpile_trotter_step(
... rows, cols, FlowSetSynthesis(rows, cols), steps=steps
... )
... evolved = Statevector(initial_state).evolve(circuit.decompose(reps=6))
... return abs(np.vdot(evolved.data, exact)) ** 2
>>>
>>> for steps in (1, 2, 4, 8):
... print(f"{steps} step(s): fidelity {trotter_fidelity(steps):.6f}")
1 step(s): fidelity 0.053820
2 step(s): fidelity 0.704014
4 step(s): fidelity 0.932937
8 step(s): fidelity 0.982687
Fidelity naik secara monoton menuju satu seiring langkah memendek, yang merupakan perilaku yang harus ditunjukkan product formula. Memperpendek langkah hanyalah satu dari dua tuas; FermionicSuzukiTrotter mempertukarkan keduanya pada orde yang lebih tinggi, seperti yang diukur di Sintesis gate Evolution dalam ruang fermionik.
Satu langkah Hamiltonian ini pada adalah aproksimasi yang buruk (keempat set hopping dan interaksi tidak komut) jadi baris pertama jauh dari satu, dan itulah intinya: konvergensinya yang mengonfirmasi konstruksi ini, bukan satu baris mana pun.
Setiap faktor dari jadwal ini mencakup setiap mode kisi ini, jadi mempersempit sebuah faktor ke support-nya tidak berpengaruh di sini dan indeks qubit absolut yang dicocokkan FlowSetSynthesis tidak berubah. Sintesis yang ditulis untuk operator yang kelompoknya hanya menyentuh sebagian register harus memperhitungkan pelabelan ulang itu.
Yang perlu dibawa pulang
-
Dua dimensi mengubah pembukuannya, bukan resepnya. Setiap bahan dari panduan 1D terbawa tanpa perubahan:
groupsuntuk pengelompokan,map_transfer_vertex_generators()untuk encoding,CustomF2QLayoutuntuk register yang tak cocok, danMapperFnEvolutionSynthesisuntuk menyambungkannya ke transpiler. Yang bertambah adalah jumlah kasus di dalam masing-masing. -
Keterputusan-lah yang membuat 2D berdepth konstan. Sebuah flow set terurai menjadi garis-garis vertex-disjoint, jadi Clifford-nya dikeluarkan secara paralel dan berbagi dua layer dua-qubit sebesar apa pun kisinya. Empat flow set dan satu kelompok diagonal lalu memberi anggaran tetap: di sini untuk keempat flow set ditambah untuk kelompok interaksi diagonal.
-
Setiap orientasi adalah bentuk Pauli-nya sendiri. mengubah
XXZmenjadiYYZdanXYXYmenjadiYXXY, jadi keempat flow set membutuhkan empat Clifford. Mengira satu Clifford mencakup kedua orientasi sebuah sumbu adalah cara termudah untuk salah dalam sintesis flow set 2D. -
Baca support dari konjugasi. Di 1D, satu brickwork
CZmemberikan citra dalam bentuk tertutup. Di 2D tidak ada rumus seperti itu, dan garis kolom punya ancilla yang tidak berurutan, jadi tebakan hasil turunan tangan tentang di mana setiap suku mendarat itu rapuh. Melakukan konjugasi dan memeriksa hasilnya hanya memerlukan satu konstruksiClifforddan tidak bisa salah; itu juga berarti Clifford yang sama validnya yang mendaratkan suku di tempat lain tetap bekerja. -
Biarkan pengelompokan yang mengurutkan. Karena
Evolutionmemecah dirinya per kelompok, sintesis diberi satu flow set sekali waktu dan tidak pernah perlu menurunkan ulang partisinya: ia membaca satu bentuk Pauli dari suku pertama dan memilih Clifford yang cocok. Mengasumsikan itu, dan melempar error saat tidak berlaku, itulah yang menjagasynthesizetetap pendek. -
Jumlah langkah berada di atas pemisahan. Sintesis di sini melihat satu flow set per gate, jadi ia tidak bisa membagi evolusi ke seluruh set.
FermionicLieTrottermelakukannya di ruang fermionik, sebelum dekomposisi, yang membuat uji konvergensi mungkin dilakukan sama sekali. -
Verifikasi pada subruang. Encoding qubit untuk mode hanya setia di tempat stabilizer mengatakan demikian. Karena VC mempertahankan , pemeriksaan andal yang paling murah adalah memproyeksikan state produk dan membandingkan densitas: tanpa intertwiner dan tanpa pencocokan spektral, yang tidak andal ketika Hamiltonian terbatasnya degenerate.
Hamiltonian ter-encode komut dengan stabilizer, jadi proyeksi hanya perlu diterapkan pada state awal. Evolusinya menjaga subruang dengan sendirinya. Pada hardware nyata, inilah juga yang membuat encoding berguna sebagai sumber daya deteksi error: stabilizer bisa diukur.
Selanjutnya
Dua arah memperluas apa yang ada di atas, dan keduanya adalah perubahan bahan, bukan metode. Yang pertama adalah encoding: Verstraete-Cirac dipilih di sini karena Ref. [1] memberikan Clifford pendiagonalnya dalam bentuk tertutup, tetapi Derby-Klassen memakai satu ancilla per plakuet ganjil alih-alih satu per site, untuk rasio qubit-ke-mode dan bukan . Operator transfernya punya bentuk Pauli yang berbeda, jadi memindahkannya berarti menulis ulang fungsi encoding dari langkah 3 dan mencari Clifford untuk setiap bentuk baru; pengelompokan, layout, dan perancah sintesisnya terbawa tanpa tersentuh.
Yang kedua adalah kisinya sendiri. Semua di sini mengasumsikan batas terbuka dan satu spesies fermionik. Batas periodik menutup setiap garis menjadi siklus: panahnya masih semua komut, karena masing-masing bertemu berikutnya secara head-to-tail, tetapi kedua bentuk Clifford dari langkah 5 dibangun untuk garis yang punya ujung. Siklus dengan site punya ikatan per orientasi sedangkan garis punya , jadi ikatan wrap-around tidak punya tempat dalam konstruksi dan perlu penanganan tersendiri. Model spinful lebih mudah: ia menggandakan register dan menambah suku interaksi on-site ke kelompok diagonal, tanpa menyentuh flow set. Tidak satu pun mengganggu argumen depth konstan, tetapi keduanya mengubah pembukuan, yang, seperti dikatakan poin pertama, adalah bagian yang diperbesar oleh dua dimensi.
Konektivitas hardware tidak dibahas di sini. Setiap circuit di atas dibangun pada register abstrak di mana sembarang pasangan qubit bisa berinteraksi, dan depth yang dilaporkan dihitung dalam kondisi itu. Perangkat nyata punya coupling map terbatas, dan memetakan encoding ini ke sana adalah masalah besar tersendiri: register VC adalah graf qubit yang gate-nya berjalan antara sebuah site dan ancilla-nya sendiri, antara site bertetangga dan antara ancilla bertetangga, jadi sebuah layout harus menjaga ketiga jenis pasangan itu tetap berdekatan secara bersamaan. Kalau tidak bisa, transpiler menyisipkan gate SWAP, dan layer yang mereka tambahkan adalah apa yang dibeli kembali oleh konstruksi depth konstan. Ref. [1] juga mengutip angka depth-nya dengan asumsi graf konektivitas persegi dan CX sebagai satu-satunya entangler native. Mencari layout dan strategi routing yang baik untuk encoding lokal pada topologi perangkat tertentu di luar cakupan panduan ini.
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.
F. Verstraete dan J. I. Cirac, Mapping local Hamiltonians of fermions to local Hamiltonians of spins, J. Stat. Mech. 2005, P09012 (2005).
Y.-A. Chen dan Y. Xu, Equivalence between fermion-to-qubit mappings in two spatial dimensions, PRX Quantum 4, 010326 (2023).