Mapování reprezentací operátorů
Mapper převádí BosonOperator na qubitový operátor, zde SparseObservable. Samotný operátor o qubitech nic neví: ukládá žebříkové operátory na módech a bosonový mód pojme libovolně mnoho částic. Vše, co operátor činí konečným (kolik obsazovacích úrovní ponecháš a jak jsou tyto úrovně rozloženy na qubitech), se rozhoduje při jeho mapování.
Knihovní mapper occupation_encoding() pracuje v obsazovací (Fockově) bázi: úroveň módu je stav s bosony. Přijímá operátor a LevelSpec, který obsahuje ořezání a kódování. Tento průvodce vysvětluje, co tyto dvě volby znamenají a co každá z nich stojí. Kompletní postup, který je používá, najdeš v části Simulace dynamiky Bose-Hubbard s výběrem kódování; jak transpiler aplikuje tytéž volby na celý obvod, viz Transpilace bosonových obvodů.
Ořezání Fockova prostoru
Mód, který je ponechán na úrovních, kde je jeho max_occupation, již neplní kanonickou komutační relaci. Označíme-li anihilační operátor ořezaný na tyto úrovně,
relace platí na každé úrovni kromě nejvyšší [1]. Můžeš si to ověřit tak, že zmapuješ komutátor obou žebříkových operátorů a přečteš jeho diagonálu na úrovních jednoho módu. Následující pomocná funkce sestaví hustou matici zmapovaného operátoru pro jeden mód, což dává smysl jen pro velmi malé registry:
>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp
>>> from qiskit_bosons.mappers import LevelEncoding, LevelSpec
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> def level_diagonal(operator, max_occupation, encoding):
... """The diagonal of a mapped one-mode operator on the codewords of the levels."""
... spec = LevelSpec(max_occupation, encoding)
... matrix = SparsePauliOp.from_sparse_observable(occupation_encoding(operator, spec)).to_matrix()
... codewords = [spec.encoding.level_to_bits(level) for level in range(max_occupation + 1)]
... return [round(float(matrix[c, c].real), 6) for c in codewords]
>>>
>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dagger = BosonOperator.from_dict({(cre(0),): 1.0})
>>> commutator = (b @ b_dagger) - (b_dagger @ b)
>>> level_diagonal(commutator, 2, "unary_unprojected") # d = 3
[1.0, 1.0, -2.0]
Poslední položka je . Protože to znamená, že lineární obal není uzavřený vůči komutátoru, žádné kódování není izomorfismem algeber. Kódování mapuje stavy spolu s operátory, které působí uvnitř ořezaného podprostoru. V praxi jsou důležité následující dva důsledky.
Ořezání musí ohraničovat dosažitelné obsazení. Nestačí ohraničit obsazení, se kterým začínáš. Beamsplitter působící na zaplní a , takže ořezání na jedničku tiše vytvoří chybný stav:
>>> from scipy.linalg import expm
>>>
>>> beamsplitter = BosonOperator.from_dict({(cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0})
>>>
>>> def stays_in_11(max_occupation):
... """Probability to remain in |1, 1> after a 50:50 beamsplitter."""
... spec = LevelSpec(max_occupation, "gray").resolve(2)
... observable = occupation_encoding(beamsplitter, spec, spec.total_width)
... matrix = SparsePauliOp.from_sparse_observable(observable).to_matrix()
... unitary = expm(-1j * (np.pi / 4) * matrix)
... index = sum(spec.encoding.level_to_bits(1) << offset for offset in spec.offsets)
... return round(float(abs(unitary[index, index]) ** 2), 6)
>>>
>>> stays_in_11(1), stays_in_11(2)
(1.0, 0.0)
S ořezáním na jedničku je pravděpodobnost setrvání v rovna jedné, protože dva stavy, které by dynamika zaplnila, neexistují. S ořezáním na dvojku vymizí, což je Hongův-Ouův-Mandelův jev. Proto je max_occupation povinný argument, který nikdy nemá výchozí hodnotu: je to modelovací rozhodnutí a konvergenční parametr, který máš procházet v rozsahu hodnot.
Mapování nekomutuje s normálním uspořádáním. Operátory a jsou před ořezáním stejné, ale mapují se na různé qubitové operátory:
>>> product = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> ordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0}) + BosonOperator.one()
>>> bool(product.equiv(ordered))
True
>>> level_diagonal(product, 3, "unary_unprojected") # d = 4
[1.0, 2.0, 3.0, 0.0]
>>> level_diagonal(ordered, 3, "unary_unprojected")
[1.0, 2.0, 3.0, 4.0]
Oba se shodují na všech úrovních kromě nejvyšší. Aplikován na nejvyšší úroveň nemá kam jít a amplituda se ořízne, takže je tam nula, zatímco dává . Rozhodni se jednou provždy, zda je tvůj hamiltonián definován v normálně uspořádané podobě, a tu pak mapuj konzistentně.
Při volbě ořezání pomáhají tři metody a všechny dávají dolní odhady. Operátor hlásí největší počet tvořicích operátorů, které v jednom členu aplikuje na každý mód, a spec jej porovnává se svými ořezáními:
>>> quartic = BosonOperator.from_dict({(cre(0), cre(0), ann(0), ann(0)): 1.0})
>>> quartic.mode_occupations(), quartic.max_occupation()
([2], 2)
>>> LevelSpec(1, "gray").truncation(quartic) # one level short
[1]
>>> LevelSpec(2, "gray").truncation(quartic)
[0]
Nenulová položka znamená, že ořezání je pod mezí, a hodnota říká o kolik. Tyto pomocné funkce nevědí nic o dynamice, takže úspěšná kontrola neznamená, že je ořezání dostatečné; výše uvedený beamsplitter má mez jedna, a přesto potřebuje ořezání na dvě. Mohou také hlásit nedostatek pro člen, který je na zachovaných úrovních nulový, jako je při ořezání na jedničku.
Popis ořezání pomocí LevelSpec
LevelSpec sdružuje ořezání každého módu s kódováním a odvozuje z nich, kolik qubitů každý mód zabírá a kde jeho qubity začínají. Všechno, co se na tom musí shodovat (mapper, rozložení v transpileru, příprava stavu), dostane stejný spec. Kódování je povinný argument a ořezání také: ani jedno nemá výchozí hodnotu.
Ořezání je buď jedno celé číslo sdílené všemi módy, nebo jedno celé číslo pro každý mód:
>>> shared = LevelSpec(2, "gray")
>>> per_mode = LevelSpec([4, 2, 2], "gray")
>>> shared.num_modes is None, per_mode.num_modes
(True, 3)
Spec sestavený z jednoho celého čísla nechává počet módů otevřený, takže ho musíš určit dříve, než se zeptáš na šířky. Posloupnost se nikdy nerozšiřuje na všechny módy, ať má jakoukoli délku, a pevně určuje počet módů:
>>> per_mode.widths, per_mode.offsets, per_mode.total_width
([3, 2, 2], [0, 3, 5], 7)
>>> shared.resolve(3).widths
[2, 2, 2]
>>> try:
... shared.widths
... except ValueError as error:
... print(error)
this spec was built from a single cutoff, so its number of modes is open; call `resolve(num_modes)` first
Mód zabírá qubity offsets[m] až offsets[m] + widths[m] - 1, takže módy s různým ořezáním mohou zabírat různý počet qubitů a offsety jsou průběžným součtem šířek. Počet módů mapperu předávat nemusíš. Převezme ho ze specu, nebo u specu s jedním ořezáním z operátoru pomocí min_num_modes(). Registr může být širší, než módy potřebují; přebytečné qubity jsou nečinné a leží na konci:
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> number.min_num_modes()
1
>>> occupation_encoding(number, LevelSpec(1, "gray")).num_qubits
1
>>> occupation_encoding(number, LevelSpec(1, "gray"), 4).num_qubits
4
Operátor, který působí na mód za posledním módem specu po jednotlivých módech, je odmítnut:
>>> far = BosonOperator.from_dict({(cre(5), ann(5)): 1.0})
>>> try:
... occupation_encoding(far, LevelSpec([1, 1], "gray"))
... except ValueError as error:
... print(error)
the operator acts on 6 modes, but the spec covers only 2
Několik omezení vyplývá z toho, jak kódování počítají qubity. U kompaktního kódování nezabírá mód s jedinou úrovní (max_occupation = 0) žádný qubit, protože jeho jediná úroveň nepotřebuje nic, čím by se odlišila od jiné. Takový mód je povolen a neposouvá qubity následujících módů, ale spec, ve kterém by žádný mód nezabíral qubit, je zamítnut. Unární kódování pojmou nejvýše 128 úrovní na mód a větší ořezání vyvolá chybu, která navrhuje kompaktní kódování:
>>> LevelSpec([0, 1], "gray").resolve(2).widths
[0, 1]
>>> try:
... LevelSpec(0, "gray")
... except ValueError as error:
... print(str(error).split(":")[0])
the spec would span no qubits
>>> try:
... LevelSpec(128, "unary_projected")
... except ValueError as error:
... print(str(error).split(" (")[0])
max_occupation = 128 is too large for the unary encodings, which hold at most 128 levels per mode
Volba kódování
LevelEncoding určuje, který bázový stav bloku qubitů představuje každou úroveň. Existují čtyři a žádné z nich není obecně nejlepší [2]. Počet qubitů na mód a bitový vzor každé úrovně z kódování vyplývají:
>>> encodings = (
... LevelEncoding.UNARY_PROJECTED,
... LevelEncoding.UNARY_UNPROJECTED,
... LevelEncoding.STANDARD_BINARY,
... LevelEncoding.GRAY,
... )
>>> rows = ["encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns"]
>>> for encoding in encodings:
... widths = [encoding.num_qubits(levels) for levels in (1, 2, 3, 4, 5, 8)]
... patterns = [f"{encoding.level_to_bits(level):05b}" for level in range(5)]
... rows.append(f"{encoding!s:18s} {widths!s:34s} {patterns}")
>>> print("\n".join(rows))
encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns
unary_projected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
unary_unprojected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
standard_binary [0, 1, 2, 2, 3, 3] ['00000', '00001', '00010', '00011', '00100']
gray [0, 1, 2, 2, 3, 3] ['00000', '00001', '00011', '00010', '00110']
Bitové vzory jsou celá čísla, v nichž bit je hodnota qubitu . Unární kódování spotřebují jeden qubit na úroveň a nastaví jeden z nich. Kompaktní kódování spotřebují qubitů a ukládají číslo úrovně, binárně nebo jako Grayův kód, v němž se sousední úrovně liší v jednom bitu. Řetězcový název kódování je přijímán všude, kde je přijímáno kódování.
Kompromis je mezi qubity a váhou operátoru. Kompaktní kódování potřebují exponenciálně méně qubitů, ale žebříkový operátor spojuje úrovně, jejichž vzory se mohou lišit v několika bitech, takže jeho členy působí na více qubitů. Následující části zkoumají, co to stojí, počínaje dvěma unárními kódováními, která mají shodné rozložení a liší se jen v operátoru.
Porovnání dvou unárních kódování
V unárním kódování žebříkový operátor přesouvá jednu excitaci mezi dvěma sousedními qubity. Abys to zapsal jako operátor na celém módu, musíš zvolit, co se stane s ostatními qubity. Obě varianty volí odlišně:
-
unary_projectedvkládá projektor na každý další qubit módu. Tím se one-hot omezení vynucuje v samotném operátoru: člen působí, jen když je zbytek módu prázdný. -
unary_unprojectedtyto qubity ponechává být, takže člen na nich působí jako identita.
Podívej se na jediný při v obou variantách. Mapper vrací SparseObservable, jehož členy jsou řetězce jednoqubitových bitových členů. Zde jsou první čtyři členy každé varianty spolu s qubity, na které působí:
>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(b_dagger, LevelSpec(3, encoding))
... terms = observable.to_sparse_list()
... print(encoding, "->", len(terms), "terms on", observable.num_qubits, "qubits")
... for label, qubits, coefficient in terms[:4]:
... print(f" {label:5s} on qubits {qubits} coefficient {complex(coefficient):.4f}")
unary_projected -> 12 terms on 4 qubits
XX00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
XY00 on qubits [0, 1, 2, 3] coefficient 0.0000-0.2500j
YX00 on qubits [0, 1, 2, 3] coefficient 0.0000+0.2500j
YY00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
unary_unprojected -> 12 terms on 4 qubits
XX on qubits [0, 1] coefficient 0.2500+0.0000j
XY on qubits [0, 1] coefficient 0.0000-0.2500j
YX on qubits [0, 1] coefficient 0.0000+0.2500j
YY on qubits [0, 1] coefficient 0.2500+0.0000j
Obě mají dvanáct členů se stejnými koeficienty. Projektované členy nesou bitový člen 0 na dvou qubitech, které neprojektované členy nezmiňují, takže každý projektovaný člen se dotýká všech čtyř qubitů a každý neprojektovaný dvou. Obecně se projektovaný člen dotýká všech qubitů módu, protože nese dva faktory, každý nebo , a projektor na každém dalším qubitu.
Oba operátory jsou si rovny na stavech, pro které je kódování určeno, tedy na one-hot bázových stavech (kódových slovech), a mimo ně se liší. Pro ověření vezmi malý operátor na dvou módech při a porovnej matice na devíti kódových slovech:
>>> hamiltonian = BosonOperator.from_dict({
... (cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0,
... (cre(0), ann(0)): 0.5, (cre(1), cre(1), ann(1), ann(1)): 0.25,
... })
>>> matrices = {}
>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(hamiltonian, LevelSpec(2, encoding))
... matrices[encoding] = SparsePauliOp.from_sparse_observable(observable).to_matrix()
>>> unary = LevelEncoding.UNARY_PROJECTED
>>> codewords = sorted(
... unary.level_to_bits(first) | (unary.level_to_bits(second) << 3)
... for first in range(3) for second in range(3)
... )
>>> projected, unprojected = matrices["unary_projected"], matrices["unary_unprojected"]
>>> bool(np.allclose(projected[np.ix_(codewords, codewords)], unprojected[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(projected, unprojected))
False
Oba také ponechávají podprostor kódových slov invariantní, takže stav připravený na kódovém slově zůstane na kódových slovech. Volba záleží jen u stavů, které tento podprostor opustí, například vlivem šumu. Tam projektovaný operátor evoluci potlačuje, protože jeho projektory selžou, zatímco neprojektovaný dál působí. Žádné z těchto chování není fyzikálním požadavkem. Neprojektovaný operátor je definován jen na one-hot podprostoru, takže jeho volba je modelovací rozhodnutí, a ne volná optimalizace.
Projektory stojí gaty. Nech evolvovat hermitovský na jednom módu po krátkou dobu a spočítej dvouqubitové gaty po transpilaci v Qiskitu:
>>> from qiskit import QuantumCircuit, transpile
>>> from qiskit.circuit.library import PauliEvolutionGate
>>>
>>> def cx_count(observable):
... circuit = QuantumCircuit(observable.num_qubits)
... circuit.append(PauliEvolutionGate(observable, time=0.1), range(observable.num_qubits))
... basis = ["cx", "rz", "sx", "x"]
... return transpile(circuit, basis_gates=basis, optimization_level=1).count_ops().get("cx", 0)
>>>
>>> hermitian = b + b_dagger
>>> levels = (3, 4, 5, 6)
>>> counts = {
... str(encoding): [
... cx_count(occupation_encoding(hermitian, LevelSpec(d - 1, encoding))) for d in levels
... ]
... for encoding in encodings
... }
Přesné počty závisejí na tom, jak Qiskit syntetizuje evoluční gate, což se mezi verzemi mění, proto je vykresli:
Počet u neprojektované varianty roste lineárně s , protože každý z přechodů je dvouqubitová rotace. Počet u projektované varianty roste mnohem rychleji, protože každý člen působí na všech qubitů. Kompaktní kódování potřebují méně qubitů, ale váha jejich členů závisí na tom, jak se liší bitové vzory sousedních úrovní: Grayův kód, v němž se liší v jednom bitu, je levnější ze dvou kompaktních kódování a standardní binární kód skáče, když přenos překlopí několik bitů najednou (). Zda kompaktní kódování předčí i neprojektované unární, závisí na a na tom, jak Qiskit evoluci syntetizuje, proto čti trend, a ne pořadí při jedné velikosti.
Projektory záleží také tehdy, když zmapovaný operátor rozvíjíš do Paulioperátorů, což dělá from_sparse_observable() tak, že každý projektor nahradí součtem dvou Pauliho operátorů. Člen s projektory se stane Pauliho členy. Zde je jediný při malém , což je jediná velikost, při které bys to měl zkoušet:
>>> rows = [f"{'d':>2s} {'stored (both)':>14s} {'expanded, projected':>20s} {'expanded, unprojected':>22s}"]
>>> for levels in (3, 4, 5):
... stored = len(occupation_encoding(b_dagger, LevelSpec(levels - 1, "unary_projected")))
... expanded = [
... len(SparsePauliOp.from_sparse_observable(
... occupation_encoding(b_dagger, LevelSpec(levels - 1, encoding))).simplify())
... for encoding in ("unary_projected", "unary_unprojected")
... ]
... rows.append(f"{levels:2d} {stored:14d} {expanded[0]:20d} {expanded[1]:22d}")
>>> print("\n".join(rows))
d stored (both) expanded, projected expanded, unprojected
3 8 16 8
4 12 48 12
5 16 128 16
Obě varianty ukládají stejný počet členů, ale rozvoj té projektované roste exponenciálně v . Pro cokoli většího než ukázkový příklad čti členy SparseObservable přímo, jako to dělá výpis výše, místo abys ho převáděl.
Kompaktní kódování své projektory vypustit nemohou. Nulové bity jsou tam součástí čísla úrovně, takže odstranění změní, které úrovně člen spojuje, a operátor už na kódových slovech není stejný. Proto existuje ve dvou variantách pouze unární kódování.
Součin žebříků se ořezává jako první
Další návrhová volba se projeví v ceně diagonálních členů. Mapper nemapuje každý žebříkový operátor zvlášť a výsledky nenásobí. Sebere žebříkové operátory, které člen aplikuje na jeden mód, vynásobí je jako matici ve Fockově bázi, tento součin jednou ořeže a výsledek zakóduje [2]. Součin, jako je operátor počtu částic , tak zůstává diagonální:
>>> number_observable = occupation_encoding(number, LevelSpec(3, "unary_unprojected"))
>>> [(label, qubits) for label, qubits, _ in number_observable.to_sparse_list()]
[('1', [1]), ('1', [2]), ('1', [3])]
>>> cx_count(number_observable)
0
Jeho členy jsou projektory na qubitu každé úrovně nad nulou, vážené číslem úrovně (úroveň nula má váhu nula, a tedy žádný člen). Jeho časový vývoj je množina jednoqubitových rotací a nepotřebuje žádný dvouqubitový gate. Vynásobení dvou zmapovaných žebříkových operátorů místo toho dává stejný operátor na kódových slovech, ale jiný mimo ně, s nediagonálními řetězci, které stojí gaty:
>>> unprojected = LevelSpec(3, "unary_unprojected")
>>> as_pauli = lambda operator: SparsePauliOp.from_sparse_observable(
... occupation_encoding(operator, unprojected, 4)).simplify()
>>> composed = as_pauli(b_dagger).dot(as_pauli(b)).simplify()
>>> direct = as_pauli(number)
>>> len(direct), len(composed)
(4, 16)
>>> cx_count(direct)
0
>>> bool(cx_count(composed) > 0)
True
>>> codewords = [LevelEncoding.UNARY_UNPROJECTED.level_to_bits(level) for level in range(4)]
>>> bool(np.allclose(direct.to_matrix()[np.ix_(codewords, codewords)],
... composed.to_matrix()[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(direct.to_matrix(), composed.to_matrix()))
False
Diagonální členy, například interakce na místě v modelu Bose-Hubbard, jsou právě tyto součiny, a proto na této volbě záleží.
Napsání vlastního mapperu
Pokud chceš kódování, které knihovna neposkytuje, sestav ho z obrazu jediného žebříkového operátoru. map_boson_action_generators() přijímá funkci, která mapuje jednu BosonAction, a funkci, která vrací identitu, a z nich sestaví mapper celých operátorů. Násobí obrazy akcí každého členu v pořadí, škáluje koeficientem a sčítá členy. Výstupem může být libovolný typ, který podporuje násobení skalárem a skládání.
Jako příklad vezmi kódování pro , které ukládá mód do jednoho qubitu. Tvořicí operátor je , což je v konvenci Qiskitu , a anihilační operátor je jeho adjungovaný operátor:
>>> from qiskit_bosons.mappers import map_boson_action_generators
>>> from qiskit_bosons.operators import BosonAction
>>>
>>> num_qubits = 2
>>>
>>> def map_action(action: BosonAction) -> SparsePauliOp:
... is_creation, mode = action
... sign = -1j if is_creation else 1j
... return SparsePauliOp.from_sparse_list(
... [("X", [mode], 0.5), ("Y", [mode], 0.5 * sign)], num_qubits=num_qubits
... )
>>>
>>> def identity() -> SparsePauliOp:
... return SparsePauliOp.from_sparse_list([("", [], 1.0)], num_qubits)
>>>
>>> model = BosonOperator.from_dict({
... (cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0, (ann(0), cre(0)): 0.7, (cre(1), ann(1)): 0.3,
... })
>>> custom = map_boson_action_generators(model, map_action, identity)
>>> library = SparsePauliOp.from_sparse_observable(occupation_encoding(model, LevelSpec(1, "gray")))
>>> bool(np.allclose(custom.to_matrix(), library.to_matrix()))
True
Tento ručně napsaný mapper násobí zmapované žebříkové operátory a s knihovním mapperem se zde shoduje, protože každý mód má dvě úrovně a qubit pojme celý mód. Při větším , jak ukázala předchozí část, nejsou skládání zmapovaných žebříků a prvotní ořezání součinu mimo kódová slova tentýž operátor.
Chceš-li takový mapper použít v obvodu, zabal ho do funkce se signaturou (operator, num_qubits) -> SparseObservable a předej ji do MapperFnEvolutionSynthesis, která jím mapuje operátor každého gatu Evolution. Pipeline transpileru je popsána v části Transpilace bosonových obvodů.
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_bosons.transpiler.passes import MapperFnEvolutionSynthesis
>>>
>>> def mapper_fn(operator, num_qubits):
... return SparseObservable.from_sparse_pauli_op(
... map_boson_action_generators(operator, map_action, identity)
... )
>>>
>>> synthesis = MapperFnEvolutionSynthesis(mapper_fn)
Za povšimnutí stojí dva detaily. Zaprvé, qubitový operátor musí mít reálné koeficienty, protože PauliEvolutionGate imaginární část odmítá. Hermitovský operátor se mapuje na reálný, ale až po sloučení členů, které se vyruší. Tento ručně napsaný mapper je neslučuje, takže hoppingový člen a jeho adjungovaný po sobě zanechají imaginární koeficienty, dokud výsledek nezjednodušíš. Knihovní mapper svůj výsledek slučuje; vlastní mapper to musí udělat sám, například tak, že ho zabalíš do simplify():
>>> hopping = BosonOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> hermitian_hopping = hopping + hopping.adjoint()
>>> raw = mapper_fn(hermitian_hopping, 2)
>>> bool(np.allclose(np.asarray(raw.coeffs).imag, 0))
False
>>> merged = raw.simplify()
>>> bool(np.allclose(np.asarray(merged.coeffs).imag, 0))
True
>>> sorted(label for label, _, _ in merged.to_sparse_list())
['XX', 'YY']
Zadruhé, součinový vzorec syntetizuje členy v pořadí, v jakém je dostane, a pořadí, které mapper produkuje, není zaručeně stabilní. Pomocné funkce simplify() a group_wise() obalují funkci mapperu, aby pořadí řídily: simplify slučuje duplicitní řetězce a řadí členy do kanonického pořadí, zatímco group_wise mapuje vždy jednu skupinu operátoru, takže členy skupiny zůstanou vedle sebe. Obě nekombinuj, protože řazení seskupení zahodí. Průvodce seskupováním ukazuje, jak skupiny přiřazovat, a průvodce syntézou ukazuje, jak pořadí ovlivňuje obvod.
Další kroky
-
Sleduj Simulace dynamiky Bose-Hubbard s výběrem kódování, kde najdeš úplnou simulaci, která prochází ořezání a porovnává kódování s přesnou dynamikou.
-
V části Transpilace bosonových obvodů se dozvíš, jak se stejný
LevelSpecaplikuje na bosonový obvod. -
Přečti si Návrhové principy reprezentací operátorů, kde se dozvíš, jak
BosonOperatorukládá a řadí své členy, včetně kontrakčního pravidla za výše uvedeným příkladem normálního uspořádání. -
Přečti si Rotace bosonových módů, kde najdeš gate, který přesouvá bosony mezi módy, což je místo, kde má varování o dosažitelném obsazení největší význam.
-
Nahlédni do API reference
qiskit_bosons.mappersaqiskit_bosons.mappers.library.
[1]
R. Somma, G. Ortiz, E. Knill, and J. Gubernatis, Quantum Simulations of Physics Problems (2003), arXiv:quant-ph/0304063, Sec. 4, Eq. 22.
N. P. D. Sawaya, T. Menke, T. H. Kyaw, S. Johri, A. Aspuru-Guzik, and G. G. Guerreschi, Resource-efficient digital quantum simulation of d-level systems for photonic, vibrational, and spin-s Hamiltonians, npj Quantum Information 6, 49 (2020), arXiv:1909.12847.