Lumaktaw sa pangunahing nilalaman
Ang proyektong ito ay nasa beta. Nakakita ng bug? Mag-bukas ng GitHub issue. May tanong o ideya? Magtanong sa Discussions — gusto naming marinig ang iyong feedback!
Gaano kaganda ang kalidad ng pagsasalin sa pahinang ito?

I-simulate ang 1D Fermi-Hubbard dynamics gamit ang flow sets

Mahalaga

Ang mga function na inilalarawan sa gabay na ito ay kasalukuyang available lamang sa Python API. Gagawing available ang katumbas na functionality sa C API sa isang susunod na release.

Gumagawa ang gabay na ito ng circuit para sa time dynamics ng one-dimensional na Fermi-Hubbard model, mula sa pagsulat ng Hamiltonian hanggang sa pagsuri ng mga evolved na site density. Ang nagpapainteresante sa ruta ay isang ideya, mula sa flow set framework nina Gandon et al. [1], ang gawang nagbigay-inspirasyon din sa TransferVertexOperator sa package na ito:

Sa halip na i-group ang mga term ng Hamiltonian pagkatapos ng fermion-to-qubit mapping, i-group ang mga ito bago ito, tungo sa flow sets, mga one-dimensional na subset ng directed fermionic interaction graph na ang mga transfer operator ay sabay-sabay na nagko-commute.

Ang pagpiling iyon ang nagtutulak sa lahat ng nasa ibaba. Nangangailangan ito ng sumusunod:

  • Isang custom na fermion-to-qubit encoding na iniakma sa mga flow set, na dito ay gumagamit ng isang ancilla qubit para hindi na lumaki ang bilang ng qubit na kaugnay ng pagpapatupad ng isang Hamiltonian term kasabay ng bilang ng mga fermionic mode (tulad ng nangyayari sa Jordan-Wigner (JW)).

  • Isang custom na synthesis na sinasamantala ang commutativity sa loob ng bawat set. Ang kapalit nito ay isang circuit na ang two-qubit depth ay pare-pareho sa laki ng system, kumpara sa linear na paglaki ng term-by-term na Trotterization na nakabatay sa JW.

Ang makinaryang nagpapasok ng sarili mong encoding sa transpiler (CustomF2QLayout at MapperFnEvolutionSynthesis) ay naaangkop sa kahit anong model.

Tingnan din

Ang transpilation guide para sa mga transpiler stage na tinutukoy sa buong gabay, at ang mappers guide para sa pangkalahatang recipe sa pagsulat ng custom na mapper.

Mga transfer operator at flow set​

Ang isang TransferVertexOperator ay binubuo mula sa mga vertex operator na VjV_j at mga transfer operator na TjkT_{jk}. Sa mga termino ng fermionic creation at annihilation operator,

Vj=1−2aj†aj ,Tjk=−12(aj†−aj)(ak†+ak) ,V_j = 1 - 2 a^\dagger_j a_j \, , \qquad T_{jk} = -\tfrac{1}{2} \left( a^\dagger_j - a_j \right) \left( a^\dagger_k + a_k \right) \, ,

kaya sinusukat ng VjV_j ang occupation ng mode na jj (ito ay +1+1 kapag walang laman at −1-1 kapag puno), samantalang inililipat ng TjkT_{jk} ang isang fermion sa directed na edge na j→kj \to k. Tandaan na ang dalawang index ng TjkT_{jk} ay hindi mapagpapalit. Dala ng unang index ang minus na kombinasyon at dala ng pangalawa ang plus, kaya naiiba ang TjkT_{jk} sa TkjT_{kj} at nagkakaroon ng oryentasyon ang edge.

Ang mga ito ay sumusunod sa halo-halong commutation relation. Ang dalawang transfer operator na umaaksyon sa magkahiwalay na site ay nagko-commute nang trivial, dahil ang bawat isa ay isang even na product ng mga fermionic operator. Ang kawili-wiling kaso ay ang nakabahaging site. Doon, ang dalawa ay nagko-commute kung ang mga arrow ay dumadaloy sa site na iyon (isa ang dumarating, isa ang umaalis) at nag-aanticommute kung nagbabanggaan ang mga ito (parehong dumarating, o parehong umaalis).

Pinakamadaling makita ang patakaran sa isang larawan. Kapag iginuhit ang mga vertex bilang mga node at ang bawat TjkT_{jk} bilang arrow mula jj patungong kk, makukuha ang directed na interaction graph, na narito para sa four-site chain na ginagamit sa buong gabay na ito, na may parehong oryentasyon ng bawat bond:

A four-site chain with both orientations of every bond drawn as arrows.

Sundan ang patakaran sa figure. Tingnan halimbawa ang T1,2T_{1,2} at T2,3T_{2,3}. Isang arrow ang dumarating sa site 2 at ang isa ay umaalis dito, kaya diretsong dumadaloy ang fermion at nagko-commute ang dalawang operator. Tingnan naman ang T1,2T_{1,2} at T3,2T_{3,2}. Parehong tumuturo papasok sa site 2 ang dalawang arrow. Nagbabanggaan sila, at nag-aanticommute ang dalawang iyon.

Ito ang nagpapagana sa mga flow set. Pumili ng set ng mga arrow na bumubuo ng directed path at ang bawat pares ay alinman sa nagtatagpo nang head-to-tail o hindi nagtatagpo, kaya nagko-commute ang buong set, kahit nagsasapaw ang mga indibidwal na operator sa mga nakabahaging site. Kaya ang pag-evolve sa ilalim ng iisang flow set ay may walang Trotter error, at tulad ng ipinapakita ng step 7, magagawa rin ito sa constant na circuit depth.

1. Ang Fermi-Hubbard Hamiltonian​

Ang 1D Fermi-Hubbard model sa LL na site (spinless, open boundaries) ay

H=−t∑j(aj†aj+1+aj+1†aj)+U∑jnjnj+1 .H = -t \sum_{j} \left( a^\dagger_j a_{j+1} + a^\dagger_{j+1} a_j \right) + U \sum_{j} n_j n_{j+1} \, .

Ayon sa Eq. (9) ng Ref. [1], ang hopping term ay t∑j(Tj,j+1+Tj+1,j)t \sum_j (T_{j,j+1} + T_{j+1,j}). Diagonal ang interaction, kaya ipinapahayag ito sa pamamagitan ng mga vertex operator gamit ang nj=(1−Vj)/2n_j = (1 - V_j)/2:

njnj+1=14(1−Vj−Vj+1+VjVj+1) .n_j n_{j+1} = \tfrac{1}{4}\left( 1 - V_j - V_{j+1} + V_j V_{j+1} \right) \, .

Ang dalawang bahagi ay direktang binuo bilang isang TransferVertexOperator. Tandaan na ang VjV_j ay iniimbak bilang diagonal na entry na (j, j):

>>> from collections import defaultdict
>>> from qiskit_fermions.operators import TransferVertexOperator
>>>
>>> def fermi_hubbard_1d(num_sites, tunneling, interaction):
... data: defaultdict[tuple[tuple[int, int], ...], complex] = defaultdict(complex)
... for j in range(num_sites - 1):
... k = j + 1
... # hopping: -t (a^dag_j a_k + a^dag_k a_j) = t (T_jk + T_kj)
... data[((j, k),)] += tunneling
... data[((k, j),)] += tunneling
... # interaction: U n_j n_k with n_j = (1 - V_j) / 2
... data[()] += interaction / 4
... data[((j, j),)] -= interaction / 4
... data[((k, k),)] -= interaction / 4
... data[((j, j), (k, k))] += interaction / 4
... return TransferVertexOperator.from_dict(data)
>>>
>>> num_sites = 4
>>> hamiltonian = fermi_hubbard_1d(num_sites, tunneling=1.0, interaction=2.0)
>>> print(format(hamiltonian))
1.500000e0 +0.000000e0j * ()
-5.000000e-1 +0.000000e0j * (V(0))
5.000000e-1 +0.000000e0j * (V(0) V(1))
1.000000e0 +0.000000e0j * (T(0,1))
1.000000e0 +0.000000e0j * (T(1,0))
-1.000000e0 +0.000000e0j * (V(1))
5.000000e-1 +0.000000e0j * (V(1) V(2))
1.000000e0 +0.000000e0j * (T(1,2))
1.000000e0 +0.000000e0j * (T(2,1))
-1.000000e0 +0.000000e0j * (V(2))
5.000000e-1 +0.000000e0j * (V(2) V(3))
1.000000e0 +0.000000e0j * (T(2,3))
1.000000e0 +0.000000e0j * (T(3,2))
-5.000000e-1 +0.000000e0j * (V(3))

