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-map ang mga representasyon ng operator

Ang mga mapper ay mga routine na nagto-transform ng mga operator mula sa isang representasyon patungo sa isa pa habang pinapanatili ang mathematical equivalence. Gamitin ang mga ito para makapagtrabaho nang flexible sa iba't ibang operator algebra at para ihanda ang mga operator para sa pagpapatakbo ng quantum circuit.

Mag-implement ng mga custom na mapper​

Ginagawang diretso ng mappers module ang pag-implement ng mga custom na mapping. Gumagana ang mga mapper sa pamamagitan ng pag-transform ng mga pundamental na aksyon (creation, annihilation, Majorana, at iba pa) na bumubuo sa isang operator, at pagkatapos ay pinagsasama ang mga na-transform na aksyong ito ayon sa mga patakaran ng target na representasyon.

Bilang konkretong halimbawa, tingnan ang isang Jordan-Wigner transformation na nagma-map ng MajoranaOperator sa isang qubit operator gamit ang map_majorana_action_generators():

>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_majorana_action_generators
>>> from qiskit_fermions.operators import MajoranaOperator
>>>
>>> # Create a Majorana operator
>>> maj_op = MajoranaOperator.from_dict({(0, 1): 1.0, (2,): 0.5})
>>>
>>> # Define how each Majorana action maps to Pauli strings
>>> num_qubits = 2
>>> def map_action(mode: int) -> SparseObservable:
... idx = mode // 2
... qubits = list(range(idx + 1))
... pauli = "Y" if mode % 2 else "X"
... return SparseObservable.from_sparse_list(
... [("Z" * idx + pauli, qubits, 1.0)],
... num_qubits=num_qubits,
... )
>>>
>>> # Apply the mapping to transform the operator
>>> custom_observable = map_majorana_action_generators(
... maj_op,
... map_action,
... identity=lambda: SparseObservable.identity(num_qubits),
... compose=SparseObservable.compose,
... )
>>> print(custom_observable.simplify())
<SparseObservable with 2 terms on 2 qubits: (0+1j)(Z_0) + (0.5+0j)(X_1 Z_0)>

Ang mga pangunahing hakbang para makagawa ng anumang custom mapper ay:

  1. Tukuyin ang action transformation: Tukuyin kung paano nagma-map ang bawat action (naka-index ayon sa mode) sa target na representasyon. Sa halimbawa sa itaas, nagma-map ang mga Majorana action sa mga Pauli string ayon sa mga patakaran ng Jordan-Wigner.

  2. Gumamit ng isa sa mga ibinigay na mapping function: Ang mappers module ay nagbibigay ng mga utility function para sa pag-iterate sa mga term ng operator at sa sunud-sunod na paglalapat ng custom action map para sa mga operator representation na ibinibigay ng operators module. Halimbawa, gumagana ang map_majorana_action_generators() sa mga Majorana operator.

  3. Kunin ang resulta: Nagbabalik ang mapping function ng operator sa target na representasyon, kung saan pare-pareho ang pagkakalapat ng transformation sa lahat ng term.

Dahil sa disenyong ito, madaling mag-prototype ng mga custom mapping nang hindi kailangang malalim na maunawaan ang panloob na istruktura ng mga operator. Awtomatikong inilalapat ang transformation sa bawat term ng operator habang napapanatili ang mathematical equivalence.

Hint

Maaari mo ring i-iterate nang manu-mano ang mga term ng isang operator sa halip na gumamit ng isa sa mga ibinigay na iterator function. Tingnan ang seksyon tungkol sa term iteration sa operators guide para sa mga detalye.

Mga library implementation​

Ang qiskit_fermions.mappers.library module ay nagbibigay ng episyente at masusing nasubukang mga implementasyon ng karaniwang mga mapping. Sinusunod ng mga ito ang parehong pangunahing pattern ng mga custom mapper pero naka-optimize para sa production at available sa parehong Python at C API.

Ang halimbawa sa ibaba ay naglalapat ng library mapper sa parehong MajoranaOperator mula sa seksyon ng custom mapper, at nagpapakita ng katumbas na mga resulta:

