Sari la conținutul principal
Acest proiect este în versiune beta. Ai găsit o eroare? Deschide un issue pe GitHub. Ai o întrebare sau o idee? Întreabă în Discussions — ne-ar plăcea să aflăm părerea ta!
Cum este calitatea traducerii pe această pagină?

Mapează reprezentările operatorilor

Un mapper transformă un BosonOperator într-un operator pe qubiți, aici un SparseObservable. Operatorul în sine nu știe nimic despre qubiți: stochează operatori de scară pe moduri, iar un mod bosonic poate conține un număr arbitrar de particule. Tot ceea ce face operatorul finit (câte niveluri de ocupare păstrezi și cum sunt dispuse aceste niveluri pe qubiți) se decide în momentul în care îl mapezi.

Mapperul din bibliotecă occupation_encoding() lucrează în baza de ocupare (Fock): nivelul nn al unui mod este starea cu nn bosoni. Acceptă operatorul și un LevelSpec, care conține trunchierea și codificarea. Acest ghid explică ce înseamnă cele două alegeri și ce costă fiecare. Pentru un traseu complet care le folosește, vezi Simulează dinamica Bose-Hubbard cu o alegere de codificare; pentru modul în care transpiler-ul aplică aceleași alegeri unui circuit întreg, vezi Transpilează circuite bosonice.

Trunchiază spațiul Fock​

Un mod păstrat la d=nmax+1d = n_\text{max} + 1 niveluri, unde nmaxn_\text{max} este max_occupation-ul său, nu mai respectă relația canonică de comutare. Notând cu bˉ\bar{b} operatorul de anihilare trunchiat la acele niveluri,

[bˉ,bˉ†]=1−d ∣d−1⟩⟨d−1∣ ,[\bar{b}, \bar{b}^\dagger] = 1 - d \, \lvert d-1 \rangle\langle d-1 \rvert \, ,

deci relația este valabilă pe orice nivel, cu excepția celui mai înalt [1]. Poți vedea acest lucru mapând comutatorul celor doi operatori de scară și citindu-i diagonala pe nivelurile unui mod. Următorul ajutor construiește matricea densă a unui operator mapat pentru un singur mod, ceea ce are sens doar pentru registre foarte mici:

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

Ultima intrare este 1−d1 - d. Deoarece aceasta înseamnă că spanul lui {bˉ†,bˉ}\{\bar{b}^\dagger, \bar{b}\} nu este închis față de comutator, nicio codificare nu este un izomorfism de algebre. O codificare mapează stări, împreună cu operatorii care acționează în subspațiul trunchiat. Următoarele două consecințe contează în practică.

Limita de trunchiere trebuie să mărginească ocuparea accesibilă. Nu este suficient să mărginești ocuparea cu care pornești. Un divizor de fascicul (beamsplitter) care acționează asupra lui ∣1,1⟩\lvert 1, 1 \rangle populează ∣2,0⟩\lvert 2, 0 \rangle și ∣0,2⟩\lvert 0, 2 \rangle, deci o limită de unu produce în tăcere o stare greșită:

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

Cu o limită de unu, probabilitatea de a rămâne în ∣1,1⟩\lvert 1, 1 \rangle este unu, deoarece cele două stări pe care le-ar popula dinamica nu există. Cu o limită de doi, ea dispare, ceea ce este efectul Hong-Ou-Mandel. De aceea max_occupation este un argument obligatoriu, care nu are niciodată o valoare implicită: este o decizie de modelare și un parametru de convergență pe care ești așteptat să îl variezi.

Maparea nu comută cu ordonarea normală. Operatorii bb†b b^\dagger și b†b+1b^\dagger b + 1 sunt egali înainte de trunchiere, dar se mapează la operatori diferiți pe qubiți:

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

Cei doi coincid pe fiecare nivel, mai puțin pe cel mai înalt. Aplicat nivelului de sus, b†b^\dagger nu are unde să ducă și amplitudinea este tăiată, deci bb†b b^\dagger este zero acolo, în timp ce b†b+1b^\dagger b + 1 dă dd. Decide o dată dacă Hamiltonianul tău este definit în formă normal ordonată și mapează consecvent acea formă.

