Langkau ke kandungan utama
Projek ini dalam fasa beta. Jumpa pepijat? Buat isu GitHub. Ada soalan atau idea? Tanya dalam Discussions — kami suka dengar maklum balas anda!
Bagaimana kualiti terjemahan pada halaman ini?

Simulasikan dinamik Fermi-Hubbard 2D dengan set aliran

Penting

Konsep dalam panduan ini kini hanya tersedia dalam API Python. Fungsi setara akan disediakan dalam API C dalam keluaran akan datang.

Lihat juga

Baca panduan set aliran 1D dahulu. Panduan ini ialah sekuel dua dimensinya dan tidak mengulang maklumat yang dibangunkan di sana: operator pindahan dan bucu, apa itu set aliran, bagaimana groups bertahan ke dalam litar, dan bagaimana pengekodan tersuai disambungkan ke dalam transpiler melalui MapperFnEvolutionSynthesis.

Lihat panduan transpilation untuk peringkat transpiler yang dirujuk sepanjang panduan ini, dan panduan pemeta untuk resipi umum menulis pemeta tersuai.

Panduan 1D berakhir dengan menyebut bahawa pengekodan setempat diperlukan apabila bekerja dengan dua dimensi. Panduan ini mengambil langkah itu, untuk model Fermi-Hubbard dua dimensi pada kekisi petak, menggunakan pengekodan Verstraete-Cirac (VC) [2].

Terdapat tiga perbezaan daripada masalah 1D, yang membentuk asas kerja ini:

  • Set aliran bertambah. Rantai mempunyai dua orientasi anak panah; kekisi petak mempunyai empat (timur, barat, utara, selatan), dan setiap satu ialah kesatuan laluan berarah yang tidak bersentuhan bucu (satu bagi setiap baris atau lajur kekisi) dan bukan satu laluan tunggal.

  • Pengekodan memperoleh ancilla bagi setiap tapak, bukan satu bagi seluruh sistem, memberikan​

    nisbah qubit-kepada-mod 22. Operator pindahan menjadi Pauli berat-3 (mengufuk) dan berat-4 (menegak), dan setiap satu daripada empat set aliran dipetakan kepada bentuk Paulinya sendiri, jadi setiap satu memerlukan Clifford pepenjurunya sendiri.

  • Hamiltonian terkod hanya setia pada subruang: ruang eigen +1+1 bersama​

    kekangan plakuet yang dibawa oleh setiap pengekodan setempat 2D [2] [3]. Dalam 1D pengekodan ialah isometri biasa dan pengesahan ialah semakan spektrum. Di sini, perbandingan mesti diunjurkan, seperti yang digambarkan oleh langkah 9.

Faedahnya berbentuk sama seperti dalam 1D: kedalaman dua-qubit bagi satu langkah Trotter menjadi malar terhadap saiz kekisi, pada 2424 (1616 untuk lompatan, sepadan dengan Rujukan [1], ditambah 88 untuk interaksi), manakala penghampiran Trotter sebutan demi sebutan bagi Hamiltonian terkod yang sama bertambah secara linear. Bilangan gate masih bertambah dengan bilangan ikatan, sudah tentu; apa yang menjadi bebas saiz ialah berapa banyak lapisan yang diduduki oleh gate tersebut. Kedalaman sepanjang masa dikira pada daftar abstrak yang bersambung sepenuhnya; memetakan pengekodan ke atas peta gandingan terhad sesuatu peranti ialah masalah tersendiri, yang dibincangkan di akhir.

Kekisi dan empat orientasinya​

Perubahan pertama kelihatan pada graf interaksi berarah. Tapak dinomborkan mengikut tertib baris-utama, site(r, c) = r * cols + c, dan setiap ikatan membawa kedua-dua orientasi, seperti rantai dalam 1D, tetapi kini ikatan berjalan sepanjang dua paksi:

A 3x3 square lattice with both orientations of every horizontal and vertical bond drawn as arrows, the center site carrying all four.

Peraturan komutasi ialah peraturan 1D, tanpa perubahan: dua operator pindahan yang berkongsi satu tapak kalis tukar tertib apabila anak panah mengalir melaluinya dan antikalis tukar tertib apabila ia berlanggar. Namun, satu tapak kini mempunyai sehingga empat ikatan bersebelahan dan bukan dua, jadi anak panah melaluinya boleh berlanggar dengan lebih banyak cara, dan set anak panah yang tidak pernah berlanggar bukan lagi satu laluan tunggal. Membaca anak panah mengufuk dari kiri ke kanan memberikan satu set sedemikian, anak panah menegak dari atas ke bawah memberikan satu lagi, dan membalikkan setiap satu memberikan dua lagi. Inilah set aliran timur, barat, utara, dan selatan yang menyusun segala-galanya yang berikut.

Pengekodan Verstraete-Cirac​

