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

Principii de proiectare a reprezentărilor operatorilor

Acest ghid explică cum este organizat un BosonOperator și cum să lucrezi cu el: cum să construiești unul, cum să inspectezi și să combini operatori și cum să aduci doi operatori într-o formă în care pot fi comparați. Începe de aici când scrii un hamiltonian.

Un BosonOperator este o sumă exactă de produse de operatori bosonici de creare și de anihilare, cu coeficienți complecși. Nu poartă nicio limită de ocupare. Un mod bosonic poate conține oricâți bosoni, deci spațiul său Fock este infinit-dimensional, iar o trunchiere intervine abia când operatorul este mapat pe qubiți (vezi Trunchiază spațiul Fock). Tot ce se întâmplă în acest ghid are loc, așadar, în algebra exactă a relațiilor canonice de comutare,

[bi,bj†]=δij,[bi,bj]=[bi†,bj†]=0 .[b_i, b_j^\dagger] = \delta_{ij}, \qquad [b_i, b_j] = [b_i^\dagger, b_j^\dagger] = 0 \, .

Două consecințe modelează interfața. Un indice de mod poate apărea de mai multe ori într-un singur termen, deoarece bi†bi†b_i^\dagger b_i^\dagger este un operator valid care creează doi bosoni. De asemenea, reordonarea a doi factori nu introduce niciodată un semn, deși mutarea unui operator de anihilare peste un operator de creare pe același mod adaugă un termen, bibi†=bi†bi+1b_i b_i^\dagger = b_i^\dagger b_i + 1.

Stocare​

Un operator își păstrează termenii în patru tablouri plate, similare formatului compressed sparse row al unei matrice rare:

coeffsUn coeficient complex pe termen.
actionsUn boolean pe factor: True pentru un operator de creare și False pentru unul de anihilare.
modesUn indice de mod pe factor.
boundariesPozițiile din actions și modes unde începe și se termină fiecare termen.

Constructorul primește direct aceste patru tablouri. Operatorul de mai jos are trei termeni, anume 2b0†b12 b_0^\dagger b_1, −ib1†b1†-i b_1^\dagger b_1^\dagger și constanta 0.50.5, iar tablourile pot fi citite înapoi cu metodele get_*:

>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> op = BosonOperator(
... coeffs=[2.0, -1.0j, 0.5],
... actions=[True, False, True, True],
... modes=[0, 1, 1, 1],
... boundaries=[0, 2, 4, 4],
... )
>>> op.get_coeffs()
[(2+0j), (-0-1j), (0.5+0j)]
>>> op.get_actions()
[True, False, True, True]
>>> op.get_modes()
[0, 1, 1, 1]
>>> op.get_boundaries()
[0, 2, 4, 4]
>>> len(op)
3

Primul termen cuprinde pozițiile 0 până la 2 din actions și modes, al doilea de la 2 la 4, iar al treilea de la 4 la 4, așa se stochează termenul constant, un termen fără factori. Modul repetat 1 din al doilea termen este păstrat așa cum a fost scris; un termen nu este niciodată eliminat sau îmbinat doar pentru că un indice de mod apare de două ori.

Notă

Un operator construit cu from_dict() nu își stochează termenii în ordinea dicționarului, deci tablourile sale pot lista termenii într-o ordine diferită de la o rulare la alta. Operatorul în sine este același, iar nimic din cele de mai jos care compară sau afișează operatori nu depinde de ordinea stocată.

Aranjamentul face construcția și aritmetica ieftine, cu o consecință: un operator poate conține același termen de mai multe ori simultan. Sumele și produsele adaugă termeni și nu îi îmbină, așa că apelează simplify() când ai nevoie de termeni unici (vezi Efectuează operații aritmetice).

Construiește operatori​

Cea mai directă cale de a construi un operator este from_dict(), care primește un dicționar de la un tuplu de factori la un coeficient. Funcțiile cre() și ann() creează factorii, iar tuplul gol este termenul constant:

>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dag = BosonOperator.from_dict({(cre(0),): 1.0})
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> print(format(number))
1.000000e0 +0.000000e0j * (+0 -0)

În forma tipărită, +0 reprezintă b0†b_0^\dagger și -0 reprezintă b0b_0. Constructorii zero() și one() returnează elementul neutru aditiv și pe cel multiplicativ, iar from_terms() reconstruiește un operator din iteratorul lui iter_terms().