>>> from qiskit_fermions.mappers.library import majorana_to_fermion, jordan_wigner
>>>
>>> # Convert MajoranaOperator to FermionOperator using library mapper
>>> ferm_op = majorana_to_fermion(maj_op)
>>>
>>> # Apply Jordan-Wigner mapper to get qubit operator
>>> sparse_observable = jordan_wigner(ferm_op, num_qubits=2)
>>> difference = sparse_observable - custom_observable
>>> print(difference.simplify() == SparseObservable.zero(num_qubits))
True
Note

Isinulat nang hiwalay ang dalawang hakbang sa itaas para ipakita ang pagsasama ng dalawang library mapper. Sa praktika, magma-map ka ng MajoranaOperator sa isang hakbang lang, gamit ang majorana_jordan_wigner() (qf_maj_op_jordan_wigner() sa C API), at ganoon din para sa iba pang uri ng operator – edge_vertex_jordan_wigner() at transfer_vertex_jordan_wigner() (qf_edge_op_jordan_wigner(), qf_transfer_op_jordan_wigner()). Pinalolobo rin ng pagdaan sa FermionOperator ang intermediate na resulta: bawat generator ng mga algebra na ito ay nagma-map sa iisang Pauli string, samantalang bawat fermionic action ay nagma-map sa kabuuan ng dalawang term, kaya ang isang term na binuo mula sa maraming generator ay lumalawak lang para muling pagsamahin pababa. Lumalaki ang natitipid na ito habang humahaba ang mga term; para sa mga term na may iisang generator ng isang hopping Hamiltonian, halos pareho ang gastos ng dalawang ruta.

Sa Python API, ang ilan sa mga library mapper na ito ay maaari ding abutin sa pamamagitan ng mga type-agnostic na convenience mapper. Ang fermion_operator() at majorana_operator() ay nasa harap ng majorana_to_fermion() at fermion_to_majorana() (bukod sa iba pa), at internal na ipinapasa ang gawain sa tamang library implementation batay sa uri ng object na ibinigay sa kanila, kaya hindi mo na kailangang piliin ang tamang function. Ganoon din ang jordan_wigner() at ang ternary_tree() (ipinakilala sa ibaba), na parehong nagpapasa sa direktang implementasyon na tumutugma sa operator na ibinigay sa kanila. Tingnan ang qiskit_fermions.protocols para sa kung paano ipinapatupad sa loob ang dispatch na ito.

Pumili ng encoding gamit ang mga ternary tree​

Hindi lang Jordan-Wigner ang fermion-to-qubit encoding, at hindi rin ito ang pinakamura. Lumalaki ang mga ZZ string nito kasabay ng mode index, kaya ang isang Majorana operator sa huli sa nn mode ay nagma-map sa isang Pauli string na may weight na nn. Ang mga encoding na ikinakalat ang parity information sa isang tree sa halip na sa isang chain ay mas mahusay nang asymptotically.

Sakop ng isang ternary tree na may nn node ang buong pamilyang ito. Ang bawat node ay isang qubit na may tatlong pababang link na may label na X, Y at Z; ang isang link ay bumababa sa isang child node (isang edge) o nagtatapos (isang leg). Ang landas ng bawat leg pabalik sa root ay bumubuo ng isang Pauli string: ang pagtawid sa link na may label na PP na bumababa mula sa node uu ay nag-aambag ng PP sa qubit uu. Bawat ganitong tree ay nagbibigay ng wastong encoding, at ang mga pamilyar ay mga partikular na hugis lang – ang chain sa kahabaan ng Z ay Jordan-Wigner, samantalang ang tree kung saan may tatlong anak ang bawat node, hanggang pinapayagan ng bilang ng node, ay balanced.

>>> from qiskit_fermions.mappers import TernaryTree, TernaryTreeEncoding
>>>
>>> # Jordan-Wigner is a chain descending along Z, with X and Y as legs at every node.
>>> TernaryTree.chain(13, "Z").max_weight
13
>>> # The balanced tree spreads the same information over depth log_3(2n+1).
>>> TernaryTree.breadth_first(13, 3).max_weight
3