Verstraete-Cirac (VC) memasangkan setiap mod fermion jj dengan qubit tambahannya sendiri. Dengan NN tapak kekisi, mod jj berada pada qubit jj dan ancillanya pada qubit N+jN + j, jadi daftar memegang 2N2N qubit. Operator bucu kekal semudah mungkin,

Vj=Zj ,V_j = Z_j \, ,

yang menjadikan penghunian boleh dibaca secara langsung kemudian. Operator pindahan bergantung pada orientasi tepi. Bagi tepi mengufuk j→kj \to k dengan jj tapak barat, dan tepi menegak dengan jj tapak utara,

Tjkhorizontal=12XjXkZN+j ,Tjkvertical=12XjYkXN+jYN+k ,T_{jk}^{\text{horizontal}} = \tfrac{1}{2} X_j X_k Z_{N+j} \, , \qquad T_{jk}^{\text{vertical}} = \tfrac{1}{2} X_j Y_k X_{N+j} Y_{N+k} \, ,

“segi tiga” berat-3 dan “petak” berat-4. Mengenal pasti setiap anak panah adalah penting: dengan jj sebagai tapak barat, j→kj \to k menunjuk ke timur, tetapi dengan jj sebagai tapak utara, ia menunjuk ke selatan. Orientasi songsang diperoleh daripada identiti Tkj=−VjVkTjkT_{kj} = -V_j V_k T_{jk}, yang menukarkan segi tiga XXZ kepada YYZ, dan petak XYXY kepada YXXY. Empat label berbeza itu ialah empat set aliran, dan selebihnya panduan ini menjejakinya.

Nota

Ini bukan betul-betul Pers. (A1) dalam Rujukan. [1], yang menyatakan “sehingga putaran qubit tunggal” dan berbeza dalam tiga perkara yang wajar disebut, kerana pembaca yang menyemak kertas kerja tersebut akan menyedarinya:

  • Faktor pendarab: Pers. (A1) tidak mempunyainya. 12\tfrac12 di sini menjadikan Tjk2=14T_{jk}^2 = \tfrac14, sepadan dengan normalisasi Tjk=−12(aj†−aj)(ak†+ak)T_{jk} = -\tfrac12 (a^\dagger_j - a_j)(a^\dagger_k + a_k) yang digunakan oleh TransferVertexOperator. Kertas kerja tersebut hanya memerlukan operator pindahannya di dalam exp⁡(−i dt H)\exp(-i\,dt\,H), yang mana faktor skala diserap ke dalam sudut putaran; di sini ia mestilah operator yang memenuhi suatu identiti.

  • Orientasi: Pers. (A1) memilih huruf mengikut pariti baris bagi jj, yang bagi tepi mendatar adalah sama untuk kedua-dua hujung, jadi ia tidak membezakan TjkT_{jk} daripada TkjT_{kj}. Kertas kerja tersebut memulihkan orientasi secara berasingan, dengan mengkonjugasi menggunakan satu lapisan RZ(π/2)R^Z(\pi/2). Panduan ini sebaliknya memasukkannya ke dalam pengekodan, dengan memperoleh songsangannya daripada Tkj=−VjVkTjkT_{kj} = -V_j V_k T_{jk}, yang menyebabkan kedua-dua orientasi menghasilkan Pauli yang berbeza dalam langkah 3.

  • Bentuk menegak: Pers. (A1) berselang-seli antara XXXX dan YYYY mengikut pariti baris, manakala panduan ini menggunakan satu XYXY. Ini ialah dua cara untuk mencapai perkara yang sama: asimetri huruf yang menyebabkan tepi menegak berkonflik dengan betul dengan tepi mendatar yang bertemu dengannya. Jangan anggap perbezaan ini sebagai pilihan asas yang bebas. Membuang selang-seli pariti dan menggunakan satu XXXX memusnahkan aljabar sepenuhnya, itulah sebabnya huruf yang berbeza disemak dan bukan diandaikan.

Nota

Ancilla dilekatkan pada sebuah tapak, memberikan nisbah 22; pengekodan Derby-Klassen pula meletakkan satu qubit tambahan bagi setiap plakuet (ganjil), untuk nisbah yang lebih kecil iaitu 3/23/2. VC digunakan di sini kerana operator pindahannya ialah operator yang Clifford pepenjurunya diberikan dalam bentuk tertutup oleh Rujukan [1].

1. Hamiltonian pada kekisi segi empat sama​

Pembinaannya sama seperti dalam 1D, tetapi dengan senarai tepi dua dimensi. Tapak mengekalkan penomboran utama-baris yang digunakan untuk rajah di atas, site(r, c) = r * cols + c, supaya ikatan mendatar menyambungkan indeks berturutan, dan ikatan menegak menyambungkan indeks yang berbeza sebanyak 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 set aliran dan bukannya dua​

Setiap sebutan lompatan dikelaskan mengikut arah anak panahnya pada kekisi. Membandingkan koordinat baris/lajur divmod bagi kedua-dua hujung memisahkan tepi mendatar daripada tepi menegak, dan tanda perbezaan menentukan orientasi. Segala yang pepenjuru (identiti, sebutan VjV_j dan VjVkV_j V_k) dimasukkan ke dalam kumpulan kelima:

Nota

Konvensyen orientasi. Nama-nama itu hanya dibaca daripada rajah di atas: anak panah ialah timur atau barat apabila ia menuju ke kanan atau kiri sepanjang baris, dan utara atau selatan apabila ia menuju ke atas atau bawah sepanjang lajur. Memandangkan penomboran berjalan utama-baris, timur dan selatan ialah arah indeks meningkat. Tiada apa-apa di hiliran yang bergantung pada nama tersebut (ia hanya melabel kumpulan), tetapi ia mudah dikelirukan, jadi wajar ditetapkan berdasarkan gambar. Rujukan [1] menamakan set aliran kekisi segi empat samanya 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 set aliran, setiap anak panah sama ada bertemu anak panah lain kepala-ke-ekor atau tidak bertemu langsung, seperti dalam 1D, jadi satu set aliran masih kalis tukar tertib sebutan demi sebutan dan evolusinya tidak membawa ralat Trotter. Yang baharu ialah set aliran kini terputus sambungan: set timur bagi kekisi rows x cols terdiri daripada rows laluan kiri-ke-kanan yang bebas. Keterputusan itu satu kelebihan dan bukannya kerumitan, seperti yang ditunjukkan oleh langkah 6.

3. Mengekod operator​

Pengekodan ialah satu fungsi yang memetakan satu operator pindahan teritlak kepada rentetan Pauli, yang diserahkan kepada map_transfer_vertex_generators(). Berbanding versi 1D, badannya mempunyai lebih banyak kes: pepenjuru, mendatar, menegak, dan orientasi songsang melalui Tkj=−VjVkTjkT_{kj} = -V_j V_k T_{jk}.

Ada satu masalah. map_transfer_vertex_generators() memanggil balik dengan sebuah penjana dan tiada yang lain, sedangkan menentukan rentetan Pauli sesuatu ikatan juga memerlukan cols untuk memulihkan koordinat baris dan lajur daripada indeks mod. Daripada menyarangkan fungsi untuk menangkapnya, functools.partial() mengikatnya, yang mengekalkan kedua-dua fungsi pada peringkat atas dan boleh dipanggil secara bebas.

>>> 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

Kelebihan mengumpulkan sebelum memetakan dapat dilihat serta-merta: setiap set aliran dikodkan kepada satu bentuk Pauli, seragam merentasi keseluruhan 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']

Empat bentuk lompatan itu berbeza, jadi setiap set aliran mendapat Clifford tersendiri dalam langkah 5. Orientasi songsang ialah Pauli yang berbeza, bukan yang sama dilabel semula; inilah sebabnya sintesis set aliran 2D memerlukan empat litar, sedangkan sintesis 1D memerlukan satu.

4. Transpilasi dengan pengekodan tersuai​

Peringkat ini ialah peringkat 1D dengan daftar qubit yang lebih lebar: CustomF2QLayout memetakan daftar NN mod ke atas 2N2N qubit, MapperFnEvolutionSynthesis membawa pengekodan, dan hujah product_formula memilih cara PauliEvolutionGate yang terhasil diurai. Bandingkan langkah 6 panduan 1D, yang membina saluran paip 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, membawa 6, 6, 6, 6, dan 22 sebutan yang dikira dalam langkah 2. Setiap satu dipetakan dan disintesis secara bebas, jadi pembahagian yang ditentukan pada peringkat fermion kekal sehingga ke dalam litar:

>>> 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 garis dasar: pengekodan dan pengumpulan kedua-duanya sudah ada, tetapi LieTrotter mensintesis setiap set aliran sebutan demi sebutan, dan kedalaman bertambah dengan kekisi.

5. Clifford berkedalaman 2 bagi setiap set aliran​

Mekanismenya sama seperti dalam 1D: konjugasikan keseluruhan set aliran yang kalis tukar tertib dengan Clifford yang memetakan setiap sebutannya kepada Pauli berpemberat-1 yang berbeza, supaya evolusi keseluruhan set hanya memerlukan putaran qubit tunggal. Dalam 1D, satu binaan bata CZ gate melakukan perkara ini.

Dalam 2D, setiap set aliran memerlukan litarnya sendiri, mengikut Bahagian IV C dan Rajah 7 Rujukan [1]. Kesemua empat beroperasi pada satu garisan kekisi pada satu masa (baris untuk timur/barat, lajur untuk utara/selatan) bersama ancilla garisan itu, dan kesemuanya mempunyai bentuk tiga bahagian yang sama:

  • Satu lapisan putaran qubit tunggal, meletakkan setiap qubit dalam asas yang diperlukan oleh set alirannya.

  • Lapisan CX pertama, sentiasa dari tapak ke ancilla pada tapak yang sama.

  • Lapisan CX kedua, menggandingkan jiran di sepanjang garisan.

