Přeskočit na hlavní obsah
Tento projekt je v betě. Našel jsi chybu? Otevřít issue na GitHubu. Máš otázku nebo nápad? Zeptej se v Discussions — rádi si poslechneme tvou zpětnou vazbu!
Jaká je kvalita překladu na této stránce?

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:

>>> 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)>

Klíčové kroky při implementaci jakéhokoli vlastního mapperu jsou:

  1. 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.

  2. Použij jednu z poskytnutých mapovacích funkcí: Modul mappers nabízí pomocné funkce, které zajišťují procházení členů operátoru a následné použití vlastní mapy akcí pro reprezentace operátorů z modulu operators. Například map_majorana_action_generators() pracuje s Majoranovými operátory.

  3. 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.

Tip

Č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:

>>> 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
Poznámka

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 ZZ rostou s indexem módu, takže Majoranův operátor na posledním z nn módů se mapuje na Pauliho řetězec o váze nn. Kódování, která rozprostírají informaci o paritě do stromu místo do řetězce, jsou asymptoticky lepší.

Ternární strom o nn 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 PP, které vede z uzlu uu, přispívá operátorem PP na qubitu uu. 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ý.

>>> 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

Tato maximální váha je optimální. Jiang et al. dokazují, že Majoranovy operátory jakéhokoli mapování fermionů na qubity pro nn módů mají průměrnou Pauliho váhu alespoň log⁡3(2n)\log_3(2n), takže žádné mapování nemůže snížit největší z nich pod ⌈log⁡3(2n+1)⌉\lceil \log_3(2n+1) \rceil. Oba stromy používají přesně nn 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.

>>> 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)>

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)>
Poznámka

Některé identity, které platí v Jordanově-Wignerově transformaci, jsou vlastnostmi řetězu, nikoli ternárních stromů. Operátor počtu je nj=(1−Zj)/2n_j = (1 - Z_j)/2 jen na řetězu; na jiném stromu se ZjZ_j stává řetězcem ZZ přes množinu qubitů. Podobně vrcholový operátor VjV_j je Pauliho operátor ZjZ_j o váze 1 jen na řetězu, kde se řetězce ZZ obou Majoranových řetězců vzájemně vyruší.

Protože se každý operátor počtu stále mapuje na jediný řetězec ZZ, je Fockův stav pod jakýmkoli ternárním stromem stále stavem výpočetní báze, ale bit qq už neodpovídá obsazení qq. Obsazení jj je místo toho parita naměřených bitů na nosiči obrazu njn_j. 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.

>>> # 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}

Všimni si počtu listů: vždy je 2n+12n+1, 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 2n+12n+1 řetězců se 2n2n spáruje do Majoranových operátorů nn módů a přesně jeden zbude.

Celková parita​

Ten zbylý list je celková fermionická parita ∏j(1−2nj)\prod_j (1 - 2 n_j). Je to vlastnost kódování, nikoli mapování nějakého operátoru, takže je to metoda samotného kódování:

>>> 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)>

Jak ukazuje vyvážený případ, toto není obecně řetězec složený ze samých ZZ: je to list dosažený cestou složenou ze samých ZZ, který působí jako identita na každém uzlu, jenž na této cestě neleží.

Důležité

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 +1+1: kódování ternárním stromem spotřebuje jeden qubit na mód a pokrývá celý 2n2^n-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ů [l,r][l, r] buď hh největší mocnina dvou, která se vejde; uzel umístěný pro tento rozsah je l+h−1l + h - 1, 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 [0,2][0, 2] 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 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')]

Jeho celková parita konkretizuje myšlenku předchozí části: zbylý list je ten, který je dosažen cestou složenou ze samých ZZ, a zde se tato cesta zastaví u kořene, takže pozorovatelná je jednoqubitové Z3Z_3, 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]