Optimal ang maximum weight na iyon. Pinatunayan nina Jiang et al. na ang mga Majorana operator ng anumang fermion-to-qubit mapping ng nn mode ay may average na Pauli weight na hindi bababa sa log⁡3(2n)\log_3(2n), kaya walang mapping na makapagpapababa sa pinakamalaki sa mga ito sa ibaba ng ⌈log⁡3(2n+1)⌉\lceil \log_3(2n+1) \rceil. Eksaktong nn qubit ang ginagamit ng parehong tree, kaya ang natitipid ay nasa Pauli weight, hindi sa bilang ng qubit.

Para mag-map ng operator, mag-compile ng tree sa isang encoding at ibigay ito sa isang mapper kasama ang bilang ng qubit para sa resulta. Pinagpapares ng pag-compile ang mga leg ng tree sa mga Majorana operator at binubuo ang kanilang mga Pauli string nang isang beses lang, kaya dapat gamitin muli ang parehong encoding sa maraming operator. Nananatili ang bilang ng qubit sa mapping call sa halip na sa encoding, kaya ang isang encoding ay makapaglilingkod sa mga register na magkakaiba ang lapad; ang bilang na mas mataas sa bilang ng node ng tree ay pinupunan ng identity.

>>> from qiskit_fermions.mappers.library import fermion_ternary_tree
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> hamiltonian = FermionOperator.from_dict(
... {((True, 0), (False, 3)): 1.0, ((True, 3), (False, 0)): 1.0}
... )
>>> encoding = TernaryTreeEncoding(TernaryTree.breadth_first(4, 3))
>>> fermion_ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>

Ang pag-map sa pamamagitan ng chain sa kahabaan ng Z ay eksaktong nagbubunga ng fermion_jordan_wigner() (qf_ferm_op_jordan_wigner() sa C API), na siyang dahilan kung bakit nasasabing ginagawang pangkalahatan nito iyon:

>>> from qiskit_fermions.mappers.library import fermion_jordan_wigner
>>> chain = TernaryTreeEncoding(TernaryTree.chain(4, "Z"))
>>> direct = fermion_jordan_wigner(hamiltonian, 4)
>>> (fermion_ternary_tree(hamiltonian, chain, 4) - direct).simplify().num_terms
0

May ternary-tree mapper ang bawat operator representation, na kahawig ng hanay ng Jordan-Wigner; tingnan ang qiskit_fermions.mappers.library para sa buong listahan, at ang Mapper Library sa C API. Sa Python API, maaari rin silang abutin sa pamamagitan ng ternary_tree(), ang type-agnostic na wrapper na pumipili ng tamang isa para sa operator na ibinigay dito, tulad ng ginagawa ng jordan_wigner():

>>> from qiskit_fermions.mappers.library import ternary_tree
>>> ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>
Note

Ang ilang identity na totoo sa ilalim ng Jordan-Wigner ay mga katangian ng chain at hindi ng mga ternary tree. Ang number operator ay nj=(1−Zj)/2n_j = (1 - Z_j)/2 sa chain lang; sa ibang tree, ang ZjZ_j ay nagiging isang ZZ string sa isang set ng mga qubit. Gayundin, ang vertex operator na VjV_j ay ang weight-1 na Pauli na ZjZ_j sa chain lang, kung saan nagkakansela ang mga ZZ chain ng dalawang Majorana string.

Dahil ang bawat number operator ay nagma-map pa rin sa iisang ZZ string, ang isang Fock state ay computational basis state pa rin sa ilalim ng anumang ternary tree, pero hindi na binabasa ng bit qq ang occupation qq. Sa halip, ang occupation jj ay ang parity ng mga nasukat na bit sa support ng image ng njn_j. Basahin ang mga support na iyon mula sa mapper sa halip na hinuhugot mula sa tree, para manatiling tama ang decoding para sa anumang hugis o permutasyon ng mode:

>>> for mode in range(4):
... number = FermionOperator.from_dict({((True, mode), (False, mode)): 1.0})
... print(mode, ternary_tree(number, encoding, 4).simplify())
0 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2 Z_1 Z_0)>
1 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_1)>
2 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2)>
3 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_3)>

Sa balanced tree na ito, okupado ang mode 0 kapag ang mga qubit 0, 1, at 2 ay may kakaibang bilang ng mga isa, kaya ang paghahanda nito ay nangangahulugan ng pag-flip sa qubit 0 lang, samantalang ang pag-decode ng nasukat na bitstring ay kumukuha ng parity sa lahat ng tatlo. Ang isang bit-flip error sa qubit 1 ay sumisira sa mga occupation ng parehong mode 0 at mode 1, na mahalaga sa anumang workflow na nagbabasa ng mga occupation mula sa maiingay na sample.

Kapag ang isang Evolution ay ibinababa sa pamamagitan ng ganitong mapper, balutin ang mapper sa group_wise() (tingnan ang Synthesize an Evolution gate in fermionic space). Ang isang hopping term ay nagma-map sa dalawang nagko-commute na Pauli string, na wala ni isa ang nagtitipid ng particle number nang mag-isa; ang produkto lang nila sa evolution ang nagtitipid. Sa ilalim ng Jordan-Wigner, parehong kumikilos ang dalawang string sa parehong mga qubit, kaya ang pinasimpleng pagkakasunod-sunod ng term ay pinananatili silang magkatabi. Sa ilalim ng ibang mga tree, magkaiba ang kanilang mga support (tulad ng Y_3 Z_1 Y_0 at X_3 Z_2 X_0 kanina), pinag-iinterleave sila ng parehong raw at pinasimpleng pagkakasunod-sunod sa mga image ng ibang term, at ang qubit-side na product formula ay nagpapatulo na ng particle number.

Bumuo ng arbitraryong tree​

Sakop ng chain at breadth-first na mga constructor ang mga pare-parehong pamilya, pero ang pangkalahatang interface ay kumukuha ng isang pares na (parent, label) bawat node, na may None para sa root (ang C API ay kumukuha ng index ng root sa halip). Mahalaga ito dahil ang mga tree na sulit hanapin ay kadalasang hindi chain ni pare-pareho – ang Bonsai algorithm, halimbawa, ay nagpapalago ng spanning tree ng coupling graph ng isang device, kaya sinusunod ng pagsasanga nito ang konektibidad na iyon.

>>> # Node 0 branches three ways, node 1 twice, and node 4 carries a single child.
>>> tree = TernaryTree([None, (0, "X"), (0, "Y"), (0, "Z"), (1, "X"), (1, "Z"), (4, "Y")])
>>> tree.num_nodes, tree.num_legs, tree.max_weight
(7, 15, 4)
>>> tree.children(0)
{'X': 1, 'Y': 2, 'Z': 3}

Pansinin ang bilang ng leg: palagi itong 2n+12n+1, anuman ang hugis. Ang node na mas kaunti ang anak ay hindi bumubuo ng mas kaunting Pauli string – ang mga hindi nagamit nitong link ay mga leg, at ang isang leg ay bumubuo ng string tulad ng mga inapo ng isang edge. Sa 2n+12n+1 na string na iyon, 2n2n ang nagpapares sa mga Majorana operator ng nn mode, at eksaktong isa ang natitira.

Kabuuang parity​

Ang natirang leg na iyon ay ang kabuuang fermionic parity na ∏j(1−2nj)\prod_j (1 - 2 n_j). Katangian ito ng encoding at hindi mapping ng anumang operator, kaya isa itong method sa mismong encoding:

>>> TernaryTreeEncoding(TernaryTree.chain(4, "Z")).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_2 Z_1 Z_0)>
>>> TernaryTreeEncoding(TernaryTree.breadth_first(4, 3)).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_0)>