Langkah 2 dan 3 ialah satu-satunya dua lapisan dua-qubit, jadi kedalaman dua-qubit ialah 22, tidak kira berapa panjang garisan itu. Ia ditulis satu fungsi bagi setiap set aliran di bawah. Putaran itu sedemikian kerana ia ditemui, dengan mengkonjugasi sebutan setiap set aliran dan menghendaki imej berpemberat-1 yang berbeza, jadi anggaplah ia sebagai diberi dan bergantunglah pada semakan yang menyusul.

Dua set mendatar ialah yang paling mudah. Sebutannya berpemberat-3, XXZ dan YYZ, dan hanya berbeza dengan SS tambahan yang menukar XX kepada YY pada tapak:

>>> 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 menegak berpemberat-4, XYXY dan YXXY, dan sewajarnya lebih sibuk: putaran berselang-seli mengikut kedudukan di sepanjang garisan, dan lapisan CX kedua juga berselang-seli, menggandingkan tapak pada kedudukan genap dan ancilla pada kedudukan 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))

Dilukis pada satu garisan tiga tapak (tapaknya dahulu, kemudian ancilla mereka), seluruh mekanisme muat dalam satu gambar. Bentuk segi tiga lebih jelas antara kedua-duanya: perubahan asas pada ancilla, kemudian dua lapisan CX, yang pertama memasangkan setiap tapak dengan ancillanya sendiri dan yang kedua mencapai tapak seterusnya di sepanjang garisan.

>>> 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>

The east Clifford on a three-site line: three H gates on the ancillas followed by two layers of two CX gates each.

Bentuk segi empat sama memerlukan lebih banyak kerja qubit tunggal (putaran berselang-seli di sepanjang garisan) dan lapisan CX keduanya berselang-seli antara menggandingkan tapak dan menggandingkan ancilla:

>>> square = QuantumCircuit(6)
>>> south_clifford(square, line, lambda mode: 3 + mode)
>>> square.draw("mpl")
<Figure size ... with 1 Axes>

The south Clifford on a three-site line: alternating single-qubit rotations followed by two CX layers, the second mixing site-site and ancilla-ancilla pairs.

Kedua-duanya berkedalaman dua lapisan CX, dan menambah tapak pada garisan melebarkannya tanpa mendalamkannya; itulah keseluruhan dakwaan kedalaman malar, yang nampak melalui pemeriksaan. Clifford barat dan utara hanya berbeza pada lapisan qubit tunggal, jadi ia tidak dilukis secara berasingan.

Apa yang sintesis masih perlukan ialah menentukan ke mana setiap sebutan berakhir. Garisan LL tapak membawa L−1L - 1 ikatan, masing-masing menyumbang satu sebutan, jadi pembantu di bawah memulangkan satu entri bagi setiap ikatan: qubit tempat sebutan itu mendarat, dan Pauli qubit tunggal yang ia menjadi. Yang penting, ini dibaca daripada konjugasi dan bukan diramal. Tidak seperti 1D, yang mana satu bata CZ memetakan Xj+1↦ZjXj+1Zj+2X_{j+1} \mapsto Z_j X_{j+1} Z_{j+2} dalam bentuk tertutup, tiada formula sedemikian di sini, dan ancilla garisan lajur tidak bersebelahan. Satu-satunya perkara yang berbeza antara set aliran ialah rentetan Pauli yang masuk, yang sudah dicetak oleh 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

Imej mesti keluar berbeza dan berpemberat-1, supaya setiap sebutan boleh diputar secara bebas pada qubitnya sendiri; kedalaman dua-qubit 22 sudah kelihatan dalam empat binaan di atas, masing-masing menghasilkan dua lapisan CX tanpa mengira panjang garisan. Di bawah, kedua-dua sifat disemak merentasi setiap set aliran, setiap garisan, dan beberapa saiz kekisi. Langkah 6 kemudian memanggil clifford_supports, jadi ia bahagian sintesis yang berfungsi dan bukan sekadar demonstrasi.

6. Sintesis yang peka set aliran​

Sintesis mendapat permulaan yang membantu. Oleh kerana Evolution memecahkan dirinya kumpulan demi kumpulan, synthesize dipanggil sekali bagi setiap set aliran (lima kali di sini, empat set lompatan ditambah kumpulan pepenjuru) supaya setiap sebutan lompatan yang dilihatnya berkongsi satu bentuk Pauli, dan satu Clifford mendiagonalkan kesemuanya. Ia hanya perlu membaca bentuk itu daripada sebutan pertama dan mengisih yang lain mengikut garisan tempat ia berada. Kemudian datang pemerhatian yang menjadikan dua dimensi lebih murah daripada yang ditakuti:

Penting