Nagkakaroon ang mga interior site ng coefficient na −1-1 sa VjV_j dahil lumilitaw sila sa dalawang bond, samantalang ang mga boundary site ay lumilitaw lamang sa isa. Ang bawat term na nakalista rito ay lumilitaw sa graph na iginuhit sa itaas. Ang mga term na T(j,k) ay ang mga arrow nito, at ang mga term na V(j) ay ang mga node nito.

2. I-partition sa mga flow set​

Susunod, lagyan ng label ang bawat term ng flow set na kinabibilangan nito. Para dito ang attribute na groups. Ang isang Evolution gate sa isang grouped na operator ay nagde-decompose sa isang Evolution bawat group (ginagamit ito ng step 6), kaya ang pag-group na napagpasyahan dito sa fermionic na antas ay nananatili hanggang sa circuit.

Para sa 1D chain, ang dalawang hopping flow set ay ang mga arrow na nakaharap sa silangan (Tj,j+1T_{j,j+1}) at ang mga arrow na nakaharap sa kanluran (Tj+1,jT_{j+1,j}). Ang mga diagonal na interaction term ay nagko-commute sa lahat ng diagonal at bumubuo ng ikatlong group:

>>> def flow_set_groups(operator):
... groups = []
... for terms, _ in operator.iter_terms():
... match terms:
... case [(left, right)] if right == left + 1:
... groups.append(0) # east-oriented: T_{j,j+1}
... case [(left, right)] if right == left - 1:
... groups.append(1) # west-oriented: T_{j+1,j}
... case _:
... groups.append(2) # diagonal interaction terms
... return groups
>>>
>>> hamiltonian.groups = flow_set_groups(hamiltonian)
>>> hamiltonian.num_groups()
3
>>> for index, group in enumerate(hamiltonian.split_out_groups()):
... print(index, sorted(terms for terms, _ in group.iter_terms()))
0 [[(0, 1)], [(1, 2)], [(2, 3)]]
1 [[(1, 0)], [(2, 1)], [(3, 2)]]
2 [[], [(0, 0)], [(0, 0), (1, 1)], [(1, 1)], [(1, 1), (2, 2)], [(2, 2)], [(2, 2), (3, 3)], [(3, 3)]]

Ang mga group 0 at 1 ang dalawang flow set. Ang bawat set ay isang directed path sa kahabaan ng chain, kaya ayon sa flow property, nagko-commute ang lahat ng term sa loob ng isang group.

Dapat ding Hermitian ang bawat group sa sarili nito, dahil ine-exponentiate nang hiwalay ng product formula ang mga group at tanging Hermitian na factor lang ang may unitary na evolution. Kinukumpirma ito ng groups_are_hermitian():

>>> from qiskit_fermions.operators.terms.grouping import groups_are_hermitian
>>>
>>> groups_are_hermitian(hamiltonian)
[True, True, True]

Sulit itong pagtuunan ng pansin, dahil ang dalawang hopping group ay one-directional: ang group 0 ay may hawak lamang na mga term na nakaharap sa silangan at ang group 1 ay lamang sa kanluran. Karaniwang sisira sa Hermiticity ang paghahati ng Hamiltonian sa ganoong paraan, dahil kailangan ng aj†aka^\dagger_j a_k ang conjugate partner nitong ak†aja^\dagger_k a_j kasama nito. Naiiwasan ito ng TransferVertexOperator dahil ang TjkT_{jk} ay Hermitian na sa sarili nito. Isa itong product ng dalawang Majorana operator (tingnan ang kahulugan nito sa step 1), hindi isang hubad na aj†aka^\dagger_j a_k, kaya ang Jordan-Wigner image nito ay may real na coefficient sa halip na imaginary. Kaya ang pagpili sa operator representation na ito ang nagpapahintulot na maging lehitimong Trotter factor ang one-directional na flow set.

Tandaan

Ang mga label na east/west na ginagamit sa buong gabay na ito ay geometric, na ang chain ay iginuhit mula kaliwa pakanan at tumataas ang mga mode index sa direksyong iyon; ang Tj,j+1T_{j,j+1} ay nakaturo sa silangan at ang Tj+1,jT_{j+1,j} ay nakaturo sa kanluran. Ang Ref. [1] ay hindi pare-pareho sa pagpapangalang ito (hindi magkatugma ang prosa at mga figure nito) kaya sadyang hindi sinusunod ng gabay na ito ang prosa, at sa halip ay pinananatili ang mga label mula sa larawan. Kapag binabasa kasabay ng paper, itugma ang mga flow set ayon sa kanilang Pauli weight (1 at 3 sa ibaba), hindi ayon sa mga salitang compass.

3. Isulat ang custom na encoding​

Inuuri ng Seksyon IV B ng Ref. [1] ang mga anyong maaaring taglayin ng isang lokal na fermion-to-qubit encoding kapag nakalimita sa isang flow set. Gumagamit ang simpleng Jordan-Wigner ng Nq=NfN_q = N_f na qubit at nima-map ang bawat hopping term sa isang weight-2 na Pauli. Ipinapakita ng klasipikasyon na ang paggastos ng dagdag na qubit ay bumibili ng istruktura sa mga na-encode na operator.

Ang encoding na ipinatupad dito ay ang parity-delocalized na konstruksyon ng Appendix B 2 (mga equation B4 at B5), na gumagamit ng isang ancilla qubit, kaya Nq=Nf+1N_q = N_f + 1. Ang fermionic parity ay dine-delocalize sa isang pares ng qubit:

Vj=ZjZj+1 ,Tj,j+1=−12Xj+1 ,Tj+1,j=+12ZjXj+1Zj+2 .V_j = Z_j Z_{j+1} \, , \qquad T_{j,j+1} = -\tfrac{1}{2} X_{j+1} \, , \qquad T_{j+1,j} = +\tfrac{1}{2} Z_j X_{j+1} Z_{j+2} \, .

Ang index na j+2j+2 sa huling expression ang nagpapatawag ng dagdag na qubit. Ang huling bond ng chain ay may j=Nf−2j = N_f - 2, kaya ang Zj+2Z_{j+2} nito ay napupunta sa qubit NfN_f, isa pagkatapos ng huling fermionic mode na Nf−1N_f - 1. Ang nasa hulihang qubit na iyon ang ancilla, at iyon ang dahilan kung bakit Nq=Nf+1N_q = N_f + 1 at hindi NfN_f. Makikita rin ang parehong abot sa VjV_j, na ang Zj+1Z_{j+1} ay nasa ancilla para sa j=Nf−1j = N_f - 1.

Agad na kita ang kapalit; ang isang buong flow set ay nima-map sa isang sum ng mga weight-1 na Pauli.

Tandaan

Ang mga prefactor na 12\tfrac{1}{2} ay itinakda ng normalization na Tjk2=14T_{jk}^2 = \tfrac{1}{4}, at ang relatibong minus sign sa pagitan ng dalawang oryentasyon ay itinakda ng product identity na Tj,j+1=−VjVj+1Tj+1,jT_{j,j+1} = -V_j V_{j+1} T_{j+1,j}, na sinasang-ayunan ng mga Pauli sa itaas. Ang pagkakamali sa alinman ay magbibigay ng operator na sumusunod sa mga commutation relation pero hindi na kumakatawan sa parehong Hamiltonian, kaya dapat mong i-verify ang encoding nang numerikal, tulad ng ipinapakita sa step 4.

Ang pagsulat ng encoding ay nangangahulugan ng pagsulat ng function na nima-map ang isang generalized transfer operator sa isang Pauli string; hinahawakan ng map_transfer_vertex_generators() ang pag-iterate sa mga term at ang pagbubuo ng mga ito:

>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_transfer_vertex_generators
>>>
>>> def flow_set_action(action, num_qubits):
... match action:
... case (j, k) if j == k:
... # vertex operator: V_j = Z_j Z_{j+1}
... return SparseObservable.from_sparse_list(
... [("ZZ", [j, j + 1], 1.0)], num_qubits=num_qubits
... )
... case (j, k) if k == j + 1:
... # east-oriented transfer operator: weight 1
... return SparseObservable.from_sparse_list(
... [("X", [k], -0.5)], num_qubits=num_qubits
... )
... case (j, k) if k == j - 1:
... # west-oriented transfer operator: weight 3
... return SparseObservable.from_sparse_list(
... [("ZXZ", [k, k + 1, k + 2], 0.5)], num_qubits=num_qubits
... )
... case _:
... raise ValueError(f"not a nearest-neighbor transfer operator: {action}")
>>>
>>> def flow_set_encoding(operator, num_qubits):
... return map_transfer_vertex_generators(
... operator,
... lambda action: flow_set_action(action, num_qubits),
... identity=lambda: SparseObservable.identity(num_qubits),
... compose=SparseObservable.compose,
... ).simplify()
Mahalaga

Inaasahan ng MapperFnEvolutionSynthesis.mapper_fn ang signature na (operator, num_qubits), at ang return type ay dapat isang SparseObservable, na nagpapagamit sa function nang direkta bilang transpiler plugin. Tingnan ang step 6.

Ang paglalapat nito sa Hamiltonian mula sa step 1 ay nagbibigay ng na-encode na operator sa Nf+1=5N_f + 1 = 5 na qubit:

>>> num_qubits = num_sites + 1
>>> encoded = flow_set_encoding(hamiltonian, num_qubits)
>>> for label, indices, coeff in sorted(encoded.to_sparse_list()):
... print(f"{label:3s} {str(indices):12s} {coeff.real:+.2f}")
[] +1.50
X [1] -0.50
X [2] -0.50
X [3] -0.50
ZXZ [0, 1, 2] +0.50
ZXZ [1, 2, 3] +0.50
ZXZ [2, 3, 4] +0.50
ZZ [0, 1] -0.50
ZZ [0, 2] +0.50
ZZ [1, 2] -1.00
ZZ [1, 3] +0.50
ZZ [2, 3] -1.00
ZZ [2, 4] +0.50
ZZ [3, 4] -0.50

Mayroong 14 na term, sa tatlong hugis: ang hopping ay naging X at ZXZ, ang interaction ay ZZ, at ang nangunguna na [] ay ang identity, na nag-aambag lamang ng global phase. Tandaan na ang mga term na ZZ ay umaabot sa pinakamalapit at susunod na pinakamalapit na kapitbahay; ang VjVj+1V_jV_{j+1} ay sumasaklaw sa mga qubit na jj hanggang j+2j+2, na siyang gastos ng pag-delocalize ng parity.

Ang pag-group ayon sa flow set ang naglalantad ng asymmetry na binibili ng konstruksyon. Ilista ang mga Pauli weight na naroon sa bawat group:

>>> for index, group in enumerate(hamiltonian.split_out_groups()):
... weights = {len(indices) for _, indices, _ in flow_set_encoding(group, num_qubits).to_sparse_list()}
... print(index, sorted(weights))
0 [1]
1 [3]
2 [0, 2]

Ang flow set na nakaharap sa silangan (group 0) ay binubuo nang buo ng weight-1 na Pauli. Kaya ang time evolution nito ay isang layer ng single-qubit rotation at hindi nangangailangan ng entangling gate, samantalang sa Jordan-Wigner ang bawat hopping term ay weight-2 at nangangailangan ng mga ito. Ito ang space-time trade-off ng Ref. [1] sa pinakasimpleng anyo nito: ang isang dagdag na qubit ay nagko-convert sa kalahati ng hopping Hamiltonian tungo sa mga single-qubit rotation.

Ang flow set na nakaharap sa kanluran (group 1) ay nagbabayad para dito sa weight tatlo, at ang interaction (group 2) ay nasa weight dalawa, kung saan ang weight-zero na entry ay ang identity. Wala pa sa mga ito ang claim tungkol sa circuit; ang magagawa ng isang synthesis sa mga weight na ito ang paksa ng mga step 6 at 7.

4. I-verify ang encoding​

Kapaki-pakinabang lamang ang custom na encoding kung ito ay tapat, at dapat suriin ang isinulat nang mano-mano. Dahil gumagamit ang encoding na ito ng Nf+1N_f + 1 na qubit, dalawang beses na mas malaki ang qubit Hilbert space kaysa sa fermionic, kaya hindi maaaring maging katumbas ng Jordan-Wigner ang na-encode na Hamiltonian term por term. Sa halip, magkaugnay ang dalawa sa pamamagitan ng isang isometry.

Pinagmulan ng isometry: Sa ilalim ng Jordan-Wigner, ang basis state ay ang occupation string; hawak ng qubit jj ang njn_j. Sa halip, iniimbak ng encoding na ito ang cumulative parities. Tukuyin ang

b0=0 ,bj+1=n0⊕n1⊕⋯⊕nj ,b_0 = 0 \, , \qquad b_{j+1} = n_0 \oplus n_1 \oplus \dots \oplus n_j \, ,

kaya itinatala ng bit na bj+1b_{j+1} ang parity ng lahat ng occupation hanggang at kasama ang site jj. Mayroong Nf+1N_f + 1 na ganoong bit para sa NfN_f na occupation, na pinagmumulan ng ancilla. Nababawi ang occupation bilang pagkakaiba ng magkalapit na bit, nj=bj⊕bj+1n_j = b_j \oplus b_{j+1}. Kaya ang isang occupied na site ay lumilitaw bilang domain wall sa pagitan ng dalawang magkatabing bit, kaya ito tinatawag na domain-wall (o Kramers-Wannier) encoding. Iyon din ang dahilan kung bakit Vj=ZjZj+1V_j = Z_j Z_{j+1}: binabasa ng product ng dalawang magkalapit na ZZ ang pagkakaibang iyon.

Kapag isinulat ang mapang ito para sa ilang estado, nagiging konkreto ang istruktura (tandaan na nakapirmi ang b0=0b_0 = 0, kaya kalahati lang ng 2Nf+12^{N_f+1} na estado ng qubit ang nasa image, at kaya isang isometry ito sa halip na isang unitary):

>>> def domain_wall_bits(occupations):
... parity = 0
... bits = [0]
... for occupation in occupations:
... parity ^= occupation
... bits.append(parity)
... return bits
>>>
>>> for occupations in ([0, 0, 0, 0], [1, 0, 0, 0], [1, 1, 0, 0], [1, 0, 1, 0]):
... bits = domain_wall_bits(occupations)
... # little-endian: reversed() prints qubit 0 on the right, as Qiskit labels do
... print("".join(map(str, occupations)), "->", "".join(str(b) for b in reversed(bits)))
0000 -> 00000
1000 -> 11110
1100 -> 00010
1010 -> 00110

Ang isang fermion sa site 0 ay binabaligtad ang bawat cumulative parity sa itaas nito, na siyang delocalization na gumagana. Kapag tinipon ang mga column na ito sa isang matrix, makukuha ang isometry na WW, at tapat ang encoding kapag pinag-uugnay nito ang dalawang Hamiltonian:

H^flow set W=W H^JW .\hat{H}_{\text{flow set}} \, W = W \, \hat{H}_{\text{JW}} \, .

Ibig sabihin, ang pagmamapa ng isang fermionic na estado papunta sa espasyo ng qubit at pagkatapos ay pag-evolve rito doon ay dapat magbigay ng parehong resulta sa pag-evolve muna nito sa fermionic na espasyo at saka pagmamapa pagkatapos.

>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp
>>> from qiskit_fermions.mappers.library import transfer_vertex_jordan_wigner
>>>
>>> def to_matrix(observable):
... # SparseObservable carries no dense-matrix method, so route through
... # SparsePauliOp. This is only ever needed for the checks below --
... # the encoding itself never leaves SparseObservable.
... return SparsePauliOp.from_sparse_observable(observable).to_matrix()
>>>
>>> def domain_wall_isometry(num_sites):
... isometry = np.zeros((2 ** (num_sites + 1), 2**num_sites), dtype=complex)
... for index in range(2**num_sites):
... occupations = [(index >> j) & 1 for j in range(num_sites)]
... bits = domain_wall_bits(occupations)
... isometry[sum(b << j for j, b in enumerate(bits)), index] = 1.0
... return isometry
>>>
>>> isometry = domain_wall_isometry(num_sites)
>>> np.allclose(isometry.conj().T @ isometry, np.eye(2**num_sites))
True
>>>
>>> def intertwines(operator):
... """Whether both mappings of `operator` agree through the isometry."""
... flow = to_matrix(flow_set_encoding(operator, num_qubits))
... reference = to_matrix(transfer_vertex_jordan_wigner(operator, num_sites).simplify())
... return np.allclose(flow @ isometry, isometry @ reference)
>>>
>>> intertwines(hamiltonian)
True
>>> # and, more strongly, once per flow set: a product formula evolves the groups
>>> # separately, so each one has to be intertwined on its own
>>> all(intertwines(group) for group in hamiltonian.split_out_groups())
True

