I-simulate ang 2D Fermi-Hubbard dynamics gamit ang mga flow set
Ang mga konsepto sa gabay na ito ay kasalukuyang available lang sa Python API. Magiging available ang katumbas na functionality sa C API sa isang susunod na release.
Basahin muna ang 1D flow-set guide. Ang gabay na ito ang dalawang-dimensyong karugtong nito at hindi inuulit ang impormasyong nabuo roon: mga transfer at vertex operator, kung ano ang flow set, kung paano nananatili ang mga groups sa circuit, at kung paano ikinakabit ang custom na encoding sa Transpiler sa pamamagitan ng MapperFnEvolutionSynthesis.
Tingnan ang gabay sa transpilation para sa mga yugto ng Transpiler na tinutukoy sa kabuuan, at ang gabay sa mga mapper para sa pangkalahatang recipe ng pagsulat ng custom na mapper.
Nagtapos ang 1D na gabay sa pagbanggit na kailangan ang mga lokal na encoding kapag nagtatrabaho sa dalawang dimensyon. Ginagawa ng gabay na ito ang hakbang na iyon, para sa dalawang-dimensyong Fermi-Hubbard model sa isang square lattice, gamit ang Verstraete-Cirac (VC) encoding [2].
May tatlong pagkakaiba mula sa 1D na problema, na bumubuo sa batayan ng gawaing ito:
-
Dumarami ang mga flow set. Ang isang chain ay may dalawang oryentasyon ng arrow; ang square lattice ay may apat (east, west, north, south), at ang bawat isa ay unyon ng mga vertex-disjoint na directed path (isa bawat row o column ng lattice) sa halip na iisang path.
-
Nagkakaroon ang encoding ng ancilla bawat site, hindi isa para sa buong sistema, na nagbibigay ng
ratio ng qubit-sa-mode na . Ang mga transfer operator ay nagiging weight-3 (pahalang) at weight-4 (patayo) na mga Pauli, at ang bawat isa sa apat na flow set ay minamapa sa sarili nitong hugis ng Pauli, kaya ang bawat isa ay nangangailangan ng sarili nitong diagonalizing na Clifford.
-
Tapat lang ang naka-encode na Hamiltonian sa isang subspace: ang magkasamang
eigenspace ng mga plaquette constraint na dala ng bawat 2D na lokal na encoding [2] [3]. Sa 1D, ang encoding ay payak na isometry at ang pag-verify ay pagsusuri ng spectrum. Dito, kailangang i-project ang paghahambing, gaya ng inilalarawan ng hakbang 9.
Ang pakinabang ay kapareho ng hugis sa 1D: ang two-qubit depth ng isang hakbang ng Trotter ay nagiging pirmi sa laki ng lattice, sa ( para sa hopping, tugma sa Ref. [1], dagdag ang para sa interaksyon), habang ang term-by-term na Trotterization ng parehong naka-encode na Hamiltonian ay linear na lumalaki. Lumalaki pa rin ang bilang ng Gate kasabay ng bilang ng mga bond, siyempre; ang nagiging hindi nakadepende sa laki ay kung ilang layer ang sinasakop ng mga Gate na iyon. Ang mga depth sa kabuuan ay binibilang sa isang abstract, ganap na konektadong register; ang pagmamapa ng encoding sa restricted na coupling map ng isang device ay sarili nitong problema, na tinatalakay sa dulo.
Ang lattice at ang apat na oryentasyon nito
Ang unang pagbabago ay nakikita sa directed na interaction graph. Binibilang ang mga site ayon sa row-major na pagkakasunod, site(r, c) = r * cols + c, at ang bawat bond ay may parehong oryentasyon, gaya ng chain sa 1D, pero ngayon ang mga bond ay tumatakbo sa dalawang axis:

Ang commutation rule ay ang sa 1D, walang pagbabago: ang dalawang transfer operator na may iisang site ay nagko-commute kapag dumadaloy ang mga arrow sa pamamagitan nito at nag-aanticommute kapag nagbabanggaan. Gayunpaman, ang isang site ngayon ay may hanggang apat na incident na bond sa halip na dalawa, kaya ang mga arrow sa pamamagitan nito ay maaaring magbanggaan sa mas maraming paraan, at ang hanay ng mga arrow na hindi kailanman nagbabanggaan ay hindi na isang path lang. Ang pagbasa sa mga pahalang na arrow mula kaliwa pakanan ay nagbibigay ng isang ganoong set, ang mga patayong arrow mula itaas pababa ay isa pa, at ang pagbaliktad ng bawat isa ay nagbibigay ng dalawa pa. Ito ang mga east, west, north, at south na flow set na nag-oorganisa ng lahat ng susunod.
Ang Verstraete-Cirac encoding
Ipinapares ng Verstraete-Cirac (VC) ang bawat fermionic na mode sa sarili nitong auxiliary na qubit. Sa na site ng lattice, ang mode ay nasa qubit at ang ancilla nito ay nasa qubit , kaya ang register ay may na qubit. Nananatiling kasimple hangga't maaari ang mga vertex operator,
na siyang nagpapadali sa direktang pagbasa ng mga occupation mamaya. Ang mga transfer operator ay nakadepende sa oryentasyon ng edge. Para sa pahalang na edge na kung saan ang ay ang kanlurang site, at isang patayong edge kung saan ang ay ang hilagang site,
isang weight-3 na “triangle” at isang weight-4 na “square”. Mahalaga ang pagtukoy sa bawat arrow: kapag ang ang kanlurang site, ang ay nakaturo sa east, pero kapag ang ang hilagang site, nakaturo ito sa south. Ang mga reverse na oryentasyon ay sumusunod sa identity na , na ginagawang YYZ ang triangle na XXZ, at YXXY ang square na XYXY. Ang apat na magkakaibang label na iyon ang apat na flow set, at ang natitirang bahagi ng gabay na ito ay sinusubaybayan ang mga ito.
Hindi eksaktong ito ang Eq. (A1) ng Ref. [1], na nagsasabing “hanggang sa single-qubit rotations” at may tatlong pagkakaiba na dapat banggitin, dahil mapapansin ito ng sinumang magsusuri sa paper:
-
Ang prefactor: Walang taglay nito ang Eq. (A1). Ang dito ang nagpapatotoo na , na tugma sa normalization na na ginagamit ng
TransferVertexOperator. Ang kailangan lang ng paper sa mga transfer operator nito ay nasa loob ng , kung saan ang scale factor ay nasasama sa rotation angle; dito, kailangan silang maging mga operator na sumusunod sa isang identity. -
Ang orientation: Pinipili ng Eq. (A1) ang mga letra ayon sa row parity ng , na para sa horizontal na edge ay pareho sa dalawang dulo, kaya hindi nito nakikilala ang sa . Hiwalay na binabawi ng paper ang orientation sa pamamagitan ng pag-conjugate gamit ang isang layer ng . Sa halip, isinasama ito ng gabay na ito sa encoding, at hinuhugot ang reverse mula sa , kaya napupunta ang dalawang orientation sa magkaibang Pauli sa step 3.
-
Ang vertical na hugis: Pinapalitan ng Eq. (A1) ang
XXXXatYYYYayon sa row parity, samantalang ang gabay na ito ay gumagamit ng iisangXYXY. Dalawa itong paraan para sa iisang layunin: isang asymmetry ng mga letra na nagpapagawa sa vertical na edge na tamang mag-clash sa mga horizontal na edge na nagtatagpo rito. Huwag ituring na malayang pagpili ng basis ang pagkakaiba. Ang pag-alis ng parity alternation at paggamit ng iisangXXXXay tuluyang sumisira sa algebra, kaya sinusuri ang magkakaibang letra sa halip na ipagpalagay lang.
Nakakabit ang ancilla sa isang site, kaya ang ratio ay ; ang Derby-Klassen encoding naman ang naglalagay ng isang auxiliary qubit bawat (odd) plaquette, para sa mas maliit na ratio na . Ginagamit dito ang VC dahil ang mga transfer operator nito ang may mga diagonalizing Clifford na ibinibigay ng Ref. [1] sa closed form.
1. Ang Hamiltonian sa isang square lattice
Pareho ang konstruksyon sa 1D, pero may two-dimensional na edge list. Pinapanatili ng mga site ang row-major na pagnunumero na ginamit sa figure sa itaas, site(r, c) = r * cols + c, kaya ang mga horizontal na bond ay nag-uugnay ng magkakasunod na index, at ang mga vertical na bond ay nag-uugnay ng mga index na magkaiba ng 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. Apat na flow set sa halip na dalawa
Ang bawat hopping term ay inuuri ayon sa direksyong tinuturo ng arrow nito sa lattice. Ang paghahambing ng divmod na row/column coordinate ng dalawang dulo ang naghihiwalay sa horizontal at vertical na mga edge, at ang sign ng pagkakaiba ang pumipili ng orientation. Lahat ng diagonal (ang identity, ang at ang na mga term) ay napupunta sa ikalimang grupo:
Orientation convention. Ang mga pangalan ay basta binasa mula sa figure sa itaas: ang arrow ay east o west depende kung nakaturo ito pakanan o pakaliwa sa isang row, at north o south depende kung nakaturo ito pataas o pababa sa isang column. Dahil row-major ang pagnunumero, ang east at south ang mga direksyong tumataas ang index. Walang nakadepende sa mga pangalan sa susunod na mga hakbang (mga label lang ito ng grupo), pero madaling malito ang mga ito, kaya mainam na iayon sa larawan. Pareho ang pagpapangalan ng Ref. [1] sa mga flow set nito sa square lattice.
>>> 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
Sa loob ng isang flow set, bawat arrow ay alinman sa nagtatagpo ng ulo-sa-buntot sa isa pa o hindi talaga nagtatagpo, gaya sa 1D, kaya ang isang flow set ay nagko-commute pa rin term-by-term at walang Trotter error ang evolution nito. Ang bago ay hindi na konektado ang isang flow set: ang east set ng isang rows x cols na lattice ay binubuo ng rows na magkakahiwalay na landas na kaliwa-pakanan. Biyaya ang pagkakahiwalay na iyon at hindi komplikasyon, gaya ng ipapakita ng step 6.
3. I-encode ang operator
Ang encoding ay isang function na nagmamapa ng isang generalized transfer operator sa isang Pauli string, na ipinapasa sa map_transfer_vertex_generators(). Kumpara sa 1D na bersyon, mas maraming kaso ang katawan nito: diagonal, horizontal, vertical, at ang mga reverse orientation sa pamamagitan ng .
May isang problema. Ang map_transfer_vertex_generators() ay tumatawag pabalik gamit ang isang generator lang at wala nang iba, samantalang ang pagpapasya sa Pauli string ng isang bond ay nangangailangan din ng cols para makuha ang row at column coordinate mula sa isang mode index. Sa halip na i-nest ang mga function para i-close over ito, binibigkis ito ng functools.partial(), kaya nananatili ang dalawang function sa top level at maaaring tawagin nang hiwalay.
>>> 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
Makikita agad ang silbi ng pag-grupo bago mag-map: ang bawat flow set ay nag-e-encode sa iisang Pauli shape, pare-pareho sa buong 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']
Magkakaiba ang apat na hopping shape, kaya ang bawat flow set ay may sariling Clifford sa step 5. Ang mga reverse orientation ay magkakaibang Pauli, hindi ang parehong Pauli na nilagyan lang ng bagong label; kaya ang 2D na flow-set synthesis ay nangangailangan ng apat na circuit, samantalang isa lang ang kailangan ng 1D na synthesis.
4. I-transpile gamit ang custom na encoding
Ang yugtong ito ay ang 1D na bersyon na may mas malapad na qubit register: minamapa ng CustomF2QLayout ang -mode na register sa na qubit, dala ng MapperFnEvolutionSynthesis ang encoding, at pinipili ng product_formula argument nito kung paano idi-decompose ang nagreresultang PauliEvolutionGate. Ihambing ang step 6 ng 1D na gabay, na bumubuo ng parehong pipeline para sa iisang 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
Limang Gate, na may 6, 6, 6, 6, at 22 na term na binilang sa step 2. Bawat isa ay minamapa at sini-synthesize nang hiwalay, kaya ang partition na napagpasyahan sa fermionic na antas ay nananatili hanggang sa 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)
Iyan ang baseline: nakalagay na ang encoding at ang pag-grupo, pero sini-synthesize ng LieTrotter ang bawat flow set term by term, at lumalaki ang depth kasabay ng lattice.
5. Isang depth-2 na Clifford bawat flow set
Pareho ang mekanismo sa 1D: i-conjugate ang buong commuting na flow set gamit ang isang Clifford na nagmamapa sa bawat term nito sa isang natatanging weight-1 na Pauli, kaya ang evolution ng buong set ay nagkakahalaga lang ng single-qubit rotations. Sa 1D, isang brickwork ng CZ na mga gate ang gumawa nito.
Sa 2D, ang bawat flow set ay nangangailangan ng sariling circuit, ayon sa Section IV C at Fig. 7 ng Ref. [1]. Lahat ng apat ay kumikilos sa isang linya ng lattice sa bawat pagkakataon (isang row para sa east/west, isang column para sa north/south) kasama ang mga ancilla ng linyang iyon, at lahat ng apat ay may parehong tatlong-bahaging hugis:
-
Isang layer ng single-qubit rotations, na naglalagay sa bawat qubit sa basis na kailangan ng flow set nito.
-
Isang unang
CXlayer, laging site-papuntang-ancilla sa parehong site. -
Isang pangalawang
CXlayer, na nag-uugnay ng mga magkakapitbahay sa kahabaan ng linya.
Ang mga step 2 at 3 lang ang dalawang two-qubit layer, kaya ang two-qubit depth ay , gaano man kahaba ang linya. Isinulat ang mga ito nang isang function bawat flow set sa ibaba. Ang mga rotation ay ganoon dahil natuklasan ang mga ito, sa pamamagitan ng pag-conjugate sa mga term ng bawat flow set at pag-require ng natatanging weight-1 na mga imahe, kaya ituring silang ibinigay at umasa sa pagsusuring kasunod.
Ang dalawang horizontal na set ang pinakasimple. Weight-3 ang mga term nila, XXZ at YYZ, at nagkakaiba lang sa dagdag na na nagpapalit ng sa sa mga 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))
Ang dalawang vertical na set ay weight-4, XYXY at YXXY, at mas abala nang naaayon: nag-aalternate ang mga rotation ayon sa posisyon sa kahabaan ng linya, at nag-aalternate din ang pangalawang CX layer, na nag-uugnay ng mga site sa even na posisyon at ng mga ancilla sa odd na posisyon (o kabaligtaran):
>>> 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))
Kapag iginuhit sa isang linyang may tatlong site (mga site muna, saka ang mga ancilla nila), magkakasya ang buong mekanismo sa isang larawan. Ang triangle na hugis ang mas malinaw sa dalawa: isang basis change sa mga ancilla, saka dalawang CX layer, kung saan ang una ay nagpapares sa bawat site sa sarili nitong ancilla at ang pangalawa ay umaabot sa susunod na site sa linya.
>>> 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>
Ang square na hugis ay nangangailangan ng mas maraming single-qubit na gawain (nag-aalternate ang mga rotation sa linya) at ang pangalawang CX layer nito ay nag-aalternate sa pag-uugnay ng mga site at pag-uugnay ng mga ancilla:
>>> square = QuantumCircuit(6)
>>> south_clifford(square, line, lambda mode: 3 + mode)
>>> square.draw("mpl")
<Figure size ... with 1 Axes>
Parehong dalawang CX layer ang lalim ng dalawa, at ang pagdagdag ng mga site sa linya ay nagpapalapad sa mga ito nang hindi pinalalalim; iyan ang buong constant-depth na pahayag, nakikita sa simpleng pagsisiyasat. Ang west at north na mga Clifford ay nagkakaiba lang sa kanilang single-qubit na mga layer, kaya hindi na sila iginuhit nang hiwalay.
Ang kailangan pa ng synthesis ay alamin kung saan napupunta ang bawat term. Ang isang linya ng na site ay may na bond, bawat isa ay may isang term, kaya ang helper sa ibaba ay nagbabalik ng isang entry bawat bond: ang qubit kung saan lumapag ang term, at ang single-qubit na Pauli na naging ito. Mahalaga, binabasa ang mga ito mula sa conjugation sa halip na hinuhulaan. Hindi tulad ng 1D, kung saan ang isang CZ brickwork ay nagmamapa ng sa closed form, walang ganoong formula dito, at ang mga ancilla ng isang column line ay hindi magkakadikit. Ang tanging nagkakaiba sa pagitan ng mga flow set ay ang Pauli string na pumapasok, na naipakita na ng step 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
Ang mga imahe ay dapat lumabas na natatangi at weight-1, para ang bawat term ay maaaring i-rotate nang hiwalay sa sarili nitong qubit; ang two-qubit depth na ay kita na sa apat na konstruksyon sa itaas, bawat isa ay naglalabas ng dalawang CX layer anuman ang haba ng linya. Sa ibaba, sinusuri ang dalawang katangian sa bawat flow set, bawat linya, at ilang laki ng lattice. Pagkatapos, tatawagin ng Step 6 ang clifford_supports, kaya ito ay gumaganang bahagi ng synthesis at hindi lang demonstrasyon.
6. Flow-set-aware na synthesis
May malaking pakinabang ang synthesis sa simula. Dahil hinahati ng Evolution ang sarili nito grupo por grupo, ang synthesize ay tinatawag nang isang beses bawat flow set (limang beses dito, apat na hopping set kasama ang diagonal na grupo) kaya ang bawat hopping term na nakikita nito ay may parehong Pauli shape, at isang Clifford ang nagda-diagonalize sa lahat. Kailangan lang nitong basahin ang hugis mula sa unang term at ayusin ang iba ayon sa linyang kinalalagyan nila. Saka ang obserbasyon na ginagawang mas mura ang dalawang dimensyon kaysa sa inaakala:
Ang mga bahagi ng isang flow set ay vertex-disjoint: walang pinagsasaluhang qubit ang mga row ng west set. Kaya lahat ng Clifford ng isang flow set ay maaaring ilabas nang sabay-sabay at pareho nilang ginagamit ang parehong dalawang two-qubit layer, sa halip na pumila nang isa-isa kada row. Kaya ang isang flow set ay nagkakahalaga ng takdang bilang ng layer (dalawa para sa Clifford at dalawa para sa inverse nito) gaano man karami ang row o column ng lattice, kaya nagiging constant ang kabuuang depth.
>>> 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)
Gaya sa 1D, sadyang isinulat ito para sa encoding na ito: kinikilala nito ang apat na Pauli shape at nagtataas ng error sa anupamang iba sa halip na tahimik na bumalik sa iba. Ipinapalagay din nito ang hati ng isang-flow-set-bawat-gate sa itaas, at sinasabi ito; ang higit sa isang hopping shape sa iisang gate ay nangangahulugang hindi nailapat ang pag-grupo, na isang error at hindi bagay na iwasan.
Tulad ng 1D na katapat nito, walang bilang ng Trotter step ang synthesis na ito. Ang isang Trotter step ay kailangang umikot sa pagitan ng mga flow set, pero dini-decompose ng transpile_trotter_step sa ibaba ang fermionic na circuit sa isang Evolution na gate bawat set, kaya isang set lang ang nakikita ng method na ito sa bawat pagkakataon at maaari lang itong umulit sa loob nito, na walang binabago: ang pag-ulit ng isang operator ay hahatiin lang ang sarili nitong evolution sa pantay na mga piraso.
Kaya ang multi-step na Trotterization ay nangangailangan ng product formula na iginagalang ang groups ng operator sa loob ng iisang gate, bago ang decomposition. Ginagawa iyon ng FermionicLieTrotter, at ginagamit ito ng step 10 para sukatin ang convergence.
Pansinin ang dalawang magkahawig na pangalan ng klase na narito. Ang LieTrotter ng Qiskit, na ginamit bilang baseline sa itaas, ay kumikilos sa namapang Hamiltonian, kung saan wala na ang pag-grupo. Ang FermionicLieTrotter ang fermionic na katapat nito, at ito ang nakakakita pa rin sa mga flow set.
7. Ang resulta: constant na depth
Lahat ng nauna ay tumakbo sa iisang na lattice na binuo sa step 1. Ang paghahambing ng dalawang synthesis path sa lumalaking mga lattice ay nangangailangan ng buong sekwensiyang iyon (operator, pag-grupo, encoding, circuit, pass manager) na ulitin bawat laki, kaya sulit na i-wrap ito nang isang beses:
>>> 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)
Ang pagkolekta ng parehong sukatan sa isang saklaw ng mga laki ng lattice at pag-plot ng mga ito laban sa gilid ng lattice ay nagpapalinaw ng pagkakaiba:
>>> 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>
Ang two-qubit depth ay 24 at constant mula hanggang (iyon ay 18 qubit hanggang 128), samantalang ang LieTrotter na inilapat sa magkaparehong naka-encode na Hamiltonian ay lumalaki nang linear kasabay ng gilid ng lattice, mula 47 hanggang 92. Lumalaki ang gate count sa dalawa, dahil may na term na ilalapat sa alinmang paraan; ang inaalis ng mga flow set ay ang paglaki ng bilang ng layer na sinasakop ng mga gate na iyon.
Ang budget ay nahahati sa 16 para sa hopping at walo para sa interaction:
-
Ang bawat isa sa apat na flow set ay nagkakahalaga ng na two-qubit layer: ang Clifford nito papasok at ang inverse palabas, parehong depth dalawa ayon sa konstruksyon, na may single-qubit rotations lang sa pagitan. Iyon ay , na siyang bilang na binabanggit ng Ref. [1], dahil mga hopping term lang ang binibilang nito. Ang pagtatakda ng sa code sa itaas ay nagpapalabas nito.
-
Ang diagonal na grupo ang nagdadagdag ng natitirang walo. Ang mga
Rzzna gate nito ay nagkakasya sa apat na layer, at bawatRzzay nagde-decompose sa dalawangCXna gate sa paligid ng isang single-qubit rotation, kaya . Optimal ang apat na layer: sa isang square lattice, ang interaction ay umaabot lang sa pinakamalapit na kapitbahay, kaya bawat interior na site ay may apat naRzzna gate (isa bawat bond) at isa lang ang maaari nitong salihan sa isang pagkakataon.
Walang binabanggit ang alinmang kabuuan tungkol sa laki ng lattice, na siyang buong punto.
8. I-verify ang circuit
Ang flow-set synthesis at ang LieTrotter ay naglalarawan ng parehong Trotter step ng parehong naka-encode na Hamiltonian, kaya dapat magkatugma ang mga unitary nila hanggang sa global phase. Mas mura ang mga random statevector sa pagtuklas ng anumang hindi pagkakatugma kaysa sa pagbuo ng buong unitary:
Parehong dini-decompose ang dalawang circuit pababa sa one- at two-qubit na mga gate bago ilapat sa isang state. Kapag hindi dini-decompose, ang bawat high-level na Evolution na instruction ay pipilit sa Statevector na buuin ang buong na dense operator nito; ang pag-decompose muna ay nagpapahintulot dito na ilapat ang mga gate nang isa-isa, na ilang daang beses na mas mabilis dito.
>>> 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)
Parehong mahalaga ang dalawang kalahati ng pagsusuring iyon. Ang unit-modulus na overlap sa isang state ay kailangan pero hindi sapat: ang synthesis na nagkamali sa state-dependent na phase ay makikita pa rin ito. Ang pag-require ng parehong phase sa ilang random na state ang nagsasara ng puwang.
9. I-verify ang physics sa stabilizer subspace
Ito ang kulubot ng dalawang dimensyon. Sa 1D, isometry ang encoding, at sapat na ang paghahambing ng mga spectrum laban sa Jordan-Wigner na reference. Hindi isometry ang VC: gumagamit ito ng na qubit para sa na mode, kaya ang -dimensional na espasyo ay naglalaman ng -dimensional na pisikal. Karaniwan ito sa mga local encoding sa dalawang dimensyon (ang sobrang mga qubit ay nilimitahan ng mga local commuting na symmetry, isa bawat plaquette [2] [3]) at nangangahulugan itong ang naka-encode na Hamiltonian ay kumakatawan lang nang tapat sa fermionic sa magkasanib na eigenspace ng mga constraint na iyon.
Ang salitang stabilizer ay may dalawang gamit sa paksang ito, at hindi magkaugnay ang dalawang kahulugan. Ginagamit ito ng Ref. [1] para sa mga commuting na transfer operator ng isang flow set, na bumubuo ng stabilizer group na dina-diagonalize ng mga Clifford ng step 5 (isang compilation device, at ang kahulugang ginamit sa lahat ng nasa itaas). Ginagamit ito ng seksyong ito sa kahulugang error-correction: mga operator na ang eigenspace ang pisikal na subspace. Tahasang binabanggit ng Ref. [1] ang banggaan na ito at isinasantabi ang pangalawang kahulugan; ang mga plaquette constraint sa ibaba ay katangian ng VC encoding [2], hindi resulta ng flow-set framework.
Ang mga generator ay binuo dito sa halip na sipiin: isa bawat square na mukha, at bawat isa ay ang naka-encode na edge-operator loop sa paligid ng mukhang iyon.
Ang mga edge operator ang natural na bagay para rito. Ang saradong loop ng mga ito ay symmetry ng anumang Hamiltonian na nagpapanatili ng bilang ng particle, samantalang ang katumbas na loop ng mga transfer operator ay hindi: nagkakaiba ang dalawa ng isang vertex operator sa source site ng bawat bond,
Sa paligid ng saradong loop, bawat site ay source ng eksaktong isang bond, kaya ang loop ay nangongolekta ng isang bawat site, na siyang local parity ng apat na site ng plaquette. Ang local parity na iyon ang gumagawa ng pagkakaiba: ang Hamiltonian ay nagha-hop ng mga particle sa hangganan ng plaquette, kaya global parity lang ang pinapanatili nito. Kaya ang hubad na transfer loop ay hindi nagko-commute rito, habang ang edge loop (na may parity factor) ay nagko-commute.
Dahil ang encoding ay nakadefine sa mga transfer operator, ang pagmamapa ng isang edge operator ay nangangailangan ng relasyon sa itaas. Isa itong limang-linyang callback, at pumapasok ito sa map_edge_vertex_generators() nang eksakto kung paano pumapasok ang vc_action sa 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))]
Bawat generator ay may weight na , nakakalat sa dalawa sa mga pisikal na qubit ng plaquette nito at apat na ancilla. Involutory ang mga ito at nagko-commute sa naka-encode na Hamiltonian, kaya napapanatili ng dynamics ang pisikal na subspace. Pareho itong mga pahayag tungkol sa mga operator, kaya sinusuri ang mga ito nang symbolic sa SparseObservable na algebra, walang kailangang na matrix:
>>> 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
Kapag nasa kamay na ang mga stabilizer, hindi na kailangan ng basis correspondence ang paghahambing, dahil ang ay nangangahulugang nasa parehong pisikal na qubit ang mga occupation sa dalawang larawan. Kaya: sumulat ng product state nang direkta sa mga pisikal na qubit na may mga ancilla sa , i-project ito sa stabilizer subspace, i-evolve gamit ang naka-encode na Hamiltonian, at basahin ang mga site density na sa magkabilang panig.
Bawat hakbang dito ay nananatili sa antas ng vector. Inilalapat ang projector nang isang na factor bawat pagkakataon sa halip na i-multiply out, at ang time evolution ay gumagamit ng expm_multiply(), na kumukuwenta ng nang hindi binubuo ang . Dahil ang naka-encode na Hamiltonian ay sparse na -dimensional na operator, ang pagkakaibang iyon ang nagpapanatiling mura sa seksyong ito.
>>> 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
Iyan ang mga naka-encode na density; tugma ang mga ito sa Jordan-Wigner na reference hanggang machine precision, sa isang lattice na ang encoding ay tapat lang pagkatapos ng projection.
10. Sukatin ang Trotter convergence
Itinatag ng Step 8 na magkatugma ang dalawang synthesis, na pahayag tungkol sa pagkakapareho at hindi sa katumpakan: parehong inilalarawan ng dalawa ang iisang Trotter step, kaya pareho ang Trotter error nila. Ang pagsukat sa error na iyon ay nangangailangan ng eksaktong reference, at nakabuo na ng isa ang step 9.
Ang bilang ng step ay galing sa FermionicLieTrotter, ang fermionic na katapat ng qubit-level na formula na ginamit bilang baseline kanina. Kapag inilapat sa buong naka-grupong operator bago ang decomposition, ang pag-ulit nito ay umiikot sa pagitan ng apat na flow set at ng diagonal na grupo sa halip na sa loob ng alinman sa mga iyon:
>>> 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
Umaakyat nang monotonic ang fidelity papunta sa isa habang umiikli ang mga step, na siyang dapat ipakita ng isang product formula. Ang pagpapaikli ng mga step ay isa lang sa dalawang knob; ang FermionicSuzukiTrotter ay nagpapalitan ng mga ito sa mas mataas na order, gaya ng sinusukat ng Synthesize an Evolution gate in fermionic space.
Ang isang step ng Hamiltonian na ito sa ay mahinang approximation (hindi nagko-commute ang apat na hopping set at ang interaction) kaya malayo sa isa ang unang row, at iyon ang punto: ang convergence ang nagpapatunay sa konstruksyon, hindi ang anumang isang row.
Ang bawat factor ng schedule ay sumasaklaw sa bawat mode ng lattice na ito, kaya ang pagpapakitid ng isang factor sa support nito ay walang epekto dito at hindi nagbabago ang absolute na mga qubit index na tinutugma ng FlowSetSynthesis. Ang synthesis na isinulat para sa operator na ang mga grupo ay humahawak lang sa bahagi ng register ay kailangang isaalang-alang ang relabeling na iyon.
Mga dapat tandaan
-
Binabago ng dalawang dimensyon ang bookkeeping, hindi ang recipe. Bawat sangkap mula sa 1D na gabay ay dinadala nang walang pagbabago:
groupspara sa pag-grupo,map_transfer_vertex_generators()para sa encoding,CustomF2QLayoutpara sa hindi magkatugmang register, atMapperFnEvolutionSynthesispara ikabit ito sa transpiler. Ang lumaki ay ang bilang ng mga kaso sa loob ng bawat isa. -
Ang disjointness ang nagpapa-constant-depth sa 2D. Ang isang flow set ay nagde-decompose sa vertex-disjoint na mga linya, kaya ang mga Clifford nito ay inilalabas nang parallel at pinagsasaluhan ang dalawang two-qubit layer gaano man kalaki ang lattice. Ang apat na flow set at isang diagonal na grupo ay nagbibigay ng takdang budget: dito ay para sa apat na flow set kasama ang para sa diagonal na interaction group.
-
Bawat orientation ay sariling Pauli shape. Ang ay ginagawang
YYZangXXZatYXXYangXYXY, kaya ang apat na flow set ay nangangailangan ng apat na Clifford. Ang pag-aakalang isang Clifford ang sumasaklaw sa dalawang orientation ng isang axis ang pinakamadaling paraan para magkamali sa 2D na flow-set synthesis. -
Basahin ang mga support mula sa conjugation. Sa 1D, ang isang
CZbrickwork ay nagbibigay ng mga imahe sa closed form. Sa 2D, walang ganoong formula, at ang mga column line ay may hindi magkakadikit na ancilla, kaya marupok ang hand-derived na hula kung saan lalapag ang bawat term. Ang pag-conjugate at pagsisiyasat sa resulta ay nagkakahalaga ng isangCliffordna konstruksyon at hindi maaaring magkamali; nangangahulugan din ito na ang isang katumbas na validong Clifford na naglalapag ng mga term sa ibang lugar ay patuloy na gumagana. -
Hayaang ang pag-grupo ang mag-ayos. Dahil hinahati ng
Evolutionang sarili nito kada grupo, ang synthesis ay binibigyan ng isang flow set sa bawat pagkakataon at hindi na kailangang kuwentahin muli ang partition: binabasa nito ang isang Pauli shape mula sa unang term at pinipili ang katugmang Clifford. Ang pag-aakalang iyon, at ang pagtataas ng error kapag hindi ito totoo, ang nagpapanatiling maikli sasynthesize. -
Nasa itaas ng hati ang bilang ng step. Ang synthesis dito ay nakakakita ng isang flow set bawat gate, kaya hindi nito mahahati ang evolution sa mga set. Ginagawa iyon ng
FermionicLieTrottersa fermionic space, bago ang decomposition, na siyang nagpapaging-posible sa convergence test. -
I-verify sa subspace. Ang -qubit na encoding ng mode ay tapat lamang kung saan sinasabi ng mga stabilizer na ito ay tapat. Dahil pinapanatili ng VC ang , ang pinakamurang maaasahang pagsusuri ay i-project ang isang product state at ihambing ang mga density: walang intertwiner at walang spectral matching, na hindi maaasahan kapag degenerate ang restricted na Hamiltonian.
Nagko-commute ang naka-encode na Hamiltonian sa mga stabilizer, kaya ang projection ay kailangan lang ilapat sa paunang state. Ang evolution mismo ang nagpapanatili sa subspace. Sa totoong hardware, ito rin ang nagpapagamit sa encoding bilang resource sa error-detection: maaaring sukatin ang mga stabilizer.
Susunod
Dalawang direksyon ang nagpapalawak sa nasa itaas, at parehong pagbabago ng sangkap at hindi ng paraan. Ang una ay ang encoding: pinili dito ang Verstraete-Cirac dahil ibinibigay ng Ref. [1] ang mga diagonalizing Clifford nito sa closed form, pero gumagastos ang Derby-Klassen ng isang ancilla bawat odd na plaquette sa halip na isa bawat site, para sa qubit-to-mode ratio na sa halip na . Magkakaiba ang Pauli shape ng mga transfer operator nito, kaya ang pag-port ay nangangahulugan ng pagsulat muli ng encoding function ng step 3 at paghahanap ng Clifford para sa bawat bagong shape; ang pag-grupo, ang layout, at ang synthesis scaffolding ay dinadala nang walang pagbabago.
Ang pangalawa ay ang lattice mismo. Ipinapalagay ng lahat dito ang open na mga hangganan at isang uri lang ng fermion. Ang periodic na mga hangganan ay nagsasara sa bawat linya bilang cycle: nagko-commute pa rin ang lahat ng arrow, dahil bawat isa ay nagtatagpo sa susunod ng ulo-sa-buntot, pero ang dalawang Clifford shape ng step 5 ay ginawa para sa linyang may mga dulo. Ang cycle ng na site ay may na bond bawat orientation samantalang ang linya ay may , kaya walang lugar ang wrap-around na bond sa konstruksyon at nangangailangan ng sariling pagtrato. Mas madali ang spinful na modelo: dinodoble nito ang register at nagdaragdag ng on-site na interaction term sa diagonal na grupo, nang hindi ginagalaw ang mga flow set. Walang nakakagambala sa constant-depth na argumento, pero parehong binabago ang bookkeeping, na, gaya ng sinasabi ng unang takeaway, ay ang bahaging pinalalaki ng dalawang dimensyon.
Hindi tinutugunan dito ang hardware connectivity. Bawat circuit sa itaas ay binuo sa abstract na register kung saan anumang pares ng qubit ay maaaring mag-interact, at ang mga iniulat na depth ay binibilang sa ganoong setting. Ang totoong mga device ay may limitadong coupling map, at ang pagmamapa ng encoding na ito sa isa ay malaking problema sa sarili nito: ang VC register ay isang -qubit na graph na ang mga gate ay tumatakbo sa pagitan ng isang site at sarili nitong ancilla, sa pagitan ng magkakapitbahay na site at sa pagitan ng magkakapitbahay na ancilla, kaya ang isang layout ay kailangang panatilihing magkatabi ang lahat ng tatlong uri ng pares nang sabay. Kung hindi, nagsisingit ang transpiler ng mga SWAP na gate, at ang mga layer na idinaragdag ng mga ito ang binabawi ng constant-depth na konstruksyon. Binabanggit din ng Ref. [1] ang depth figure nito sa ilalim ng ipinapalagay na square connectivity graph na CX lang ang native na entangler. Ang paghahanap ng mahusay na mga layout at routing strategy para sa mga local encoding sa isang partikular na device topology ay lampas sa saklaw ng gabay na ito.
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 and J. I. Cirac, Mapping local Hamiltonians of fermions to local Hamiltonians of spins, J. Stat. Mech. 2005, P09012 (2005).
Y.-A. Chen and Y. Xu, Equivalence between fermion-to-qubit mappings in two spatial dimensions, PRX Quantum 4, 010326 (2023).