Trei metode te ajută să alegi o limită, iar toate sunt margini inferioare. Operatorul raportează cel mai mare număr de operatori de creare pe care îi aplică fiecărui mod într-un singur termen, iar un spec îl compară cu limitele sale:

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

O intrare nenulă înseamnă că limita este sub margine, iar valoarea arată cu cât. Acești ajutoare nu cunosc dinamica, deci o verificare trecută nu face o limită suficientă; divizorul de fascicul de mai sus are o margine de unu, dar necesită o limită de doi. Ele pot raporta și un deficit pentru un termen care este zero pe nivelurile păstrate, așa cum este b†b†bbb^\dagger b^\dagger b b pentru o limită de unu.

Descrie trunchierea cu un LevelSpec​

Un LevelSpec grupează limita fiecărui mod cu codificarea și derivă din ele câți qubiți ocupă fiecare mod și unde încep qubiții săi. Tot ceea ce trebuie să fie de acord în privința asta (mapperul, aranjamentul transpiler-ului, pregătirea stării) primește același spec. Codificarea este un argument obligatoriu, la fel ca limita: niciuna nu are valoare implicită.

Limita este fie un singur întreg, comun tuturor modurilor, fie un întreg pentru fiecare mod:

>>> shared = LevelSpec(2, "gray")
>>> per_mode = LevelSpec([4, 2, 2], "gray")
>>> shared.num_modes is None, per_mode.num_modes
(True, 3)

Un spec construit dintr-un singur întreg lasă deschis numărul de moduri, deci trebuie să îl rezolvi înainte de a cere lățimile. O secvență nu este niciodată difuzată, indiferent de lungime, și fixează numărul de moduri:

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

Modul mm ocupă qubiții de la offsets[m] la offsets[m] + widths[m] - 1, deci modurile cu limite diferite pot ocupa numere diferite de qubiți, iar decalajele sunt suma cumulată a lățimilor. Nu trebuie să transmiți numărul de moduri mapperului. Acesta îl preia din spec sau, pentru un spec cu o singură limită, din operator prin min_num_modes(). Registrul poate fi mai larg decât au nevoie modurile; qubiții în plus sunt inactivi și se află la final:

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

Un operator care acționează asupra unui mod aflat după ultimul dintr-un spec pe moduri este refuzat:

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

Câteva limite decurg din felul în care codificările numără qubiții. Într-o codificare compactă, un mod cu un singur nivel (max_occupation = 0) nu ocupă niciun qubit, deoarece singurul său nivel nu are nevoie de nimic pentru a fi deosebit de altul. Un astfel de mod este permis și nu mută qubiții modurilor următoare, dar un spec în care fiecare mod nu ar ocupa niciun qubit este respins. Codificările unare păstrează cel mult 128 de niveluri pe mod, iar o limită mai mare produce o eroare care sugerează o codificare compactă:

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

Alege o codificare​

Un LevelEncoding decide ce stare de bază a unui bloc de qubiți reprezintă fiecare nivel. Există patru și niciuna nu este cea mai bună în general [2]. Numărul de qubiți pe mod și tiparul de biți al fiecărui nivel decurg din codificare:

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

Tiparele de biți sunt întregi în care bitul qq este valoarea qubitului qq. Codificările unare consumă un qubit pe nivel și setează unul dintre ei. Codificările compacte consumă ⌈log⁡2d⌉\lceil \log_2 d \rceil qubiți și stochează numărul nivelului, în binar sau ca cod Gray, în care nivelurile vecine diferă printr-un bit. Numele sub formă de șir al unei codificări este acceptat oriunde este acceptată codificarea.

Compromisul este între qubiți și greutatea operatorului. Codificările compacte necesită exponențial mai puțini qubiți, dar un operator de scară conectează niveluri ale căror tipare pot diferi în mai mulți biți, deci termenii săi acționează asupra mai multor qubiți. Următoarele secțiuni analizează cât costă acest lucru, începând cu cele două codificări unare, care au dispuneri identice și diferă doar prin operator.

Compară cele două codificări unare​

În codificarea unară, un operator de scară mută o excitație între doi qubiți vecini. Pentru a-l scrie ca operator pe întregul mod, trebuie să alegi ce faci cu ceilalți qubiți. Cele două variante aleg diferit:

  • unary_projected pune un proiector ∣0⟩⟨0∣\lvert 0 \rangle\langle 0 \rvert pe fiecare alt qubit al modului. Aceasta impune constrângerea one-hot chiar în operator: termenul acționează doar dacă restul modului este gol.

  • unary_unprojected lasă acei qubiți neatinși, deci termenul acționează ca identitate asupra lor.