Eksaktong totoo ang intertwining relation, at hiwalay para sa bawat flow set. Ang pangalawa at mas malakas na pahayag na iyon ang pinagbabatayan ng mga susunod na hakbang: ang product formula ay nag-e-evolve ng mga grupo nang isa-isa, at ang product ng mga intertwined na factor ay intertwined din, kaya anumang approximation na binuo mula sa mga grupong ito ay nagkakasundo sa pagitan ng dalawang encoding kahit magkaiba ang mga encoding. Bilang corollary, dapat magkasundo ang mga spectrum, kung saan ang bawat Jordan-Wigner eigenvalue ay lumilitaw nang dalawang beses sa mas malaking espasyo, minsan para sa bawat halaga ng walang-limitasyong global parity:

>>> jw_matrix = to_matrix(transfer_vertex_jordan_wigner(hamiltonian, num_sites).simplify())
>>> flow_matrix = to_matrix(flow_set_encoding(hamiltonian, num_qubits))
>>>
>>> jw_spectrum = np.linalg.eigvalsh(jw_matrix)
>>> flow_spectrum = np.linalg.eigvalsh(flow_matrix)
>>> np.allclose(
... np.sort(flow_spectrum),
... np.sort(np.concatenate([jw_spectrum, jw_spectrum])),
... )
True
>>> print(f"{jw_spectrum[0]:.10f} {flow_spectrum[0]:.10f}")
-1.8557725066 -1.8557725066

5. Buuin ang fermionic circuit​

Bago gumamit ng anumang qubit, ipinapahayag ang dynamics sa fermionic na antas. Ang isang FermionicCircuit ay umaandar sa mga fermionic mode sa halip na mga qubit, at ang isang Evolution Gate sa ibabaw ng Hamiltonian ang buong circuit:

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import FermionicLieTrotter
>>>
>>> total_time = 1.0
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(
... Evolution(
... num_sites, hamiltonian, time=total_time, synthesis=FermionicLieTrotter()
... ),
... circuit.modes,
... )
>>> circuit.draw("mpl")
<Figure size ... with 1 Axes>

A single Evolution gate spanning all four fermionic modes.

Mayroong isang opaque na kahon sa lahat ng apat na mode, at ang kawili-wiling bahagi ay kung saan ito nade-decompose. Sinasabi ng argumentong synthesis kung paano: ang FermionicLieTrotter ay ang first-order na product formula, na hinahati ang operator ayon sa mga groups nito at ine-evolve ang bawat grupo nang paisa-isa. Kaya ang paghahati sa flow set mula sa hakbang 2 ay ang Trotterization, at isang decompose() lang ang gumagawa nito, na nagbibigay ng isang Evolution bawat flow set ayon sa pagkakasunod ng grupo: east, west, tapos ang diagonal na interaksyon, na may dalang 3, 3, at 8 na term ayon sa pagkakasunod.

>>> flow_set_circuit = circuit.decompose()
>>> flow_set_circuit.draw("mpl")
<Figure size ... with 1 Axes>

The same circuit decomposed into three Evolution gates, one per flow set.

Tandaan na lahat ng ito ay nangyayari sa fermionic na espasyo: wala pang nababanggit na qubit, at nagawa na ang approximation. Kaya ang bilang ng mga hakbang ng Trotter ay nabibilang din dito, bilang reps, na naghahati sa oras at inuulit ang buong sweep:

>>> def trotter_steps(reps):
... step = FermionicCircuit(num_sites)
... step.append(
... Evolution(
... num_sites,
... hamiltonian,
... time=total_time,
... synthesis=FermionicLieTrotter(reps=reps),
... ),
... step.modes,
... )
... return step.decompose()
>>>
>>> trotter_steps(3).count_ops()["Evolution"]
9

Siyam na Gate: ang tatlong flow set, inuulit nang tatlong beses sa tig-isang katlo ng oras. Ang pag-ikot na iyon ang nagpapaging Trotterization nito sa halip na simpleng paghihiwa-hiwalay ng isang grupo. Ang pag-uulit ng isang grupo ay walang magagawa, dahil nagko-commute ang magkakasunod na exponential ng parehong operator, kaya ang A(t/3) A(t/3) A(t/3) ay A(t) lang. Ang formula na bumabasa sa mga grupo ay kayang pagsalitan ang mga ito; ang formula na ginamit pagkatapos ng mapping ay hindi, dahil wala na ang pagkakagrupo sa puntong iyon.

Ito ang hakbang na nagpapahalaga sa pagkakagrupo. Ang bawat inilabas na Gate ay minamapa at sini-synthesize nang hiwalay, kaya ang lahat ng susunod ay umaandar sa paghahating napagpasyahan sa fermionic na antas. Kapag nilaktawan ang decomposition, ibinibigay ang buong operator sa susunod na yugto at hindi na pinapansin ang pagkakagrupo.

6. I-transpile gamit ang custom na encoding​

Handa na ngayong gamitin ng Transpiler ang encoding. Kailangan pa ng dalawang bahagi:

  • Isang layout. Ang default na TrivialF2QLayout ay nagpapalagay ng isang qubit bawat mode, na mali sa sitwasyong ito. Ang CustomF2QLayout ay iniuugnay ang fermionic register sa isang QuantumRegister na iba ang laki, na siyang paraan kung paano pumapasok ang mga ancilla qubit sa pipeline.

  • Isang synthesis plugin. Ang MapperFnEvolutionSynthesis ay direktang kumukuha ng mapper function, minamapa ang Hamiltonian ng Evolution Gate gamit ito, at naglalabas ng PauliEvolutionGate, na pinapanatili ang convention na e−itHe^{-itH}. Ang product_formula nito ang nagpapasya kung paano gagawing mga rotation ang Gate na iyon. Tandaan na ito ay isang pangalawa, hiwalay na product formula, na umaandar sa mga qubit pagkatapos ng mapping: ang LieTrotter ng Qiskit ang ipinapasa rito, at papalitan ito ng hakbang 7 ng isang nakakaalam sa mga flow set.

Dahil inuulit ng bawat paghahambing sa ibaba ang pipeline na ito, makabubuting ibalot ito nang isang beses:

