Mga prinsipyo ng disenyo ng operator representations
Ipinaliliwanag ng gabay na ito kung paano inaayos ang isang BosonOperator at kung paano ito gamitin: paano bumuo ng isa, paano mag-inspect at magsama ng mga operator, at paano dalhin ang dalawang operator sa anyong maaaring paghambingin. Magsimula rito kapag isinusulat mo ang isang Hamiltonian.
Ang BosonOperator ay isang eksaktong sum ng mga product ng bosonic creation at annihilation operator na may complex na coefficient. Wala itong occupation cutoff. Ang isang bosonic mode ay kayang maglaman ng arbitraryong dami ng boson, kaya infinite-dimensional ang Fock space nito, at pumapasok lang ang truncation kapag ang operator ay ma-map sa mga qubit (tingnan ang Truncate the Fock space). Kaya lahat ng nasa gabay na ito ay nangyayari sa eksaktong algebra ng canonical commutation relations,
Dalawang kahihinatnan ang humuhubog sa interface. Maaaring umulit ang isang mode index sa loob ng iisang term, dahil ang ay isang valid na operator na lumilikha ng dalawang boson. Gayundin, ang pagpapalit ng ayos ng dalawang factor ay hindi kailanman nagdadala ng sign, bagaman ang paglipat ng annihilation operator lampas sa creation operator sa parehong mode ay nagdaragdag ng isang term, .
Imbakan
Itinatago ng isang operator ang mga term nito sa apat na flat array, katulad ng compressed sparse row format ng isang sparse matrix:
coeffs | Isang complex na coefficient bawat term. |
actions | Isang boolean bawat factor: True para sa creation at False para sa annihilation operator. |
modes | Isang mode index bawat factor. |
boundaries | Ang mga posisyon sa actions at modes kung saan nagsisimula at nagtatapos ang bawat term. |
Direktang tumatanggap ang constructor ng apat na array na ito. Ang operator sa ibaba ay may tatlong term, ang , , at ang constant na , at mababasa mo pabalik ang mga array gamit ang mga get_* method:
>>> 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
Ang unang term ay sumasaklaw sa posisyon 0 hanggang 2 ng actions at modes, ang pangalawa ay 2 hanggang 4, at ang pangatlo ay 4 hanggang 4, na siyang paraan ng pag-imbak ng constant term, isang term na walang factor. Ang inulit na mode 1 sa pangalawang term ay pinananatili ayon sa pagkakasulat; hindi kailanman tinatanggal o pinagsasama ang isang term dahil dalawang beses lumabas ang isang mode index.
Ang operator na binuo gamit ang from_dict() ay hindi iniimbak ang mga term nito ayon sa ayos ng dictionary, kaya maaaring iba-iba ang ayos ng mga term sa mga array nito sa bawat takbo. Pareho pa rin ang operator mismo, at walang nasa ibaba na naghahambing o nagpi-print ng mga operator ang umaasa sa naka-imbak na ayos.
Ginagawa ng layout na mura ang construction at arithmetic, na may isang kahihinatnan: maaaring hawakan ng isang operator ang parehong term nang maraming beses nang sabay. Ang mga sum at product ay nagdaragdag ng mga term at hindi pinagsasama ang mga ito, kaya tawagin ang simplify() kapag kailangan mo ng mga unique na term (tingnan ang Do arithmetic).
Bumuo ng mga operator
Ang pinakadirektang paraan para bumuo ng operator ay ang from_dict(), na tumatanggap ng dictionary mula sa tuple ng mga factor papunta sa coefficient. Ang mga function na cre() at ann() ang lumilikha ng mga factor, at ang walang laman na tuple ang constant term:
>>> 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)
Sa naka-print na anyo, ang +0 ay kumakatawan sa at ang -0 ay sa . Ang mga constructor na zero() at one() ay nagbabalik ng additive at multiplicative identity, at ang from_terms() ay muling bumubuo ng operator mula sa iterator ng iter_terms().
Ang operator library ay nagbibigay ng dimensionless na position at momentum quadrature,
bilang position() at momentum(). Kasama ang mga commutator function ng parehong module, maaari mong direktang suriin ang 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
Nagbibigay din ang module ng anti_commutator() at double_commutator(). Lahat ng tatlo ay tumatanggap ng mga operator na magkapareho ang uri at nagbabalik ng bagong operator.
Bantayan ang ayos ng isang product
Maaaring i-multiply ang dalawang operator sa dalawang paraan, at madaling malito ang mga ito. Ang @ operator ay ang mathematical product, kaya ang a @ b ay . Ang & operator ay composition, na sumusunod sa convention ng Qiskit na pagbasa mula kaliwa pakanan: ang a & b ay unang ina-apply ang a at pagkatapos ang b, kaya ito ay . Ang mga commutator function ay gumagamit ng @, kaya ang commutator(a, b) ay . Kapag ikaw mismo ang bumuo ng bracket gamit ang &, tahimik nitong binabaligtad ang sign:
>>> (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
Suriin ang isang operator
Hindi direktang iterable ang isang operator, dahil ang mga term nito ay nasa mga flat array sa itaas. Gamitin na lang ang iter_terms(), na nagbibigay ng listahan ng mga pares na (is_creation, mode) at isang coefficient para sa bawat term. Nagbabalik ang iterator ng mga kopya, kaya hindi nababago ang operator kapag binago ang mga ito.
>>> 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))]
Ilang method ang nagbubuod ng istruktura ng isang operator:
>>> hopping.get_support()
{0, 1, 2}
>>> hopping.min_num_modes()
3
>>> hopping.max_rank()
4
Ang get_support() ay ang set ng mga mode na pinagkikilusan ng operator, ang min_num_modes() ay ang pinakamaliit na register na kayang maglaman nito, at ang max_rank() ay ang haba ng pinakamahabang term nito.
Ang mode ay isang abstract na index na nagla-label ng isang bosonic degree of freedom. Wala itong sariling kahulugan: ikaw ang tumutukoy kung aling site ng lattice, aling vibrational coordinate o aling optical port ang kinakatawan ng isang mode. Ginagamit ng BosonicCircuit ang parehong mga index (tingnan ang Work with bosonic circuits), kaya nagkakasundo ang operator at ang mga gate na may dala nito kung ano ang mode 2 nang hindi alam ng alinman kung ano ang kinakatawan nito. Walang nakatakda sa operator kung ilang level ang isang mode, kaya rin ang laki ng register ay lower bound lang; ang min_num_modes() ay ang pinakamaliit na register na naglalaman ng operator, hindi isang katangian nito.
Iniuulat ng mga occupation method kung ilang boson ang kayang ilagay ng isang term sa isang mode. Ito ay lower bound sa cutoff na kailangan ng isang mapping. Hindi ito sapat na cutoff, dahil ang paulit-ulit na paglalapat ng operator, tulad sa isang time evolution, ay maaaring magpuno ng mas matataas na level (tingnan ang Truncate the Fock space):
>>> hopping.mode_occupations()
[1, 1, 2]
>>> hopping.mode_occupations([2, 0])
[2, 1]
>>> hopping.max_occupation()
2
Dalawa pang pagsusuri ang tumitingin sa mga conserved na dami. Kino-conserve ng isang operator ang particle number kapag bawat term ay may kasing-daming creation at annihilation operator. Ang pagpasa ng listahan ng mga block size ay sumusubok sa parehong kondisyon sa loob ng bawat block ng magkakasunod na mode:
>>> 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
Sa wakas, ang relabel_modes() ay nagpapalit ng pangalan ng mga mode: ang mode i ay nagiging permutation[i]. Dapat sapat ang haba ng listahan para ma-index ang bawat mode ng operator at hindi dapat umulit ang isang entry:
>>> print(format(move.relabel_modes([5, 6, 7])))
1.000000e0 +0.000000e0j * (+5 -7)
Mag-arithmetic
Sinusuportahan ng mga operator ang addition, subtraction, negation, at multiplication at division sa isang numero, sa karaniwang anyo ng operator o in place:
>>> total = 2 * b_dag + b_dag - 3j * b
>>> len(total)
3
>>> len(total.simplify())
2
>>> total.simplify().equiv(3 * b_dag - 3j * b)
True
Hindi kailanman pinagsama ang tatlong term ng total, kaya hawak ng operator ang b_dag nang dalawang beses hanggang pinagsama ito ng simplify(). Sinusuma ng method ang mga coefficient ng magkaparehong term at tinatanggal ang mga may kabuuang magnitude na mas mababa sa tolerance. Isa itong structural na operasyon: inihahambing nito ang mga nakasulat na factor at hindi naglalapat ng commutation relations. Ang kaugnay na method na ichop() ay nag-aalis ng maliliit na coefficient in place nang hindi muna nagsasama, kaya tawagin ang simplify() bago ang ichop() kapag maaaring may duplicate na term ang isang operator.
Gumagamit ang mga product ng @ o & gaya ng inilarawan sa itaas, at ang ** ay nagtataas ng operator sa integer na power:
>>> 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)
Inililista ng resulta ang apat na ayos ayon sa pagkakasulat. Ang pagbawas ng mga ito sa iisang anyo ay nangangailangan ng commutation relations, na ilalapat ng susunod na seksyon.
Binabaligtad ng adjoint ang ayos ng mga factor sa bawat term, pinapalitan ang creation at annihilation operator, at kino-conjugate ang mga coefficient. Sinusubok ng method na is_hermitian() kung ang isang operator ay katumbas ng adjoint nito:
>>> 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
Ang nag-iisang hopping term ay hindi Hermitian, na mahalaga kapag nagpapangkat ka ng mga term para sa time evolution: bawat grupo ay dapat Hermitian sa sarili nito (tingnan ang Mag-group ng mga operator term: gamitin ang estruktura ng operator).
I-normal order at ihambing ang mga operator
Ang dalawang product ng ladder operator ay maaaring iisang operator pero magmukhang magkaiba, dahil ang at ay magkapantay sa algebra. Ang mga operator na magkapantay sa ganitong kahulugan ay hindi kailangang iimbak nang magkapareho, kaya kailangan ng commutation relations para maihambing ang mga ito. Inilalapat ito ng method na normal_ordered(): inililipat nito ang bawat creation operator sa kaliwa ng bawat annihilation operator at nagdaragdag ng term tuwing may pares sa parehong mode na pinagpalit.
>>> 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)
Ibinabalik ang na-order na operator bilang hindi pinagsamang sum, na may limang naka-imbak na term na nagiging tatlo pagkatapos itong pagsamahin ng simplify(). Hindi kailanman nawawala ang isang term dahil sa normal ordering: ang at bawat ibang inulit na factor ay nananatili.
Itinatakda ng argument na sandwich kung paano inaayos ang mga mode sa loob ng dalawang grupo ng mga factor. Bilang default, parehong naka-sort nang pababa ang dalawang grupo. Sa sandwich=True, pataas ang mga creation operator at pababa ang mga annihilation operator, na naglalagay ng mas malalaking index papunta sa gitna:
>>> 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))]
Sinusubok ng == operator ang eksaktong pagkakapantay ng mga naka-imbak na term, nang walang tolerance at walang simplification, kaya maaari itong mag-ulat ng pagkakaiba sa pagitan ng dalawang operator na magkapantay sa matematika. Ang method na equiv() ay unang dinadala ang pagkakaiba ng dalawang operator sa normal order at saka sinusubok kung bawat coefficient ay mas mababa sa tolerance. Ito ang paghahambing na halos palagi mong gusto:
>>> 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
Ang method na simplify lang ay iniiwang magkahiwalay ang dalawang anyo, dahil nagsasama lang ito ng magkaparehong nakasulat na term. Gayundin, inihahambing ng is_hermitian() ang isang operator sa adjoint nito sa normal order, kaya nakikilala nito ang bilang Hermitian nang walang karagdagang gawain.
Ang normal ordering ay isang eksaktong identity sa untruncated na algebra, at umaasa rito ang equiv. Hindi ito identity pagkatapos ng truncation. Ang isang na-map na operator ay nakadepende sa anyong imamapa mo, kaya magkaiba ang mga na-map na imahe ng contracted at reordered sa itaas, sa pinakamataas na retained level lang. Tingnan ang Truncate the Fock space para sa laki ng pagkakaiba at kung paano pumili ng anyo.
Ayusin at i-filter ang mga term
Ang pag-reorder ng mga term ng isang operator, kaiba sa mga factor sa loob ng isang term, ay hindi nagbabago sa operator pero maaaring magbago sa asal ng mga algorithm na nagbabasa ng mga term nang sunud-sunod, tulad ng randomized na product formula. Ang mga function ng qiskit_bosons.operators.terms ay nagbabalik ng bagong operator at pinananatiling nakakabit ang anumang group index sa mga term nito.
Isinasaayos ng canonical_order() ang mga term sa nakapirming ayos na nakadepende lang sa mga nakasulat na factor, kaya pareho ang basa ng isang operator gaano man ito binuo. Nagso-sort ang order_terms() ayon sa key na ibinibigay mo, na tumatanggap ng bawat term bilang tuple na ibinibigay ng 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)]
Kung may dalang group index ang isang operator (tingnan ang Mag-group ng mga operator term: gamitin ang estruktura ng operator), ginagawa ng function na group_order() na isang tuloy-tuloy na hanay ng mga term ang bawat grupo:
>>> 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]
Ang huling helper ay nag-aalis ng mga term sa halip na mag-reorder. Ang isang term na product ng mga number operator, , ay nagmu-multiply lang ng phase sa bawat Fock state, kaya maaaring alisin ng filter_diagonal_terms() ang constant, ang mga number operator, at ang mga product nila kapag hindi mahalaga ang phase na iyon sa kinukwenta mo. Binabago nito ang operator in place at nangangailangan ng normal-ordered na operator, na hindi nito sinusuri:
>>> 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
Ang dalawang hopping term lang ang natitira. Ang on-site interaction at ang number operator ay diagonal sa occupation basis, kaya inalis ang mga ito kasama ng constant. Tama ang paghawak sa inulit na mode sa interaction: ang ay diagonal, samantalang ang ay hindi.
Susunod na hakbang
-
Ipinapakita ng Mag-group ng mga operator term: gamitin ang estruktura ng operator kung paano i-tag ang mga term gamit ang group index.
-
Ginagawang qubit operator ng Map operator representations ang isang operator, kasama ang pagpili ng encoding at ang epekto ng truncation.
-
Inilalagay ng Work with bosonic circuits ang isang operator sa isang circuit bilang gate na
Evolution. -
Binubuo ng Simulate Bose-Hubbard dynamics with a choice of encoding ang Hamiltonian ng isang Bose-Hubbard chain at sini-simulate ito mula simula hanggang katapusan.
-
Dino-dokumento ng mga module na
qiskit_bosons.operatorsatqiskit_bosons.operators.termsang kumpletong interface.