Komponen satu set aliran adalah bercerai bucu: baris set barat tidak berkongsi qubit. Jadi semua Clifford sesuatu set aliran boleh dikeluarkan bersama dan berkongsi dua lapisan dua-qubit yang sama, dan bukannya beratur satu baris demi satu baris. Satu set aliran oleh itu menelan bilangan lapisan yang tetap (dua untuk Clifford dan dua untuk songsangannya) tidak kira berapa banyak baris atau lajur kekisi itu, itulah sebabnya jumlah kedalaman akhirnya malar.

>>> 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 dalam 1D, ini sengaja ditulis untuk pengekodan ini: ia mengenali empat bentuk Pauli dan menimbulkan ralat pada apa-apa yang lain dan bukannya kembali secara senyap. Ia juga mengandaikan pembahagian satu-set-aliran-bagi-setiap-gate di atas, dan menyatakannya; lebih daripada satu bentuk lompatan dalam satu gate bermakna pengumpulan tidak dilaksanakan, yang merupakan ralat dan bukan sesuatu untuk dielakkan.

Nota

Seperti rakan 1D-nya, sintesis ini tidak mengambil bilangan langkah Trotter. Satu langkah Trotter perlu berkitar antara set aliran, tetapi transpile_trotter_step di bawah menguraikan litar fermion kepada satu gate Evolution bagi setiap set, jadi kaedah ini hanya melihat satu set pada satu masa dan hanya boleh mengulang di dalamnya, yang tidak mengubah apa-apa: mengulang satu operator hanya membahagikan evolusinya sendiri kepada kepingan yang sama.

Trotterisasi berbilang langkah oleh itu memerlukan formula hasil darab yang menghormati groups operator di dalam satu gate, sebelum penguraian. FermionicLieTrotter melakukannya, dan langkah 10 menggunakannya untuk mengukur penumpuan.

Perhatikan dua kelas yang bernama serupa di sini. LieTrotter Qiskit, yang digunakan sebagai garis dasar di atas, bertindak pada Hamiltonian yang dipetakan, yang pengumpulannya sudah hilang. FermionicLieTrotter ialah padanan fermionnya, dan ia yang masih dapat melihat set aliran.

7. Hasilnya: kedalaman malar​

Segala-galanya setakat ini dijalankan pada satu kekisi 3×33 \times 3 yang dibina dalam langkah 1. Membandingkan kedua-dua laluan sintesis merentasi kekisi yang membesar memerlukan keseluruhan urutan itu (operator, pengumpulan, pengekodan, litar, pengurus laluan) diulang bagi setiap saiz, jadi wajar dibungkus sekali:

>>> 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-dua metrik merentasi pelbagai saiz kekisi dan memplotkannya terhadap tepi kekisi menjadikan perbezaannya 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>

Two-qubit depth and CX count against lattice edge; the qubit-level LieTrotter grows linearly while the flow-set synthesis stays flat at 24.

Kedalaman dua-qubit ialah 24 dan malar dari 3×33 \times 3 hingga 8×88 \times 8 (iaitu 18 qubit hingga 128), manakala LieTrotter yang dikenakan pada Hamiltonian berpengekodan serupa bertambah secara linear dengan tepi kekisi, dari 47 kepada 92. Bilangan gate bertambah dalam kedua-duanya, kerana ada O(N)O(N) sebutan untuk dikenakan sama ada cara; apa yang dibuang oleh set aliran ialah pertumbuhan bilangan lapisan yang diduduki gate tersebut.

Belanjawan terbahagi kepada 16 untuk lompatan dan lapan untuk interaksi:

  • Setiap satu daripada empat set aliran menelan 2+22 + 2 lapisan dua-qubit: Clifford-nya masuk dan songsangannya keluar, kedua-duanya berkedalaman dua mengikut binaan, dengan hanya putaran qubit tunggal di antaranya. Itu ialah 4×4=164 \times 4 = 16, angka yang dipetik oleh Rujukan [1], kerana ia hanya mengira sebutan lompatan. Menetapkan U=0U = 0 dalam kod di atas menghasilkannya semula.

  • Kumpulan pepenjuru menambah lapan yang selebihnya. Rzz gate-nya dipadatkan kepada empat lapisan, dan setiap Rzz terurai kepada dua gate CX mengapit satu putaran qubit tunggal, jadi 4×2=84 \times 2 = 8. Empat lapisan ialah optimum: pada kekisi segi empat sama, interaksi hanya mencapai jiran terdekat, jadi setiap tapak dalaman membawa empat gate Rzz (satu bagi setiap ikatan) dan hanya boleh mengambil bahagian dalam satu pada satu masa.

Tiada apa-apa dalam kedua-dua jumlah itu merujuk kepada saiz kekisi, dan itulah tujuan keseluruhannya.

8. Sahkan litar​

Sintesis set aliran dan LieTrotter menghuraikan langkah Trotter yang sama bagi Hamiltonian terkod yang sama, jadi unitarinya mesti sepadan sehingga fasa global. Vektor keadaan rawak mengesan sebarang percanggahan dengan jauh lebih murah daripada membina unitari penuh:

Nota