>>> from qiskit.circuit import QuantumRegister
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG, QuantumDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import (
... CustomF2QLayout,
... F2QSynthesis,
... MapperFnEvolutionSynthesis,
... )
>>>
>>> def flow_set_pass_manager(mode_register, product_formula=None):
... '''Build the pipeline mapping `mode_register` with the flow-set encoding.'''
... synthesis = F2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... flow_set_encoding, product_formula=product_formula
... )
... # this encoding spends one ancilla on top of the fermionic modes
... qubit_register = QuantumRegister(mode_register.size + 1, "q")
... return MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... layout=CustomF2QLayout({mode_register: qubit_register}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>>
>>> from qiskit.synthesis import LieTrotter
>>>
>>> pass_manager = flow_set_pass_manager(flow_set_circuit.register, LieTrotter())
>>> qubit_circuit = pass_manager.run(flow_set_circuit)
>>> qubit_circuit.num_qubits
5

Ang apat na fermionic mode ay naging isang circuit na may limang qubit, kung saan ang ancilla ay ibinigay ng custom na layout.

Ngayon, tingnan natin kung ano talaga ang laman ng resultang circuit. Ang paulit-ulit na pag-decompose ay binabawasan ang mga evolution Gate hanggang U at CX, para ang gastos sa two-qubit ng dalawang synthesis strategy na pinaghahambing sa ibaba ay masukat sa parehong sukatan: ang count_ops() ay mag-uulat ng bilang ng CX, at ang depth() ay kumukuha ng filter function para sukatin ang two-qubit depth. Tandaan na ang reps na ito ay isang depth ng decomposition, walang kaugnayan sa bilang ng mga hakbang ng Trotter na ginagawa ng isang product formula, na tinatawag ding reps sa mas huling bahagi ng gabay na ito:

>>> decomposed = qubit_circuit.decompose(reps=6)
>>> decomposed.count_ops()["cx"]
26
>>> decomposed.depth(lambda instruction: len(instruction.qubits) == 2)
23

Dalawampu't anim na CX Gate sa dalawampu't tatlong layer, kaya halos walang tumatakbo nang sabay-sabay. Ipinapakita ng pagguhit ng circuit kung bakit:

>>> decomposed.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The transpiled circuit under LieTrotter&#39;s term-by-term synthesis, a long serial chain of CX ladders.

Ang bawat term ay na-compile nang hiwalay: isang pagpapalit ng basis, isang CX ladder pababa sa isang rotation, tapos binabawi ang ladder. Libre ang weight-1 na flow set; ang mga term nito ay mga nag-iisang rotation na walang ladder sa paligid, pero ang bawat weight-3 na term ay nagbabayad ng apat na CX na Gate, at dahil sunud-sunod na inilalabas ang mga ladder, walang nagpapatong.

Kaya nananatili ang pagkakagrupo sa buong pipeline (tatlong flow set, tatlong na-map na PauliEvolutionGate) pero halos walang naibibigay na pakinabang. Nagampanan ng fermionic Trotterization ng hakbang 5 ang trabaho nito; ang sumasayang dito ay ang formula sa panig ng qubit. Ang LieTrotter ay tumatanggap ng hanay ng mga term na wala itong alam tungkol sa istruktura, at kino-compile ang bawat isa nang hiwalay. Hindi nito alam na nagko-commute ang mga ito sa isa't isa, kaya sini-serialize nito ang maaari sanang magbahagi ng isang pagpapalit ng basis.

Iyan ang puwang na nilalayong isara ng encoding na ito: dalawang product formula ang gumagana, at ang fermionic lang ang nabigyan ng istruktura hanggang ngayon. Ang susunod na hakbang ay nagbibigay ng synthesis sa panig ng qubit na sinasamantala rin ito.

7. Isang synthesis na may alam sa flow set​

Ang pagbawi sa bentahe ay nangangahulugang pagsasabi sa synthesis sa panig ng qubit tungkol sa istruktura. Ito ang Seksyon IV C ng Ref. [1]: dahil nagko-commute ang lahat ng term ng isang flow set, maaari silang sabay-sabay na i-diagonalize ng isang Clifford circuit, i-rotate sa isang pinagsasaluhang basis at i-rotate pabalik, sa halip na minsan bawat term.

Para sa encoding na ito, simple ang Clifford. Ang pag-conjugate gamit ang isang chain ng mga CZ na Gate sa pinakamalapit na kapitbahay ay nagmamapa ng

Xj+1  ⟼  ZjXj+1Zj+2X_{j+1} \; \longmapsto \; Z_j X_{j+1} Z_{j+2}

nang sabay-sabay para sa bawat jj. Ang isang CZ chain ay ginagawang isang layer ng mga independent na single-qubit na XX rotation ang buong weight-3 (west) na flow set. Ang interface ng EvolutionSynthesis ay humihingi lang ng isang method na synthesize(), na siya lang ang ipinapatupad ng class na ito. Wala itong dalang bilang ng mga hakbang ng Trotter, dahil tumatanggap ito ng isang flow set sa bawat pagkakataon at kaya hindi nito mahahati ang evolution sa mga set. Ang paghahating iyon ay nabibilang sa mas mataas na antas, sa fermionic na espasyo, kung saan nakikita ng isang F2FSynthesisPlugin ang lahat ng mga ito nang sabay (tingnan ang hakbang 9):

>>> from qiskit.circuit import QuantumCircuit
>>> from qiskit.synthesis.evolution import EvolutionSynthesis
>>>
>>> def cz_brickwork(circuit):
... # emit even and odd bonds as two layers, so the CZs are scheduled in
... # parallel: a chain of overlapping CZs has depth 2, not num_qubits - 1
... #
... # NOTE: CZ gates are diagonal, so they all commute and each is its own inverse.
... # This whole layer is therefore self-inverse, which is why the conjugation below
... # calls it twice rather than calling it and then its inverse.
... for offset in (0, 1):
... for j in range(offset, circuit.num_qubits - 1, 2):
... circuit.cz(j, j + 1)
>>>
>>> # The interaction reaches nearest (distance 1) and next-nearest (distance 2)
>>> # neighbors, so every interior qubit carries four Rzz gates and can only do one at
>>> # a time: four layers is the best any schedule can do. These offsets achieve it.
>>> def rzz_layers(circuit, angles):
... """Emit the diagonal Rzz gates in four layers of disjoint qubit pairs."""
... near = [(j, k) for j, k in angles if k - j == 1]
... far = [(j, k) for j, k in angles if k - j == 2]
... schedule = [
... [(j, k) for j, k in near if j % 2 == 0],
... [(j, k) for j, k in far if j % 4 in (0, 1)],
... [(j, k) for j, k in near if j % 2 == 1],
... [(j, k) for j, k in far if j % 4 in (2, 3)],
... ]
... # emit layer by layer, since Qiskit's depth follows the order gates were added
... for layer in schedule:
... for j, k in layer:
... circuit.rzz(angles[j, k], j, k)
>>>
>>> class FlowSetSynthesis(EvolutionSynthesis):
... '''Synthesize one flow set of the encoded Hamiltonian at a time.'''
...
... preserve_order = True
...
... def synthesize(self, evolution):
... circuit = QuantumCircuit(evolution.operator.num_qubits)
... east, west, diagonal = [], [], {}
...
... for label, indices, coeff in evolution.operator.to_sparse_list():
... angle = 2 * evolution.time * coeff.real
... match label, indices:
... case "", _: # the identity contributes only a global phase
... circuit.global_phase -= angle / 2
... case "X", [qubit]: # east flow set: already weight-1
... east.append((qubit, angle))
... case "ZXZ", [j, k, l] if (k, l) == (j + 1, j + 2):
... west.append((k, angle)) # needs the CZ conjugation below
... case "ZZ", [j, k]: # interaction: diagonal, see rzz_layers above
... diagonal[min(j, k), max(j, k)] = angle
... case _:
... raise ValueError(f"unexpected Pauli term: {label} on {indices}")
...
... # east flow set: bare rotations, no entangling gates at all
... for qubit, angle in east:
... circuit.rx(angle, qubit)
...
... # west flow set: one CZ chain diagonalizes the WHOLE set at once
... if west:
... cz_brickwork(circuit)
... for qubit, angle in west:
... circuit.rx(angle, qubit)
... cz_brickwork(circuit)
...
... if diagonal:
... rzz_layers(circuit, diagonal)
...
... return circuit
Tandaan

Dalawang detalye ang madaling makaligtaan. Ang isang custom na EvolutionSynthesis ay dapat maglantad ng attribute na preserve_order, na binabasa ng high-level-synthesis pass.

Parehong naglalabas ng kanilang mga Gate ang dalawang helper sa tahasang mga layer sa halip na ayon sa pagkakasunod ng term, dahil ini-schedule ng depth() ang mga Gate sa lalong madaling panahon, ayon sa pagkakasunod ng pagdaragdag. Dahil nagko-commute ang lahat ng Gate sa loob ng isa sa mga set na ito, malaya ang pagpili ng pagkakasunod. Mali ang pagpili ng pagkakasunod ng synthesis na term-by-term. Ang sunud-sunod na paglalabas ng nagpapatong na CZ chain ay nag-uulat ng depth na lumalaki kasabay ng bilang ng qubit sa halip na ang pirming 2 na maaabot ng hardware.

Tandaan

Ito ang dahilan kung bakit walang dalang bilang ng mga hakbang ng Trotter ang synthesis sa itaas. Tumatanggap ito ng isang flow set bawat Gate, kaya ang pag-uulit sa loob nito ay uulit sa loob ng isang set sa halip na umikot sa pagitan ng mga ito:

repeating within one set: A(t/2) A(t/2) B(t/2) B(t/2)
what a Trotterization needs: A(t/2) B(t/2) A(t/2) B(t/2)

Walang naibibigay ang unang anyo: nagko-commute ang magkakasunod na exponential ng parehong operator, kaya ang A(t/2) A(t/2) ay A(t) lang at nananatili ang error sa halaga nito sa isang hakbang gaano man karaming hakbang ang hingin.

Kaya ang bilang ng hakbang ay nabibilang sa fermionic na FermionicLieTrotter ng hakbang 5, na bumabasa ng mga groups at kayang pagsalitan ang mga ito. Iyan ang punto ng paghahati ng trabaho: ang fermionic na formula ang nagpapasya kung ilang hakbang at sa anong pagkakasunod, at ang class na ito ang nagpapasya kung gaano kamura ang pagsasakatuparan ng bawat resultang set.

Upang piliin ang pagkakasunod, ipasa ito sa MapperFnEvolutionSynthesis.product_formula, na siyang argumentong kinukuha ng flow_set_pass_manager. Walang ibang nagbabago sa pipeline (o sa fermionic circuit na pinagtatakbuhan nito):

>>> pass_manager = flow_set_pass_manager(flow_set_circuit.register, FlowSetSynthesis())
>>>
>>> flow_circuit = pass_manager.run(flow_set_circuit).decompose(reps=6)
>>> flow_circuit.count_ops()["cx"]
22
>>> flow_circuit.depth(lambda instruction: len(instruction.qubits) == 2)
12
>>> flow_circuit.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The flow-set-aware circuit, markedly shorter than the previous one, with entangling gates stacked into parallel layers.

Mula sa depth na 23 na may 26 na CX na Gate pababa sa depth na 12 na may 22 na CX na Gate. Tandaan na mas kaunti ito sa entangling na Gate at halos kalahati ng depth. Kapag ikinumpara ang dalawang larawan, ang pagkakaiba ay hindi na mas mura ang mga Gate kundi nagpapatong-patong ang mga ito. Wala na ang mga serial na ladder; ang mga entangling na Gate ay nakapatong na sa isa't isa sa mga pinagsasaluhang layer dahil ang buong west flow set ay iniikot sa isang pinagsasaluhang basis at ang interaksyon ay inilalabas sa mga layer ng magkakahiwalay na pares.

Tandaan

Ang FlowSetSynthesis ay nagpa-pattern-match sa absolute na mga index ng qubit, na ligtas dito dahil sumasaklaw sa buong register ang bawat flow set ng Hamiltonian na ito. Ang fermionic na product formula ay pinipikit ang bawat factor sa mga mode na talagang hinahawakan nito, kaya ang pagkakagrupong ang mga set ay sumasakop lang sa bahagi ng chain ay darating na may bagong label at hindi magtutugma ang mga pattern sa itaas.

Tandaan

Ipinapakita ng dalawang drawing ang circuit pagkatapos ng decompose(reps=6), na isinusulat muli ang lahat sa basis na U at CX, kaya ang mga Gate na CZ, Rx, at Rzz na inilabas ng FlowSetSynthesis ay hindi nakikita bilang ganoon. Sinadya iyon: inilalagay nito ang parehong circuit sa parehong basis, na siyang tanging paraan para maikumpara ang mga bilang at depth ng two-qubit sa itaas. Ang istruktura ng layer ang kapansin-pansin sa mga figure na ito, hindi ang mga label ng Gate.

8. Pirming depth sa malaking sukat​

Ang kawili-wiling tanong ay hindi ang bilang sa apat na mode kundi kung paano ito lumalaki. Hindi lumalaki ang dalawang CZ brickwork layer kasabay ng haba ng chain, kaya dapat pirmi ang two-qubit depth ng isang hakbang ng Trotter. Direktang sinusukat ito sa buong Fermi-Hubbard Hamiltonian, malayo sa kayang i-simulate gamit ang brute force:

>>> def evolution_stats(num_sites, product_formula):
... operator = fermi_hubbard_1d(num_sites, tunneling=1.0, interaction=2.0)
... operator.groups = flow_set_groups(operator)
...
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(
... num_sites, operator, time=total_time, synthesis=FermionicLieTrotter()
... ),
... circuit.modes,
... )
... circuit = circuit.decompose() # one Evolution per flow set, as in step 5
...
... pass_manager = flow_set_pass_manager(circuit.register, product_formula)
... decomposed = pass_manager.run(circuit).decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return decomposed.count_ops()["cx"], decomposed.depth(two_qubit)
>>>
>>> sites = [4, 10, 20, 50, 100]
>>> flow = [evolution_stats(n, FlowSetSynthesis()) for n in sites]
>>> lie_trotter = [evolution_stats(n, LieTrotter()) for n in sites]
>>>
>>> [depth for _, depth in flow]
[12, 12, 12, 12, 12]
>>> [depth for _, depth in lie_trotter]
[23, 47, 87, 207, 407]

