Mapování reprezentací operátorů
Mappery jsou rutiny, které transformují operátory z jedné reprezentace do druhé při zachování matematické ekvivalence. Používej je k flexibilní práci napříč různými operátorovými algebrami a k přípravě operátorů pro spuštění kvantových obvodů.
Implementace vlastních mapperů
Modul mapperů usnadňuje implementaci vlastních mapování. Mappery fungují tak, že transformují základní akce (kreaci, anihilaci, Majoranovu akci a podobně), z nichž se operátor skládá, a poté tyto transformované akce kombinují podle pravidel cílové reprezentace.
Jako konkrétní příklad si vezměme Jordanovu-Wignerovu transformaci, která pomocí map_majorana_action_generators() převádí MajoranaOperator na qubitový operátor:
- 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.
Klíčové kroky při implementaci jakéhokoli vlastního mapperu jsou:
-
Definuj transformaci akcí: Urči, jak se každá akce (indexovaná podle módu) mapuje na cílovou reprezentaci. Ve výše uvedeném příkladu se Majoranovy akce mapují na Pauliho řetězce podle pravidel Jordanovy-Wignerovy transformace.
-
Použij jednu z poskytnutých mapovacích funkcí: Modul
mappersnabízí pomocné funkce, které zajišťují procházení členů operátoru a následné použití vlastní mapy akcí pro reprezentace operátorů z moduluoperators. Napříkladmap_majorana_action_generators()pracuje s Majoranovými operátory. -
Získej výsledek: Mapovací funkce vrátí operátor v cílové reprezentaci, na jehož členy se transformace aplikovala konzistentně.
Díky tomuto návrhu lze snadno vytvářet prototypy vlastních mapování bez hluboké znalosti vnitřních struktur operátorů. Transformace se automaticky aplikuje na každý člen operátoru a přitom zůstává zachována matematická ekvivalence.
Členy operátoru můžeš také procházet ručně, místo abys použil některou z poskytnutých iteračních funkcí. Podrobnosti najdeš v části o iteraci členů v průvodci operátory.
Knihovní implementace
Modul qiskit_fermions.mappers.library poskytuje efektivní a důkladně otestované implementace běžných mapování. Řídí se stejným principem jako vlastní mappery, ale jsou optimalizované pro produkční nasazení a jsou k dispozici v Python i C API.
Následující příklad aplikuje knihovní mapper na stejný MajoranaOperator z části o vlastních mapperech a ukazuje ekvivalentní výsledky:
- 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);
Dvoukrokový postup výše je rozepsán proto, aby ukázal složení dvou knihovních mapperů. V praxi bys MajoranaOperator mapoval raději v jednom kroku pomocí majorana_jordan_wigner() (qf_maj_op_jordan_wigner() v C API) a stejně tak u ostatních typů operátorů – edge_vertex_jordan_wigner() a transfer_vertex_jordan_wigner() (qf_edge_op_jordan_wigner(), qf_transfer_op_jordan_wigner()). Cesta přes FermionOperator také nafukuje mezivýsledek: každý generátor těchto algeber se mapuje na jediný Pauliho řetězec, zatímco každá fermionická akce se mapuje na součet dvou členů, takže člen sestavený z několika generátorů se nejprve rozroste, aby se pak znovu sloučil. Tato úspora roste s délkou členů; u členů s jediným generátorem v hoppingovém hamiltoniánu stojí obě cesty zhruba stejně.
V Python API jsou některé z těchto knihovních mapperů dostupné i přes univerzální pohodlné mappery nezávislé na typu. fermion_operator() a majorana_operator() stojí před majorana_to_fermion() a fermion_to_majorana() (mimo jiné) a interně deleguji na vhodnou knihovní implementaci podle typu předaného objektu, takže nemusíš správnou funkci vybírat sám. Totéž platí pro jordan_wigner() a pro ternary_tree() (představený níže): každý z nich předá práci té přímé implementaci, která odpovídá danému operátoru. Jak je toto předávání implementováno uvnitř, najdeš v qiskit_fermions.protocols.
Zvol kódování pomocí ternárních stromů
Jordanova-Wignerova transformace není jediným kódováním fermionů na qubity a není ani nejlevnější. Její řetězce rostou s indexem módu, takže Majoranův operátor na posledním z módů se mapuje na Pauliho řetězec o váze . Kódování, která rozprostírají informaci o paritě do stromu místo do řetězce, jsou asymptoticky lepší.
Ternární strom o uzlech zachycuje celou tuto rodinu. Každý uzel je qubit se třemi odchozími spojeními označenými X, Y a Z; spojení buď vede k potomku (hrana), nebo končí (list). Cesta každého listu zpět ke kořeni vyjadřuje Pauliho řetězec: přechod spojením označeným , které vede z uzlu , přispívá operátorem na qubitu . Každý takový strom dává platné kódování a známá kódování jsou prostě jeho zvláštní tvary – řetěz podél Z je Jordanova-Wignerova transformace, zatímco strom, v němž má každý uzel tři potomky, pokud to počet uzlů dovolí, je vyvážený.
- 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);
Tato maximální váha je optimální. Jiang et al. dokazují, že Majoranovy operátory jakéhokoli mapování fermionů na qubity pro módů mají průměrnou Pauliho váhu alespoň , takže žádné mapování nemůže snížit největší z nich pod . Oba stromy používají přesně qubitů, takže úspora je v Pauliho váze, nikoli v počtu qubitů.
Chceš-li operátor namapovat, zkompiluj strom do kódování a předej ho mapperu spolu s počtem qubitů výsledku. Kompilace spáruje listy stromu do Majoranových operátorů a jednou sestaví jejich Pauliho řetězce, takže totéž kódování by se mělo používat opakovaně pro mnoho operátorů. Počet qubitů zůstává u volání mapování, nikoli u kódování, takže jedno kódování může sloužit registrům různých šířek; počet větší než počet uzlů stromu se doplní identitou.
- 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);
Mapování přes řetěz podél Z přesně reprodukuje fermion_jordan_wigner() (qf_ferm_op_jordan_wigner() v C API), v čemž spočívá smysl, v němž ho toto kódování zobecňuje:
>>> 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
Každá reprezentace operátoru má mapper pro ternární strom, který odpovídá sadě pro Jordanovu-Wignerovu transformaci; úplný seznam najdeš v qiskit_fermions.mappers.library a v části Mapper Library v C API. V Python API jsou dostupné také přes ternary_tree(), univerzální obal nezávislý na typu, který vybere správný mapper pro daný operátor, stejně jako to dělá 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)>
Některé identity, které platí v Jordanově-Wignerově transformaci, jsou vlastnostmi řetězu, nikoli ternárních stromů. Operátor počtu je jen na řetězu; na jiném stromu se stává řetězcem přes množinu qubitů. Podobně vrcholový operátor je Pauliho operátor o váze 1 jen na řetězu, kde se řetězce obou Majoranových řetězců vzájemně vyruší.
Protože se každý operátor počtu stále mapuje na jediný řetězec , je Fockův stav pod jakýmkoli ternárním stromem stále stavem výpočetní báze, ale bit už neodpovídá obsazení . Obsazení je místo toho parita naměřených bitů na nosiči obrazu . Tyto nosiče vyčti z mapperu, místo abys je odvozoval ze stromu, aby dekódování zůstalo správné pro libovolný tvar či permutaci módů:
>>> 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)>
Na tomto vyváženém stromu je mód 0 obsazen, když qubity 0, 1 a 2 drží lichý počet jedniček, takže jeho příprava znamená převrátit samotný qubit 0, zatímco dekódování naměřeného bitového řetězce vyžaduje paritu přes všechny tři. Jediná chyba převrácení bitu na qubitu 1 pak poškodí obsazení módu 0 i módu 1, což je důležité pro každý postup, který čte obsazení z zašuměných vzorků.
Když se Evolution převádí přes takový mapper, obal mapper do group_wise() (viz Syntéza hradla Evolution ve fermionickém prostoru). Hoppingový člen se mapuje na dva komutující Pauliho řetězce, z nichž žádný sám nezachovává počet částic; zachovává ho teprve jejich součin v evoluci. V Jordanově-Wignerově transformaci oba řetězce působí na stejné qubity, takže zjednodušené pořadí členů je udržuje vedle sebe. Na jiných stromech se jejich nosiče liší (jako u Y_3 Z_1 Y_0 a X_3 Z_2 X_0 výše), surové i zjednodušené pořadí je prokládá obrazy jiných členů a součinový vzorec na straně qubitů pak počet částic ztrácí.
Sestav libovolný strom
Konstruktory řetězu a prohledávání do šířky pokrývají jednotné rodiny, ale obecné rozhraní bere jeden pár (parent, label) na uzel, přičemž kořen má None (C API místo toho bere index kořene). To je důležité, protože stromy, které stojí za hledání, nejsou obvykle ani řetěz, ani jednotné – například algoritmus Bonsai vytváří kostru vazebního grafu zařízení, takže jeho větvení sleduje tuto konektivitu.
- 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);
Všimni si počtu listů: vždy je , ať je tvar jakýkoli. Uzel s méně potomky nevytváří méně Pauliho řetězců – jeho nepoužitá spojení jsou listy a list vyjadřuje řetězec stejně jako potomci hrany. Z těchto řetězců se spáruje do Majoranových operátorů módů a přesně jeden zbude.
Celková parita
Ten zbylý list je celková fermionická parita . Je to vlastnost kódování, nikoli mapování nějakého operátoru, takže je to metoda samotného kódování:
- 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);
Jak ukazuje vyvážený případ, toto není obecně řetězec složený ze samých : je to list dosažený cestou složenou ze samých , který působí jako identita na každém uzlu, jenž na této cestě neleží.
Celková parita je zde pozorovatelná k měření, nikoli omezení k vynucení. Obě její vlastní hodnoty jsou fyzikální, takže stav neprojektuj na sektor : kódování ternárním stromem spotřebuje jeden qubit na mód a pokrývá celý -rozměrný prostor, takže nezůstává žádný nefyzikální podprostor, který by bylo třeba odstranit.
Pracovní příklad: strom Bravyiho-Kitaeva
Specifikace se častěji počítají, než aby se psaly ručně, protože strom skutečného kódování je málokdy dost malý, aby šel napsat ručně. Bravyiho-Kitaevovo kódování je dobrý příklad: patří do této rodiny, ale jeho strom není jednotný, takže ho žádný pohodlný konstruktor nepokrývá.
Jeho strom se staví rekurzivním dělením uzlů, jeden na mód. V rozsahu uzlů buď největší mocnina dvou, která se vejde; uzel umístěný pro tento rozsah je , poslední v onom bloku. Jeho spojení X vede ke stromu postavenému z bloku před ním a jeho spojení Z ke stromu postavenému ze zbytku za ním, přičemž jeho spojení Y zůstane listem:
>>> 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)
Při čtyřech módech je celý rozsah jediný blok, takže jako první se umístí uzel 3, s před ním a ničím za ním; tento podrozsah dává uzel 1 nad jednotlivými uzly 0 a 2. Kompilace stromu potvrdí, že kódování je Bravyiho-Kitaevovo a nikoli jen strom téhož tvarového typu – jeho první Majoranův obraz je :
>>> 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')]
Jeho celková parita konkretizuje myšlenku předchozí části: zbylý list je ten, který je dosažen cestou složenou ze samých , a zde se tato cesta zastaví u kořene, takže pozorovatelná je jednoqubitové , a nikoli řetězec přes všechny čtyři qubity:
>>> encoding.total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3)>
Zvětšení nevyžaduje nic než větší argument. Protože žádný uzel nikdy nevyužije své spojení Y pro hranu, každý se větví nejvýše dvěma směry, takže Pauliho váha roste logaritmicky, nikoli lineárně:
>>> [bravyi_kitaev_tree(n).max_weight for n in (4, 8, 16, 32, 64)]
[3, 4, 5, 6, 7]