Kedua-dua litar diuraikan kepada gate satu- dan dua-qubit sebelum dikenakan pada keadaan. Jika tidak diuraikan, setiap arahan Evolution peringkat tinggi memaksa Statevector membina operator tumpat penuh 22N×22N2^{2N} \times 2^{2N}nya; menguraikan dahulu membolehkannya mengenakan gate satu demi satu, yang beratus kali ganda lebih pantas di sini.

>>> 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)
Nota

Kedua-dua bahagian semakan itu penting. Pertindihan modulus-unit pada satu keadaan adalah perlu tetapi tidak mencukupi: sintesis yang tersalah fasa bergantung keadaan masih akan menunjukkannya. Menghendaki fasa yang sama merentasi beberapa keadaan rawak itulah yang menutup jurang tersebut.

9. Sahkan fizik pada subruang penstabil​

Inilah kerumitan dua dimensi. Dalam 1D, pengekodan ialah isometri, dan membandingkan spektrum dengan rujukan Jordan-Wigner sudah memadai. VC bukan isometri: ia menggunakan 2N2N qubit untuk NN mod, jadi ruang berdimensi 22N2^{2N} memuatkan satu ruang fizikal berdimensi 2N2^N. Ini bersifat generik bagi pengekodan setempat dalam dua dimensi (qubit berlebihan dikekang oleh simetri kalis tukar tertib setempat, satu bagi setiap plakuet [2] [3]) dan ia bermakna Hamiltonian terkod mewakili Hamiltonian fermion dengan setia hanya pada ruang eigen +1+1 bersama bagi kekangan tersebut.

Amaran

Perkataan penstabil mempunyai dua fungsi dalam bidang ini, dan kedua-dua maknanya tidak berkaitan. Rujukan [1] menggunakannya untuk operator pindahan kalis tukar tertib bagi satu set aliran, yang membentuk kumpulan penstabil yang didiagonalkan oleh Clifford langkah 5 (alat pengkompilan, dan makna yang dimaksudkan di mana-mana di atas). Bahagian ini menggunakannya dalam erti pembetulan ralat: operator yang ruang eigen +1}-nya ialah subruang fizikal. Rujukan [1] menandakan secara jelas pertembungan ini dan mengetepikan makna kedua; kekangan plakuet di bawah ialah sifat pengekodan VC [2], bukan hasil rangka kerja set aliran.

Penjana dibina di sini dan bukan dipetik: satu bagi setiap muka segi empat sama, dan setiap satu hanyalah gelung operator tepi terkod mengelilingi muka itu.

Operator tepi ialah objek yang wajar untuk ini. Gelung tertutup operator tepi ialah simetri bagi mana-mana Hamiltonian yang memelihara bilangan zarah, manakala gelung operator pindahan yang sepadan bukan: kedua-duanya berbeza dengan operator bucu pada tapak sumber setiap ikatan,

Tjk=i2 VjEjk⟺Ejk=−2i VjTjk ,T_{jk} = \tfrac{i}{2} \, V_j E_{jk} \qquad \Longleftrightarrow \qquad E_{jk} = -2i \, V_j T_{jk} \, ,

Mengelilingi gelung tertutup setiap tapak ialah sumber tepat satu ikatan, jadi gelung itu mengumpul satu VjV_j bagi setiap tapak, iaitu pariti setempat bagi empat tapak plakuet. Pariti setempat itulah yang membuat perbezaan: Hamiltonian melompatkan zarah merentasi sempadan plakuet, jadi ia hanya memelihara pariti global. Gelung pindahan kosong oleh itu gagal kalis tukar tertib dengannya, manakala gelung tepi (yang membawa faktor pariti) kalis tukar tertib.

Memandangkan pengekodan ditakrifkan pada operator pindahan, memetakan operator tepi memerlukan hubungan di atas. Itu ialah panggil balik lima baris, dan ia dimasukkan ke dalam map_edge_vertex_generators() sama seperti vc_action dimasukkan ke dalam 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 penjana berpemberat 66, tersebar pada dua qubit fizikal plakuetnya dan empat ancilla. Ia involutori dan kalis tukar tertib dengan Hamiltonian terkod, jadi subruang fizikal dikekalkan oleh dinamik. Kedua-dua sifat ialah pernyataan tentang operator, jadi ia disemak secara simbolik pada aljabar SparseObservable, tanpa memerlukan matriks 218×2182^{18} \times 2^{18}:

>>> 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 penstabil di tangan, perbandingan tidak memerlukan padanan asas, kerana Vj=ZjV_j = Z_j bermakna penghunian berada pada qubit fizikal yang sama dalam kedua-dua gambaran. Jadi: tulis keadaan hasil darab terus pada qubit fizikal dengan ancilla dalam ∣0⟩|0\rangle, unjurkan ke subruang penstabil, evolusikan dengan Hamiltonian terkod, dan baca ketumpatan tapak ⟨nj⟩=(1−⟨Zj⟩)/2\langle n_j \rangle = (1 - \langle Z_j \rangle)/2 daripada kedua-dua belah.