Privește un singur b†b^\dagger la d=4d = 4 în ambele. Mapperul returnează un SparseObservable, ai cărui termeni sunt șiruri de termeni de bit pe un singur qubit. Iată primii patru termeni din fiecare, cu qubiții asupra cărora acționează:

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

Ambele au doisprezece termeni cu aceiași coeficienți. Termenii proiectați poartă un termen de bit 0 pe cei doi qubiți pe care termenii neproiectați nu îi menționează, deci fiecare termen proiectat atinge toți cei patru qubiți, iar fiecare termen neproiectat atinge doi. În general, un termen proiectat atinge toți cei dd qubiți ai modului, deoarece poartă doi factori, fiecare XX sau YY, și un proiector pe fiecare alt qubit.

Cei doi operatori sunt egali pe stările pentru care este gândită codificarea, stările de bază one-hot (cuvintele de cod), și diferă în afara lor. Pentru a verifica, ia un operator mic pe două moduri la d=3d = 3 și compară matricile pe cele nouă cuvinte de cod:

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

Ambii lasă, de asemenea, invariant subspațiul cuvintelor de cod, deci o stare pregătită pe un cuvânt de cod rămâne pe cuvinte de cod. Alegerea contează doar pentru stările care părăsesc acel subspațiu, de exemplu din cauza zgomotului. Acolo operatorul proiectat suprimă evoluția, deoarece proiectorii săi eșuează, în timp ce cel neproiectat continuă să acționeze. Niciunul dintre comportamente nu este o cerință fizică. Operatorul neproiectat este definit doar pe subspațiul one-hot, deci alegerea lui este o decizie de modelare și nu o optimizare gratuită.

Ceea ce costă proiectorii sunt porțile. Evoluează un b†+bb^\dagger + b hermitian pe un singur mod pentru un timp scurt și numără porțile pe doi qubiți după transpilarea Qiskit:

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

Numerele exacte depind de modul în care Qiskit sintetizează poarta de evoluție, care se schimbă între versiuni, așa că trasează-le grafic:

CX count against the number of levels d for the four encodings. The unary_projected count grows fastest, the unary_unprojected count grows linearly, and the Gray code is the cheapest.

Numărul pentru varianta neproiectată crește liniar cu dd, deoarece fiecare dintre cele d−1d - 1 tranziții este o rotație pe doi qubiți. Numărul pentru varianta proiectată crește mult mai repede, deoarece fiecare termen acționează asupra tuturor celor dd qubiți. Codificările compacte necesită mai puțini qubiți, dar greutatea termenilor lor depinde de modul în care diferă tiparele de biți ale nivelurilor vecine: codul Gray, în care diferă printr-un bit, este cea mai ieftină dintre cele două codificări compacte, iar binarul standard face un salt când un transport schimbă mai mulți biți deodată (d=5d = 5). Dacă o codificare compactă o depășește și pe cea unară neproiectată depinde de dd și de modul în care Qiskit sintetizează evoluția, așa că citește tendința, nu clasamentul la o singură dimensiune.

Proiectorii contează și când expandezi un operator mapat în Pauli, ceea ce face from_sparse_observable() înlocuind fiecare proiector cu o sumă de doi operatori Pauli. Un termen cu kk proiectori devine 2k2^k termeni Pauli. Iată un singur b†b^\dagger la dd mic, singura dimensiune la care ar trebui să încerci asta:

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

Ambele variante stochează același număr de termeni, dar expansiunea celei proiectate crește exponențial în dd. Pentru orice depășește un exemplu simplu, citește direct termenii SparseObservable, așa cum face listarea de mai sus, în loc să îl convertești.

Codificările compacte nu își pot elimina proiectorii. Acolo, biții zero fac parte din numărul nivelului, deci eliminarea unui ∣0⟩⟨0∣\lvert 0 \rangle\langle 0 \rvert schimbă nivelurile pe care le conectează un termen, iar operatorul nu mai este același pe cuvintele de cod. De aceea doar codificarea unară are două variante.