Tulad ng ipinapakita ng balanced na kaso, ito ay hindi sa pangkalahatan ang all-ZZ na string: ito ang leg na naaabot ng all-ZZ na landas, na kumikilos bilang identity sa bawat node na wala sa landas na iyon.

Important

Ang kabuuang parity dito ay isang observable na susukatin, hindi constraint na ipapataw. Parehong pisikal ang dalawa nitong eigenvalue, kaya huwag mag-project ng state sa +1+1 na sektor: ang isang ternary-tree encoding ay gumagamit ng isang qubit bawat mode at onto ito sa buong 2n2^n-dimensional na espasyo, kaya walang di-pisikal na subspace na maaaring i-project palayo.

Halimbawang ginawa: isang Bravyi-Kitaev tree​

Mas madalas na kinukwenta ang mga espesipikasyon kaysa isinusulat nang direkta, dahil bihirang sapat na maliit ang tree ng totoong encoding para isulat nang mano-mano. Magandang halimbawa ang Bravyi-Kitaev: kabilang ito sa pamilyang ito, pero hindi pare-pareho ang tree nito, kaya walang convenience constructor na sumasaklaw dito.

Binubuo ang tree nito sa pamamagitan ng recursive na paghahati sa mga node, isa para sa bawat mode. Sa loob ng isang saklaw ng mga node na [l,r][l, r], hayaang ang hh ay ang pinakamalaking power ng dalawa na kasya; ang node na inilalagay para sa saklaw na iyon ay l+h−1l + h - 1, ang huli sa block na iyon. Ang X link nito ay bumababa sa tree na binuo mula sa block bago nito at ang Z link nito sa tree na binuo mula sa natitira pagkatapos nito, at ang Y link nito ay naiiwang leg:

>>> def bravyi_kitaev_tree(num_modes: int) -> TernaryTree:
... """Builds the ternary tree of the Bravyi-Kitaev encoding on ``num_modes`` modes."""
... spec: list[tuple[int, str] | None] = [None] * num_modes
... def build(left: int, right: int) -> int | None:
... if left > right:
... return None # an empty range: the parent's link stays a leg
... block = 1
... while 2 * block <= right - left + 1:
... block *= 2
... node = left + block - 1
... for child, label in (
... (build(left, node - 1), "X"), # the block before this node
... (build(node + 1, right), "Z"), # the remainder after it
... ):
... if child is not None:
... spec[child] = (node, label)
... return node
... build(0, num_modes - 1)
... return TernaryTree(spec)

Sa apat na mode, iisang block ang buong saklaw, kaya ang unang inilalagay na node ay 3, na may [0,2][0, 2] bago nito at wala pagkatapos; ang sub-range na iyon ay nagbibigay ng node 1 sa ibabaw ng mga nag-iisang node 0 at 2. Kinukumpirma ng pag-compile ng tree na ang encoding ay Bravyi-Kitaev at hindi lang basta tree na may parehong uri ng hugis – ang una nitong Majorana image ay X0X1X3X_0 X_1 X_3:

>>> tree = bravyi_kitaev_tree(4)
>>> tree.children(1)
{'X': 0, 'Z': 2}
>>> encoding = TernaryTreeEncoding(tree)
>>> encoding.majorana_image(0)
[(0, 'X'), (1, 'X'), (3, 'X')]

Ginagawang konkreto ng kabuuang parity nito ang punto ng nakaraang seksyon: ang natirang leg ay ang naaabot ng all-ZZ na landas, at dito humihinto ang landas na iyon sa root, kaya ang observable ay ang single-qubit na Z3Z_3 sa halip na isang string sa lahat ng apat na qubit:

>>> encoding.total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3)>

Walang kailangan sa pagpapalaki kundi mas malaking argumento. Dahil walang node na gumagamit ng Y link nito para sa isang edge, bawat isa ay sumasanga nang hanggang dalawang direksyon lang, kaya ang Pauli weight ay lumalaki nang logarithmic sa halip na linear:

>>> [bravyi_kitaev_tree(n).max_weight for n in (4, 8, 16, 32, 64)]
[3, 4, 5, 6, 7]