Setiap langkah di sini kekal pada peringkat vektor. Unjuran dikenakan satu faktor (1+S)/2(1 + S)/2 pada satu masa dan bukan didarabkan sepenuhnya, dan evolusi masa menggunakan expm_multiply(), yang mengira e−iHt∣ψ⟩e^{-iHt} |\psi\rangle tanpa pernah membentuk e−iHte^{-iHt}. Oleh kerana Hamiltonian terkod ialah operator jarang berdimensi 22N2^{2N}, perbezaan itulah yang menjadikan bahagian ini 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 ketumpatan terkod; ia sepadan dengan rujukan Jordan-Wigner sehingga ketepatan mesin, pada kekisi yang pengekodannya setia hanya selepas unjuran.

10. Ukur penumpuan Trotter​

Langkah 8 menetapkan bahawa kedua-dua sintesis bersetuju antara satu sama lain, yang merupakan pernyataan tentang kesetaraan dan bukan ketepatan: kedua-duanya menghuraikan satu langkah Trotter yang sama, jadi kedua-duanya membawa ralat Trotter yang sama. Mengukur ralat itu memerlukan rujukan tepat, dan langkah 9 baru sahaja membinanya.

Bilangan langkah datang daripada FermionicLieTrotter, padanan fermion bagi formula peringkat qubit yang digunakan sebagai garis dasar sebelum ini. Dikenakan pada keseluruhan operator berkumpulan sebelum penguraian, ulangannya berkitar antara empat set aliran dan kumpulan pepenjuru dan bukannya di dalam mana-mana satu daripadanya:

>>> 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

Kesetiaan meningkat secara monotonik ke arah satu apabila langkah memendek, iaitu tingkah laku yang mesti ditunjukkan oleh formula hasil darab. Memendekkan langkah hanyalah satu daripada dua tombol; FermionicSuzukiTrotter menukar ganti keduanya pada peringkat yang lebih tinggi, seperti yang diukur oleh Sintesis gate Evolution dalam ruang fermion.

Satu langkah Hamiltonian ini pada t=1t = 1 ialah penghampiran yang lemah (empat set lompatan dan interaksi tidak kalis tukar tertib) jadi baris pertama jauh daripada satu, dan itulah maksudnya: penumpuanlah yang mengesahkan binaan itu, bukan mana-mana satu baris.

Nota

Setiap faktor jadual merangkumi setiap mod kekisi ini, jadi mengecilkan faktor kepada sokongannya tidak melakukan apa-apa di sini dan indeks qubit mutlak yang dipadankan oleh FlowSetSynthesis tidak berubah. Sintesis yang ditulis untuk operator yang kumpulannya hanya menyentuh sebahagian daftar perlu mengambil kira pelabelan semula itu.

Apa yang perlu diambil​

  • Dua dimensi mengubah pengurusan butiran, bukan resipinya. Setiap bahan daripada panduan 1D terbawa tanpa diubah: groups untuk pengumpulan, map_transfer_vertex_generators() untuk pengekodan, CustomF2QLayout untuk daftar yang tidak sepadan, dan MapperFnEvolutionSynthesis untuk menyambungkannya ke dalam transpiler. Yang bertambah ialah bilangan kes di dalam setiap satu.

  • Keterpisahan itulah yang menjadikan 2D berkedalaman malar. Satu set aliran terurai kepada garisan bercerai bucu, jadi Clifford-nya dikeluarkan secara selari dan berkongsi dua lapisan dua-qubit tidak kira betapa besar kekisinya. Empat set aliran dan satu kumpulan pepenjuru kemudian memberikan belanjawan tetap: di sini 1616 untuk empat set aliran ditambah 88 untuk kumpulan interaksi pepenjuru.

  • Setiap orientasi ialah bentuk Pauli tersendiri. Tkj=−VjVkTjkT_{kj} = -V_j V_k T_{jk} menukar XXZ kepada YYZ dan XYXY kepada YXXY, jadi empat set aliran memerlukan empat Clifford. Menganggap bahawa satu Clifford meliputi kedua-dua orientasi sesuatu paksi ialah cara paling mudah untuk tersilap dalam sintesis set aliran 2D.

  • Baca sokongan daripada konjugasi. Dalam 1D, satu bata CZ memberikan imej dalam bentuk tertutup. Dalam 2D tiada formula sedemikian, dan garisan lajur mempunyai ancilla yang tidak bersebelahan, jadi tekaan terbitan tangan tentang tempat setiap sebutan mendarat adalah rapuh. Mengkonjugasi dan memeriksa hasilnya hanya memerlukan satu binaan Clifford dan tidak mungkin salah; ia juga bermakna Clifford yang sama sah yang mendaratkan sebutan di tempat lain terus berfungsi.

  • Biarkan pengumpulan membuat pengisihan. Oleh kerana Evolution memecahkan dirinya mengikut kumpulan, sintesis diserahkan satu set aliran pada satu masa dan tidak perlu memperoleh semula pembahagian: ia membaca satu bentuk Pauli daripada sebutan pertama dan memilih Clifford yang sepadan. Mengandaikan itu, dan menimbulkan ralat apabila ia tidak benar, itulah yang memastikan synthesize ringkas.

  • Bilangan langkah terletak di atas pemisahan. Sintesis di sini melihat satu set aliran bagi setiap gate, jadi ia tidak dapat membahagikan evolusi merentasi set. FermionicLieTrotter melakukannya dalam ruang fermion, sebelum penguraian, yang menjadikan ujian penumpuan mungkin sama sekali.

  • Sahkan pada subruang. Pengekodan 2N2N qubit bagi NN mod hanya setia di tempat penstabil mengatakan ia setia. Oleh kerana VC mengekalkan Vj=ZjV_j = Z_j, semakan boleh dipercayai yang paling murah ialah mengunjurkan keadaan hasil darab dan membandingkan ketumpatan: tiada pengaitan dan tiada padanan spektrum, yang tidak boleh dipercayai apabila Hamiltonian terhad adalah merosot.