Ang two-qubit depth ay nananatili sa 12 mula apat na mode hanggang 100, habang ang LieTrotter ay linear na lumalaki hanggang 407. Pinapayagan ng mga flow set na bahagyang mag-parallelize sa loob ng pipeline, pero ang bawat set ay Trotterized pa rin term-by-term, kaya pinabagal lang ang paglaki, hindi naalis:

>>> import matplotlib.pyplot as plt
>>>
>>> figure, axes = plt.subplots(1, 2, figsize=(9, 3.5), layout="constrained")
>>> for panel, index, title in zip(axes, (1, 0), ("two-qubit depth", "CX count")):
... _ = panel.plot(sites, [stat[index] for stat in lie_trotter], "o-", label="qubit-level")
... _ = panel.plot(sites, [stat[index] for stat in flow], "s-", label="flow set")
... _ = panel.set(xlabel="modes", title=title)
... _ = panel.legend()
>>> figure
<Figure size ... with 2 Axes>

Two-qubit depth against number of modes; the qubit-level LieTrotter grows linearly while the flow-set synthesis stays flat at 12.

Linear na lumalaki ang bilang ng Gate sa dalawa (may O(N)O(N) na term na kailangang ilapat, at hindi iyon maiiwasan), pero inilalapat ng flow-set synthesis ang mga ito sa pirming bilang ng mga parallel na layer. Ito ang constant-depth na resulta ng Ref. [1].

Tandaan

Ang depth na 12 ay nahahati sa tatlong flow set bilang 0 + 4 + 8, sa bawat haba ng chain. Ang east flow set ay nag-aambag ng zero; ito ay mga hubad na Rx rotation. Ang west flow set ay nagkakahalaga ng 4, mula sa dalawang CZ brickwork layer na papasok at dalawang palabas; kapag tinanggal ang interaksyon, ang natitirang two-qubit depth ay apat sa anumang haba ng chain. Ang natitirang walo ay ang interaksyon na ang mga Rzz na term ay umaandar sa parehong pinakamalapit at susunod-sa-pinakamalapit na kapitbahay kaya nangangailangan ng apat na magkakahiwalay na layer, na bawat isa ay nagkakahalaga ng dalawang CX na Gate. Pinakamainam ang apat dito: ang bawat panloob na qubit ay may apat na Rzz na Gate (dalawa sa bawat kapitbahay at dalawa sa bawat susunod-sa-pinakamalapit) at isa lang ang sabay nitong masasalihan, kaya walang schedule na makakagawa ng mas mahusay.

Dalawang katlo ng depth ay ang interaksyon, na nagpapakitang mahusay ang encoding na ito sa hopping, at kaya sa dynamics ng isang free-fermion chain. Gayunpaman, tandaan na hindi nagko-commute ang interaksyon sa hopping. Ang pag-alis dito ay hindi isang approximation sa Fermi-Hubbard kundi ibang modelo. Sulit tuklasin kung saan babagsak ang balanse para sa isang partikular na problema.

9. Suriin ang dynamics​

Sa ilalim ng domain-wall map, ang isang pattern ng occupation ay nagiging isang cumulative-parity string, kaya naiiba ang paghahanda ng paunang estado kumpara sa Jordan-Wigner. Ito ay isang praktikal na bunga na madaling makaligtaan ng delocalization ng parity. Ang helper mula sa hakbang 4 ang nagbibigay ng tamang label:

>>> occupations = [1, 0, 1, 0]
>>> initial_label = "".join(str(b) for b in reversed(domain_wall_bits(occupations)))
>>> initial_label
'00110'