Biblioteca de operatori oferă cuadraturile adimensionale de poziție și impuls,

q^=b†+b2,p^=i(b†−b)2 ,\hat{q} = \frac{b^\dagger + b}{\sqrt{2}}, \qquad \hat{p} = \frac{i \left(b^\dagger - b\right)}{\sqrt{2}} \, ,

ca position() și momentum(). Împreună cu funcțiile de comutator din același modul, poți verifica direct algebra:

>>> from qiskit_bosons.operators.library import commutator, momentum, position
>>> q, p = position(0), momentum(0)
>>> q.is_hermitian() and p.is_hermitian()
True
>>> commutator(q, p).equiv(1j * BosonOperator.one())
True
>>> commutator(b, b_dag).equiv(BosonOperator.one())
True

Modulul oferă și anti_commutator() și double_commutator(). Toate trei primesc operatori de același tip și returnează un operator nou.

Atenție la ordinea într-un produs​

Doi operatori pot fi înmulțiți în două moduri, iar acestea se confundă ușor. Operatorul @ este produsul matematic, deci a @ b este ABAB. Operatorul & este compunerea, care urmează convenția Qiskit de citire de la stânga la dreapta: a & b aplică mai întâi a și apoi b, deci este BABA. Funcțiile de comutator folosesc @, deci commutator(a, b) este [A,B][A, B]. Dacă construiești singur paranteza cu &, o negi fără avertisment:

>>> (b @ b_dag).equiv(b_dag @ b + BosonOperator.one())
True
>>> (b & b_dag).equiv(b_dag @ b)
True
>>> ((b @ b_dag) - (b_dag @ b)).equiv(BosonOperator.one())
True
>>> ((b & b_dag) - (b_dag & b)).equiv(-BosonOperator.one())
True

Inspectează un operator​

Un operator nu poate fi iterat direct, deoarece termenii lui se află în tablourile plate de mai sus. Folosește în schimb iter_terms(), care furnizează pentru fiecare termen o listă de perechi (is_creation, mode) și un coeficient. Iteratorul returnează copii, deci modificarea lor nu schimbă operatorul.

>>> hopping = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(2), cre(2), ann(2), ann(2)): 0.5,
... }
... )
>>> len(hopping)
3
>>> sorted(hopping.iter_terms())
[([(True, 0), (False, 1)], (-1+0j)), ([(True, 1), (False, 0)], (-1+0j)), ([(True, 2), (True, 2), (False, 2), (False, 2)], (0.5+0j))]

Mai multe metode rezumă structura unui operator:

>>> hopping.get_support()
{0, 1, 2}
>>> hopping.min_num_modes()
3
>>> hopping.max_rank()
4

get_support() este mulțimea modurilor pe care acționează operatorul, min_num_modes() este cel mai mic registru care îl poate conține, iar max_rank() este lungimea celui mai lung termen al său.

Notă

Un mod este un indice abstract care etichetează un grad de libertate bosonic. Nu are o semnificație proprie: tu stabilești ce situs al unei rețele, ce coordonată vibrațională sau ce port optic reprezintă un mod. BosonicCircuit folosește aceiași indici (vezi Lucrează cu circuite bosonice), deci un operator și porțile care îl poartă sunt de acord asupra a ce este modul 2, fără ca vreunul să știe ce reprezintă. Nimic din operator nu fixează câte niveluri are un mod, motiv pentru care dimensiunea registrului este doar o limită inferioară; min_num_modes() este cel mai mic registru care conține operatorul, nu o proprietate a lui.

Metodele de ocupare raportează câți bosoni poate plasa un singur termen într-un mod. Aceasta este o limită inferioară pentru limita de care are nevoie o mapare. Nu este o limită suficientă, deoarece aplicările repetate ale operatorului, ca într-o evoluție în timp, pot popula niveluri mai înalte (vezi Trunchiază spațiul Fock):

>>> hopping.mode_occupations()
[1, 1, 2]
>>> hopping.mode_occupations([2, 0])
[2, 1]
>>> hopping.max_occupation()
2

Alte două verificări examinează mărimi conservate. Un operator conservă numărul de particule când fiecare termen are tot atâția operatori de creare câți de anihilare. Transmiterea unei liste de dimensiuni de bloc testează aceeași condiție în fiecare bloc de moduri consecutive:

>>> hopping.conserves_particle_number()
True
>>> move = BosonOperator.from_dict({(cre(0), ann(2)): 1.0})
>>> move.conserves_sector([3])
True
>>> move.conserves_sector([2, 1])
False

În fine, relabel_modes() redenumește modurile: modul i devine permutation[i]. Lista trebuie să fie suficient de lungă pentru a indexa fiecare mod al operatorului și nu trebuie să repete nicio intrare:

>>> print(format(move.relabel_modes([5, 6, 7])))
1.000000e0 +0.000000e0j * (+5 -7)

Efectuează operații aritmetice​

Operatorii suportă adunarea, scăderea, negarea, precum și înmulțirea și împărțirea cu un număr, în forma obișnuită a operatorilor sau pe loc:

>>> total = 2 * b_dag + b_dag - 3j * b
>>> len(total)
3
>>> len(total.simplify())
2
>>> total.simplify().equiv(3 * b_dag - 3j * b)
True

Cei trei termeni din total nu au fost niciodată îmbinați, deci operatorul a conținut b_dag de două ori până când simplify() i-a combinat. Metoda însumează coeficienții termenilor identici și elimină termenii a căror magnitudine totală este sub o toleranță. Este o operație structurală: compară factorii scriși și nu aplică relațiile de comutare. Metoda înrudită ichop() elimină pe loc coeficienții mici fără a îmbina mai întâi, așa că apelează simplify() înainte de ichop() când un operator poate conține termeni duplicați.

Produsele folosesc @ sau &, așa cum s-a descris mai sus, iar ** ridică un operator la o putere întreagă:

>>> x = b_dag + b
>>> print(format((x ** 2).simplify()))
1.000000e0 +0.000000e0j * (-0 -0)
1.000000e0 +0.000000e0j * (-0 +0)
1.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0)

Rezultatul listează cele patru ordonări așa cum au fost scrise. Reducerea lor la o singură formă necesită relațiile de comutare, pe care secțiunea următoare le aplică.

Adjunctul inversează ordinea factorilor din fiecare termen, schimbă între ele operatorii de creare și de anihilare și conjugă coeficienții. Metoda is_hermitian() testează dacă un operator este egal cu adjunctul său:

>>> hop = BosonOperator.from_dict({(cre(0), ann(1)): 1j})
>>> print(format(hop.adjoint()))
0.000000e0 -1.000000e0j * (+1 -0)
>>> hop.is_hermitian()
False
>>> (hop + hop.adjoint()).is_hermitian()
True

Un singur termen de salt nu este hermitian, ceea ce contează când grupezi termeni pentru o evoluție în timp: fiecare grup trebuie să fie hermitian de unul singur (vezi Grupează termenii operatorului: folosește structura operatorului).

Ordinea normală și compararea operatorilor​

Două produse de operatori de scară pot fi același operator și totuși să arate diferit, deoarece b b†b\,b^\dagger și b†b+1b^\dagger b + 1 sunt egale în algebră. Operatorii egali în acest sens nu trebuie neapărat stocați la fel, deci compararea lor necesită relațiile de comutare. Metoda normal_ordered() le aplică: mută fiecare operator de creare în stânga fiecărui operator de anihilare și adaugă un termen ori de câte ori o pereche pe același mod este schimbată.

>>> product = BosonOperator.from_dict({(ann(0), cre(0), ann(0), cre(0)): 1.0})
>>> ordered = product.normal_ordered()
>>> len(ordered)
5
>>> print(format(ordered.simplify()))
1.000000e0 +0.000000e0j * ()
3.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0 -0 -0)

Operatorul ordonat este returnat ca sumă neîmbinată, cu cinci termeni stocați care se reduc la trei după ce simplify() îi îmbină. Ordonarea normală nu face niciodată ca un termen să dispară: b†b†b^\dagger b^\dagger și orice alt factor repetat rămân.

Argumentul sandwich stabilește cum sunt aranjate modurile din interiorul celor două grupuri de factori. Implicit, ambele grupuri sunt sortate descrescător. Cu sandwich=True, operatorii de creare sunt crescători, iar cei de anihilare descrescători, ceea ce plasează indicii mai mari spre mijloc:

>>> pair = BosonOperator.from_dict({(cre(0), cre(1), ann(0), ann(1)): 1.0})
>>> list(pair.normal_ordered().iter_terms())
[([(True, 1), (True, 0), (False, 1), (False, 0)], (1+0j))]
>>> list(pair.normal_ordered(sandwich=True).iter_terms())
[([(True, 0), (True, 1), (False, 1), (False, 0)], (1+0j))]

Operatorul == testează egalitatea exactă a termenilor stocați, fără toleranță și fără simplificare, deci poate raporta o diferență între doi operatori egali matematic. Metoda equiv() aduce mai întâi diferența celor doi operatori la ordine normală și apoi testează dacă fiecare coeficient este sub o toleranță. Este comparația pe care o vrei aproape întotdeauna:

>>> reordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0, (): 1.0})
>>> contracted = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> contracted == reordered
False
>>> contracted.simplify() == reordered.simplify()
False
>>> contracted.equiv(reordered)
True

Metoda simplify singură lasă cele două forme separate, deoarece îmbină doar termeni scriși identic. De asemenea, is_hermitian() compară un operator cu adjunctul său în ordine normală, deci recunoaște b b†+b†bb\,b^\dagger + b^\dagger b ca hermitian fără alt efort.

Notă

Ordonarea normală este o identitate exactă în algebra netrunchiată, iar equiv se bazează pe aceasta. Nu este o identitate după o trunchiere. Un operator mapat depinde de forma pe care o mapezi, deci imaginile mapate ale lui contracted și reordered de mai sus diferă, doar pe cel mai înalt nivel păstrat. Vezi Trunchiază spațiul Fock pentru mărimea diferenței și cum să alegi o formă.

Ordonează și filtrează termeni​

Reordonarea termenilor unui operator, spre deosebire de factorii din interiorul unui termen, nu schimbă operatorul, dar poate schimba comportamentul algoritmilor care citesc termenii în ordine, precum o formulă de produs randomizată. Funcțiile din qiskit_bosons.operators.terms returnează un operator nou și păstrează indicii de grup atașați termenilor lor.

canonical_order() sortează termenii într-o ordine fixă care depinde doar de factorii scriși, deci un operator se citește la fel indiferent cum a fost asamblat. order_terms() sortează după o cheie pe care o furnizezi tu, care primește fiecare termen ca tuplul furnizat de iter_terms():

>>> from qiskit_bosons.operators.terms import canonical_order, order_terms
>>> mixed = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> list(canonical_order(mixed).iter_terms())
[([], (2+0j)), ([(True, 0), (False, 1)], (3+0j)), ([(True, 1), (False, 0)], (1+0j))]
>>> by_weight = order_terms(mixed, key=lambda term: abs(term[1]), reverse=True)
>>> [coeff for _, coeff in by_weight.iter_terms()]
[(3+0j), (2+0j), (1+0j)]

Dacă un operator poartă indici de grup (vezi Grupează termenii operatorului: folosește structura operatorului), funcția group_order() face din fiecare grup o succesiune contiguă de termeni:

>>> from qiskit_bosons.operators.terms import group_order
>>> grouped = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> grouped.groups = [1, 0, 1]
>>> group_order(grouped).groups
[0, 1, 1]

Ultimul helper elimină termeni în loc să îi reordoneze. Un termen care este produs de operatori de număr, bi†bib_i^\dagger b_i, doar înmulțește fiecare stare Fock cu o fază, deci filter_diagonal_terms() poate elimina constanta, operatorii de număr și produsele lor atunci când acea fază este irelevantă pentru ce calculezi. Modifică operatorul pe loc și necesită un operator în ordine normală, pe care nu o verifică:

>>> from qiskit_bosons.operators.terms import filter_diagonal_terms
>>> model = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(0), cre(0), ann(0), ann(0)): 0.5,
... (cre(0), ann(0)): 0.2,
... (): 0.1,
... }
... )
>>> filter_diagonal_terms(model)
>>> len(model)
2
>>> model.is_hermitian()
True

Rămân doar cei doi termeni de salt. Interacțiunea on-site și operatorul de număr sunt diagonali în baza de ocupare, deci sunt eliminați împreună cu constanta. Modul repetat din interacțiune este tratat corect: b0†b0†b0b0b_0^\dagger b_0^\dagger b_0 b_0 este diagonal, în timp ce b0†b0†b0b_0^\dagger b_0^\dagger b_0 nu ar fi.

Pașii următori​