Nota

Hamiltonian terkod kalis tukar tertib dengan penstabil, jadi unjuran hanya perlu dikenakan pada keadaan awal. Evolusi memelihara subruang dengan sendirinya. Pada perkakasan sebenar, ini juga yang menjadikan pengekodan berguna sebagai sumber pengesanan ralat: penstabil boleh diukur.

Seterusnya​

Dua arah memanjangkan apa yang di atas, dan kedua-duanya ialah perubahan bahan dan bukan kaedah. Yang pertama ialah pengekodan: Verstraete-Cirac dipilih di sini kerana Rujukan [1] memberikan Clifford pendiagonalannya dalam bentuk tertutup, tetapi Derby-Klassen menggunakan satu ancilla bagi setiap plakuet ganjil dan bukan satu bagi setiap tapak, untuk nisbah qubit-ke-mod 3/23/2 dan bukan 22. Operator pindahannya mempunyai bentuk Pauli yang berbeza, jadi pemindahan bermaksud menulis semula fungsi pengekodan langkah 3 dan mencari Clifford bagi setiap bentuk baharu; pengumpulan, susun atur, dan perancah sintesis terbawa tanpa disentuh.

Yang kedua ialah kekisi itu sendiri. Segala-galanya di sini mengandaikan sempadan terbuka dan satu spesies fermion. Sempadan berkala menutup setiap garisan menjadi kitaran: anak panah masih semuanya kalis tukar tertib, kerana setiap satu bertemu yang seterusnya kepala-ke-ekor, tetapi dua bentuk Clifford langkah 5 dibina untuk garisan berhujung. Kitaran LL tapak membawa LL ikatan bagi setiap orientasi manakala garisan membawa L−1L - 1, jadi ikatan lilitan tidak mempunyai tempat dalam binaan dan memerlukan pengendalian tersendiri. Model berspin lebih mudah: ia menggandakan daftar dan menambah sebutan interaksi atas-tapak pada kumpulan pepenjuru, tanpa menyentuh set aliran. Tiada satu pun mengganggu hujah kedalaman malar, tetapi kedua-duanya mengubah pengurusan butiran, yang, seperti dikatakan oleh pengajaran pertama, ialah bahagian yang dibesarkan oleh dua dimensi.

Penting

Kesambungan perkakasan tidak dibincangkan di sini. Setiap litar di atas dibina pada daftar abstrak yang mana-mana pasangan qubit boleh berinteraksi, dan kedalaman yang dilaporkan dikira dalam keadaan itu. Peranti sebenar mempunyai peta gandingan yang terhad, dan memetakan pengekodan ini ke atasnya ialah masalah besar tersendiri: daftar VC ialah graf 2N2N qubit yang gate-nya berjalan antara satu tapak dan ancillanya sendiri, antara tapak berjiran dan antara ancilla berjiran, jadi susun atur mesti mengekalkan ketiga-tiga jenis pasangan itu bersebelahan serentak. Jika tidak boleh, transpiler memasukkan gate SWAP, dan lapisan yang ditambahnya ialah apa yang dibeli semula oleh binaan kedalaman malar. Rujukan [1] juga memetik angka kedalamannya di bawah graf kesambungan segi empat sama yang diandaikan dengan CX sebagai satu-satunya pengikat asli. Mencari susun atur dan strategi penghalaan yang baik bagi pengekodan setempat pada topologi peranti tertentu adalah di luar skop panduan ini.

[1] (1,2,3,4,5,6,7,8,9,10)

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.

[2] (1,2,3,4)

F. Verstraete and J. I. Cirac, Mapping local Hamiltonians of fermions to local Hamiltonians of spins, J. Stat. Mech. 2005, P09012 (2005).

[3] (1,2)

Y.-A. Chen and Y. Xu, Equivalence between fermion-to-qubit mappings in two spatial dimensions, PRX Quantum 4, 010326 (2023).