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():
- PYTHON
- C
>>> 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)>
// The C API provides direct mapping functions for common transformations.
// Custom mapper prototyping as shown in Python is primarily a Python feature.
// For custom mappings in C, you will need to perform the iteration and
// conversion logic entirely by yourself.
Ang mga pangunahing hakbang para makagawa ng anumang custom mapper ay:
-
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.
-
Gumamit ng isa sa mga ibinigay na mapping function: Ang
mappersmodule 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 ngoperatorsmodule. Halimbawa, gumagana angmap_majorana_action_generators()sa mga Majorana operator. -
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.
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:
- PYTHON
- C
>>> 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
#include <qiskit_fermions.h>
// Create a MajoranaOperator with the same terms as maj_op above
uint64_t num_terms = 2;
uint64_t num_modes = 3;
uint32_t modes[3] = {0, 1, 2};
QkComplex64 coeffs[2] = {{1.0, 0.0}, {0.5, 0.0}};
uint32_t boundaries[3] = {0, 2, 3};
QfMajoranaOperator *maj_op = qf_maj_op_new(num_terms, num_modes, coeffs, modes, boundaries);
// Convert MajoranaOperator to FermionOperator using library mapper
QfFermionOperator *ferm_op = qf_majorana_to_fermion(maj_op);
// Apply Jordan-Wigner mapper to get qubit operator
QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_jordan_wigner(ferm_op, 2, &qubit_op);
assert(exit == QfExitCode_Success);
// Clean up
qf_ferm_op_free(ferm_op);
qf_maj_op_free(maj_op);
qk_obs_free(qubit_op);
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 string nito kasabay ng mode index, kaya ang isang Majorana operator sa huli sa mode ay nagma-map sa isang Pauli string na may weight na . 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 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 na bumababa mula sa node ay nag-aambag ng sa qubit . 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.
- PYTHON
- C
>>> 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
#include <qiskit_fermions.h>
QfTernaryTree *chain;
qf_ternary_tree_chain(13, QfPauliLabel_Z, &chain);
QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *balanced;
qf_ternary_tree_breadth_first(13, 3, order, &balanced);
assert(qf_ternary_tree_max_weight(chain) == 13);
assert(qf_ternary_tree_max_weight(balanced) == 3);
qf_ternary_tree_free(chain);
qf_ternary_tree_free(balanced);
Optimal ang maximum weight na iyon. Pinatunayan nina Jiang et al. na ang mga Majorana operator ng anumang fermion-to-qubit mapping ng mode ay may average na Pauli weight na hindi bababa sa , kaya walang mapping na makapagpapababa sa pinakamalaki sa mga ito sa ibaba ng . Eksaktong 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.
- PYTHON
- C
>>> 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)>
// Create the hopping Hamiltonian a^dagger_0 a_3 + a^dagger_3 a_0
QfFermionOperator *hamiltonian = qf_ferm_op_zero();
QkComplex64 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
bool actions[4] = {true, false, true, false};
uint32_t indices[4] = {0, 3, 3, 0};
for (int i = 0; i < 2; i++) {
qf_ferm_op_add_term(hamiltonian, 2, actions + 2 * i, indices + 2 * i, &coeffs[i]);
}
QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *tree;
qf_ternary_tree_breadth_first(4, 3, order, &tree);
// Compile the tree once, then reuse the encoding for every operator mapped through it.
QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);
QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_ternary_tree(hamiltonian, encoding, 4, &qubit_op);
assert(exit == QfExitCode_Success);
// Clean up
qf_ferm_op_free(hamiltonian);
qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(qubit_op);
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)>
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 sa chain lang; sa ibang tree, ang ay nagiging isang string sa isang set ng mga qubit. Gayundin, ang vertex operator na ay ang weight-1 na Pauli na sa chain lang, kung saan nagkakansela ang mga chain ng dalawang Majorana string.
Dahil ang bawat number operator ay nagma-map pa rin sa iisang string, ang isang Fock state ay computational basis state pa rin sa ilalim ng anumang ternary tree, pero hindi na binabasa ng bit ang occupation . Sa halip, ang occupation ay ang parity ng mga nasukat na bit sa support ng image ng . 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.
- PYTHON
- C
>>> # 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}
// Node 0, the root, branches three ways, node 1 twice, and node 4 carries a single child.
// The root has no parent and no incoming link, so its entries are never read: any value works.
#define IGNORED 0
uint32_t parents[7] = {IGNORED, 0, 0, 0, 1, 1, 4};
QfPauliLabel labels[7] = {
IGNORED, QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z,
QfPauliLabel_X, QfPauliLabel_Z, QfPauliLabel_Y,
};
QfTernaryTree *tree;
QfExitCode exit = qf_ternary_tree_new(7, 0, parents, labels, &tree);
assert(exit == QfExitCode_Success);
assert(qf_ternary_tree_num_nodes(tree) == 7);
assert(qf_ternary_tree_num_legs(tree) == 15);
assert(qf_ternary_tree_max_weight(tree) == 4);
qf_ternary_tree_free(tree);
Pansinin ang bilang ng leg: palagi itong , 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 na string na iyon, ang nagpapares sa mga Majorana operator ng mode, at eksaktong isa ang natitira.
Kabuuang parity
Ang natirang leg na iyon ay ang kabuuang fermionic parity na . Katangian ito ng encoding at hindi mapping ng anumang operator, kaya isa itong method sa mismong encoding:
- PYTHON
- C
>>> 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)>
QfTernaryTree *tree;
qf_ternary_tree_chain(4, QfPauliLabel_Z, &tree);
QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);
// On the chain this is the full Z string; on the balanced tree it is not.
QkObs *parity;
qf_ternary_tree_encoding_total_parity(encoding, 4, &parity);
qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(parity);
Tulad ng ipinapakita ng balanced na kaso, ito ay hindi sa pangkalahatan ang all- na string: ito ang leg na naaabot ng all- na landas, na kumikilos bilang identity sa bawat node na wala sa landas na iyon.
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 na sektor: ang isang ternary-tree encoding ay gumagamit ng isang qubit bawat mode at onto ito sa buong -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 , hayaang ang ay ang pinakamalaking power ng dalawa na kasya; ang node na inilalagay para sa saklaw na iyon ay , 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 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 :
>>> 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- na landas, at dito humihinto ang landas na iyon sa root, kaya ang observable ay ang single-qubit na 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]