Un produs de operatori de scară este trunchiat mai întâi​

O altă alegere de proiectare se vede în costul termenilor diagonali. Mapperul nu mapează fiecare operator de scară și nu înmulțește rezultatele. El adună operatorii de scară pe care un termen îi aplică unui mod, îi înmulțește ca matrice d×dd \times d în baza Fock, trunchiază acel produs o singură dată și codifică rezultatul [2]. Un produs precum operatorul număr b†bb^\dagger b rămâne astfel diagonal:

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

Termenii săi sunt proiectorii ∣1⟩⟨1∣\lvert 1 \rangle\langle 1 \rvert pe qubitul fiecărui nivel peste zero, ponderați cu numărul nivelului (nivelul zero are pondere zero și deci niciun termen). O evoluție în timp a lui este un set de rotații pe un singur qubit și nu necesită nicio poartă pe doi qubiți. În schimb, înmulțirea celor doi operatori de scară mapați dă același operator pe cuvintele de cod, dar unul diferit în afara lor, cu șiruri în afara diagonalei care costă porți:

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

Termenii diagonali, precum interacțiunea locală din modelul Bose-Hubbard, sunt astfel de produse, iar aici contează această alegere.

Scrie un mapper personalizat​

Dacă vrei o codificare pe care biblioteca nu o oferă, construiește-o din imaginea unui singur operator de scară. map_boson_action_generators() primește o funcție care mapează un singur BosonAction, o funcție care returnează identitatea, și construiește din ele mapperul operatorilor întregi. Ea înmulțește în ordine imaginile acțiunilor fiecărui termen, scalează cu coeficientul și însumează termenii. Rezultatul poate fi orice tip care acceptă înmulțirea cu scalari și compunerea.

Ca exemplu, ia codificarea pentru d=2d = 2 care stochează un mod într-un qubit. Un operator de creare este ∣1⟩⟨0∣\lvert 1 \rangle\langle 0 \rvert, adică (X−iY)/2(X - iY)/2 în convenția Qiskit, iar un operator de anihilare este adjunctul său:

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

Acest mapper scris de mână înmulțește operatorii de scară mapați și coincide cu mapperul din bibliotecă aici, deoarece fiecare mod are două niveluri și un qubit conține întregul mod. La dd mai mare, așa cum a arătat secțiunea anterioară, compunerea scărilor mapate și trunchierea mai întâi a produsului nu sunt același operator în afara cuvintelor de cod.

Pentru a folosi un astfel de mapper într-un circuit, învelește-l într-o funcție cu semnătura (operator, num_qubits) -> SparseObservable și dă-l lui MapperFnEvolutionSynthesis, care mapează cu el operatorul fiecărei porți Evolution. Pipeline-ul transpiler-ului este descris în Transpilează circuite bosonice.

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

Merită cunoscute două detalii. În primul rând, operatorul pe qubiți trebuie să aibă coeficienți reali, deoarece PauliEvolutionGate respinge o parte imaginară. Un operator hermitian se mapează la unul real, dar doar după ce termenii care se anulează au fost uniți. Acest mapper scris de mână nu îi unește, deci un termen de salt și adjunctul său lasă în urmă coeficienți imaginari până când simplifici rezultatul. Mapperul din bibliotecă își unește singur rezultatul; unul personalizat trebuie să o facă, de exemplu învelindu-l în 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']

În al doilea rând, o formulă de produs sintetizează termenii în ordinea în care îi primește, iar ordinea produsă de un mapper nu este garantat stabilă. Ajutoarele simplify() și group_wise() învelesc o funcție mapper pentru a o controla: simplify unește șirurile duplicate și sortează termenii într-o ordine canonică, în timp ce group_wise mapează câte un grup al operatorului pe rând, astfel încât termenii unui grup rămân adiacenți. Nu le combina, deoarece sortarea anulează gruparea. Ghidul de grupare arată cum se atribuie grupurile, iar ghidul de sinteză arată cum afectează ordinea circuitul.

Pașii următori​

[1]

R. Somma, G. Ortiz, E. Knill, and J. Gubernatis, Quantum Simulations of Physics Problems (2003), arXiv:quant-ph/0304063, Sec. 4, Eq. 22.

[2] (1,2)

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.