Kapag handa na ang estado, dalawang tanong ang natitira, at pinakamainam na itanong ang mga ito nang magkahiwalay: kinakalkula ba ng flow-set synthesis ang tamang bagay, at kinakalkula ba ito nang mas mura?

Unahin ang kawastuhan. Ang FlowSetSynthesis at ang LieTrotter ng Qiskit ay tumatanggap ng parehong naka-encode na flow set mula sa parehong fermionic Trotterization, kaya dapat nilang isakatuparan ang parehong unitary hanggang sa isang global phase. Ang mga custom na Clifford ay nag-aayos muli ng trabaho; hindi nila binabago ang resulta. Natutukoy ng mga random na statevector ang anumang pagkakaiba nang mas mura kaysa sa pagbuo ng buong unitary:

>>> from qiskit.quantum_info import Statevector
>>>
>>> rng = np.random.default_rng(1234)
>>>
>>> def agrees_up_to_one_phase(reps, samples=3):
... """Compare both qubit-side syntheses of the same fermionic Trotterization."""
... step = trotter_steps(reps)
... trotter = flow_set_pass_manager(step.register, LieTrotter()).run(step)
... flow = flow_set_pass_manager(step.register, FlowSetSynthesis()).run(step)
... trotter, flow = trotter.decompose(reps=6), flow.decompose(reps=6)
... dimension = 2**trotter.num_qubits
... residuals, phases = [], []
... for _ in range(samples):
... vector = rng.normal(size=dimension) + 1j * rng.normal(size=dimension)
... state = Statevector(vector / np.linalg.norm(vector))
... a = state.evolve(trotter).data
... b = state.evolve(flow).data
... overlap = np.vdot(a, b)
... phases.append(overlap / abs(overlap))
... residuals.append(np.linalg.norm(b - a * overlap / abs(overlap)))
... # the SAME global phase must work for every input state
... spread = max(abs(phase - phases[0]) for phase in phases)
... return bool(max(residuals) < 1e-9), bool(spread < 1e-9)
>>>
>>> [agrees_up_to_one_phase(reps) for reps in (1, 3)]
[(True, True), (True, True)]
Tandaan

Mahalaga ang dalawang kalahati ng pagsusuring iyon. Ang unit-modulus na overlap sa isang estado ay kailangan pero hindi sapat: ang synthesis na nagkamali sa phase na nakadepende sa estado ay lalabas pa rin. Ang pag-require ng parehong phase sa ilang random na estado ang nagsasara sa puwang. Mahalaga rin ang pagsusuri ng higit sa isang reps, dahil kinukumpirma nitong nananatili ang pagkakasundo sa pagsasalitan ng mga flow set sa halip na totoo lang para sa isang sweep.

Kaya walang epekto sa katumpakan ang pagpili ng synthesis sa panig ng qubit, na siyang nagpapaging makabuluhan ng paghahambing ng gastos. Ang natitirang tanong ay kung ano ang naibibigay ng encoding sa kabuuan laban sa halatang alternatibo: payak na Jordan-Wigner sa num_sites na qubit, na walang ancilla at walang custom na Clifford. Parehong naiiba ang encoding at ang synthesis sa panig ng qubit sa pagitan ng dalawang column, pero ipinakita ng pagsusuri sa itaas na ang huli ay hindi makakaapekto sa resulta, kaya ang sinusubok ng mga fidelity ay ang encoding. Nagsisimula rin ang dalawang pipeline mula sa parehong fermionic Trotterization, kaya ang anumang pagkakaiba sa fidelity ay magpapahiwatig ng bug sa halip na isang trade-off. Ang dalawang column ay ang mga encoding, flow-set laban sa Jordan-Wigner:

>>> def jordan_wigner_pass_manager(mode_register, product_formula):
... """The same pipeline under plain Jordan-Wigner: one qubit per mode."""
... synthesis = F2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... transfer_vertex_jordan_wigner, product_formula=product_formula
... )
... qubit_register = QuantumRegister(mode_register.size, "q")
... return MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... layout=CustomF2QLayout({mode_register: qubit_register}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>>
>>> from scipy.linalg import expm
>>>
>>> jw_state = np.zeros(2**num_sites, dtype=complex)
>>> jw_state[sum(o << j for j, o in enumerate(occupations))] = 1.0
>>> reference_jw = expm(-1j * total_time * jw_matrix) @ jw_state
>>> reference_flow = expm(-1j * total_time * flow_matrix) @ (isometry @ jw_state)
>>>
>>> def cost_and_fidelity(reps):
... step = trotter_steps(reps)
... flow = flow_set_pass_manager(step.register, FlowSetSynthesis()).run(step)
... jw = jordan_wigner_pass_manager(step.register, LieTrotter()).run(step)
... flow, jw = flow.decompose(reps=6), jw.decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... evolved_flow = Statevector.from_label(initial_label).evolve(flow).data
... evolved_jw = Statevector(jw_state).evolve(jw).data
... return (
... abs(np.vdot(evolved_flow, reference_flow)) ** 2,
... flow.count_ops()["cx"],
... flow.depth(two_qubit),
... abs(np.vdot(evolved_jw, reference_jw)) ** 2,
... jw.count_ops()["cx"],
... jw.depth(two_qubit),
... )
>>>
>>> for reps in (1, 2, 4, 8):
... flow_f, flow_cx, flow_d, jw_f, jw_cx, jw_d = cost_and_fidelity(reps)
... print(
... f"{reps} step(s): flow set {flow_f:.6f}"
... f" ({flow_cx:3d} CX, depth {flow_d:3d})"
... f" JW {jw_f:.6f} ({jw_cx:3d} CX, depth {jw_d:3d})"
... )
1 step(s): flow set 0.590661 ( 22 CX, depth 12) JW 0.590661 ( 18 CX, depth 14)
2 step(s): flow set 0.926656 ( 44 CX, depth 24) JW 0.926656 ( 36 CX, depth 26)
4 step(s): flow set 0.982053 ( 88 CX, depth 48) JW 0.982053 ( 72 CX, depth 50)
8 step(s): flow set 0.995395 (176 CX, depth 96) JW 0.995395 (144 CX, depth 98)

Basahin muna ang column ng fidelity: magkapareho, sa bawat naka-print na digit, sa dalawang magkaibang Hilbert space sa ilalim ng dalawang magkaibang encoding. Iyon ang fermionic Trotterization na lumilitaw. Naayos ang approximation sa hakbang 5, bago napili ang alinmang encoding, kaya ang dalawang circuit ay may eksaktong error ng isang first-order na formula sa tatlong flow set, at bumababa ito nang humigit-kumulang apat na beses sa bawat pagdoble ng bilang ng hakbang.

Ang pinagkaiba ng mga encoding ay ang gastos, at sulit na sabihin nang tuwiran ang trade. Ang flow set ay gumagastos ng mas marami na entangling na Gate: 22 laban sa 18 sa isang hakbang. Hindi ang ancilla ang dahilan, dahil wala itong sariling two-qubit na Gate. Ang dahilan ay ang interaksyon, eksakto gaya ng babala ng hakbang 3: ang pag-delocalize ng parity ay ginagawang pito ang tatlong nearest-neighbor na ZZ na term na sumasaklaw sa pinakamalapit at susunod-sa-pinakamalapit na kapitbahay, at bawat isa ay nagkakahalaga ng dalawang CX na Gate. Iyan ang presyong binabayaran para maging mura ang hopping.

Bahagya lang din itong nananalo sa depth sa laking ito (12 laban sa 14), dahil masyadong maliit ang apat na mode para lumitaw ang mga asymptotics. Ang punto ay ang scaling na sinukat sa hakbang 8: pirmi ang depth na 12, habang lumalaki ang depth ng Jordan-Wigner kasabay ng chain. Sa 14 na mode, 12 laban sa 34 na ito.

Babala

Mahalaga ang mga tawag sa decompose() sa itaas, para sa dalawang magkahiwalay na dahilan.

Ang evolve() at Operator ay gumagamit ng eksaktong depinisyon ng isang PauliEvolutionGate, na nilalampasan ang custom na synthesis sa panig ng qubit. Ang pag-verify laban sa circuit na hindi dinecompose ay nag-uulat ng perpektong fidelity kahit para sa sadyang maling synthesis.

Ganoon din ang nalalapat sa isang antas pataas, sa fermionic synthesis. Ang isang Evolution Gate na hindi kailanman dinecompose ay napupunta nang buo sa yugto ng fermion-to-qubit, na minamapa ito nang hindi sinasangguni ang Evolution.synthesis, kaya ang reps ay tahimik na walang magiging epekto. Ginagawa ng trotter_steps mula sa hakbang 5 ang decomposition na iyon; ang pipeline na nagtatakda ng method gamit ang F2FSynthesis ay nakukuha ang parehong expansion mula sa pass.

Bilang huling cross-check, ang mga densidad ng site na ⟨nj(t)⟩=(1−⟨Vj⟩)/2\langle n_j(t) \rangle = (1 - \langle V_j \rangle)/2 na kinalkula sa naka-encode na espasyo ay dapat magparami ng resulta ng Jordan-Wigner. Ang pagsusuring ito ay tungkol sa encoding, hindi sa circuit, kaya walang approximation ng Trotter na pumapasok dito. Parehong kinakalkula nang eksakto ang dalawang time evolution, sa pamamagitan ng matrix exponentiation ng kani-kanilang Hamiltonian. Kaya ang anumang pagkakaiba ay ituturo ang encoding sa halip na ang product formula:

>>> time = 0.8
>>> state_jw = np.zeros(2**num_sites, dtype=complex)
>>> state_jw[sum(o << j for j, o in enumerate(occupations))] = 1.0
>>> evolved_jw = expm(-1j * time * jw_matrix) @ state_jw
>>> evolved_flow = expm(-1j * time * flow_matrix) @ (isometry @ state_jw)
>>>
>>> for j in range(num_sites):
... v_flow = to_matrix(
... SparseObservable.from_sparse_list([("ZZ", [j, j + 1], 1.0)], num_qubits)
... )
... v_jw = to_matrix(SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_sites))
... density_flow = (1 - np.vdot(evolved_flow, v_flow @ evolved_flow).real) / 2
... density_jw = (1 - np.vdot(evolved_jw, v_jw @ evolved_jw).real) / 2
... print(f"site {j}: {density_flow:.10f} {density_jw:.10f}")
site 0: 0.6507935456 0.6507935456
site 1: 0.5963201678 0.5963201678
site 2: 0.3365016619 0.3365016619
site 3: 0.4163846247 0.4163846247

Nagkakasundo ang mga densidad hanggang sampung decimal place.

Ang dapat tandaan​

  • Mag-grupo bago mag-mapa. Ang mga flow set ay tinutukoy sa directed na fermionic interaction graph, kaya pinipili ang pagkakagrupo habang available pa ang istruktura ng problema. Dinadala ito ng attribute na groups papasok sa circuit, kung saan ginagawa ito ng isang decompose() na isang Evolution Gate bawat flow set.

  • Ang mga ancilla ay bumibili ng istruktura. Ang paggastos ng isang dagdag na qubit ay ginagawang weight-1 na mga Pauli ang buong flow set, na ang evolution ay hindi nangangailangan ng entangling na Gate. Ang Ref. [1] ay naoobserbahan ang trade-off na ito sa pangkalahatan. Ang mas malalaking ratio ng qubit-sa-fermion ay nagpapahintulot ng mas mababaw na evolution circuit.

  • Ang custom na encoding ay isang function. Imapa ang isang generalized na transfer operator sa isang Pauli string; ang map_transfer_vertex_generators() at MapperFnEvolutionSynthesis na ang bahala sa iba.

  • Hindi sapat ang mapping at pagkakagrupo lang. Ang paghahati ng evolution bawat flow set ay nagpabagal lang sa linear na paglaki ng depth; ang term-by-term na synthesis ng bawat set ay sinayang pa rin ang katotohanang nagko-commute ang mga term nito. Ang pagbibigay lang ng EvolutionSynthesis na may alam sa flow set ang nagpapirmi sa depth. Ang mga custom na encoding, pagkakagrupo, at custom na synthesis ay nagkukumplemento sa isa't isa: kailangan ang lahat ng tatlo.

  • Dalawang product formula, dalawang trabaho. Ang FermionicLieTrotter ang nag-aayos ng approximation sa fermionic na espasyo, kung saan nakikita pa ang pagkakagrupo, kaya ito lang ang nagtatakda ng Trotter error at bilang ng hakbang. Ang EvolutionSynthesis sa panig ng qubit ay nagpapasya na lang kung gaano kamura ang pagsasakatuparan ng bawat set: kinukumpirma ng hakbang 9 na parehong unitary ang ibinibigay ng dalawang pagpili. Ang paghihiwalay ng dalawa ang nagpapahintulot na i-tune nang hiwalay ang katumpakan at ang gastos. Tingnan ang Mag-synthesize ng Evolution gate sa fermionic na espasyo.

  • I-verify ito. Ang encoding na tumutupad sa mga commutation relation ay maaari pa ring kumatawan sa ibang Hamiltonian kung mali ang prefactor o sign. Nahuhuli ito ng pagsusuri sa isang intertwining relation (o, mas mura, ang spectrum).

Tandaan

Ang FlowSetSynthesis sa itaas ay sadyang isinulat para sa encoding na ito sa isang 1D chain. Nagpa-pattern-match ito sa mga partikular na label ng Pauli na nilalabas ng encoding at nagtataas ng error sa anumang iba pa, sa halip na tahimik na bumalik sa isang generic na product formula. Ang pangkalahatang implementasyon ay may dalawang kalahati. Ang pagbasa ng istruktura ng flow-set mula sa mga groups ng operator ay ginagawa na ng isang F2FSynthesisPlugin para sa isang evolution. Ang pagkuha ng diagonalizing na Clifford mula sa stabilizer group, gaya ng inilarawan sa Seksyon IV C ng Ref. [1], ang kalahating nananatiling partikular sa encoding.

Gayundin, ang pag-delocalize ng parity ay ginagawang weight-2 ang interaction term na njnj+1n_j n_{j+1} sa halip na diagonal sa mga single qubit, na siyang katumbas na gastos ng mas murang mga hopping term.

Susunod: dalawang dimensyon​

Ang lahat sa itaas ay isang-dimensyon, at sinadya ito. Sa isang chain, ang mga flow set ay ang dalawang oryentasyon lang ng arrow, at ang encoding ay nangangailangan ng iisang ancilla. Wala sa dalawang pagpapasimpleng iyon ang nananatili sa dalawang dimensyon, kung saan kailangan ang mga lokal na encoding.

Sa isang planar na lattice, ang flow set ay isang directed path (o unyon ng mga vertex-disjoint na directed path) sa interaction graph, at ang paghahanap ng maliit na hanay ng mga ganoong path na sumasaklaw sa bawat edge ay nagiging isang combinatorial na problema sa sarili nito, sa halip na isang dalawang-linyang klasipikasyon. Nagbabago rin ang katangian ng mga encoding; sa halip na iisang ancilla para sa buong chain, nagdaragdag sila ng isa bawat site o cell ng lattice. Ipinapares ng Verstraete-Cirac ang bawat fermionic na site sa sarili nitong auxiliary na qubit, isang ratio ng qubit-sa-mode na 22, habang ang Derby-Klassen ay naglalagay ng isa sa gitna ng bawat odd na plaquette, para sa ratio na 3/23/2. Ang layout ng register ay may tunay nang dalawang-dimensyong istruktura, at ang stabilizer group na dapat i-project ay hindi na trivial. Ang naka-encode na Hamiltonian ay tapat lang sa isang subspace, na nagbabago sa parehong paghahanda ng estado at sa recipe ng pag-verify mula sa hakbang 4.

Ang mga bahaging binubuo ng gabay na ito ang dinadala pasulong: ang CustomF2QLayout para sa register na iba ang laki sa bilang ng mode, ang map_transfer_vertex_generators() para sa mismong encoding, ang MapperFnEvolutionSynthesis para ikabit ito sa Transpiler, at isang custom na EvolutionSynthesis para pakinabangan ang istruktura ng flow-set. Ang encoding at ang flow-set decomposition lang ang magkaiba.

Ipinagpapatuloy ito ng 2D flow-set guide, na tinatalakay ang Verstraete-Cirac encoding sa isang square lattice: apat na flow set, isang depth-2 na Clifford para sa bawat isa, at ang stabilizer projection na kailangan ng pag-verify pagkatapos.

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

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.