Mag-map ng mga representasyon ng operator
Ginagawang qubit operator ng isang mapper ang isang BosonOperator, dito ay isang SparseObservable. Walang alam ang operator mismo tungkol sa mga qubit: nag-iimbak ito ng mga ladder operator sa mga mode, at ang isang bosonic mode ay kayang humawak ng napakaraming particle. Lahat ng nagpapahangganan sa operator (kung ilang occupation level ang itatago mo, at kung paano ilalatag ang mga level na iyon sa mga qubit) ay napagpapasyahan kapag ima-map mo ito.
Ang library mapper na occupation_encoding() ay gumagana sa occupation (Fock) basis: ang level ng isang mode ay ang estado na may boson. Kinukuha nito ang operator at isang LevelSpec, na may hawak ng truncation at ng encoding. Ipinapaliwanag ng gabay na ito kung ano ang ibig sabihin ng dalawang pagpiling iyon at kung ano ang gastos ng bawat isa. Para sa buong daloy na gumagamit ng mga ito, tingnan ang Mag-simulate ng Bose-Hubbard dynamics na may pagpili ng encoding; para malaman kung paano inilalapat ng transpiler ang parehong mga pagpili sa buong circuit, tingnan ang Mag-transpile ng mga bosonic circuit.
I-truncate ang Fock space
Ang isang mode na itinatago sa na mga level, kung saan ang ay ang max_occupation nito, ay hindi na sumusunod sa canonical commutation relation. Kung isusulat natin ang para sa annihilation operator na na-truncate sa mga level na iyon,
kaya totoo ang relasyon sa bawat level maliban sa pinakamataas [1]. Makikita mo ito sa pamamagitan ng pag-map sa commutator ng dalawang ladder operator at pagbasa sa diagonal nito sa mga level ng isang mode. Binubuo ng sumusunod na helper ang dense matrix ng isang na-map na operator para sa isang mode, na makatwiran lang para sa napakaliliit na register:
>>> 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]
Ang huling entry ay . Dahil ang ibig sabihin nito ay hindi sarado sa ilalim ng commutator ang span ng , walang encoding na isomorphism ng mga algebra. Ang encoding ay nagma-map ng mga estado, kasama ang mga operator na kumikilos sa loob ng na-truncate na subspace. Mahalaga sa praktika ang sumusunod na dalawang konsekwensya.
Dapat sakop ng cutoff ang occupation na maaabot. Hindi sapat na sakop lang nito ang occupation na sinimulan mo. Ang beamsplitter na kumikilos sa ay nagpupuno sa at , kaya ang cutoff na isa ay tahimik na gagawa ng maling estado:
>>> 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)
Sa cutoff na isa, ang probability na manatili sa ay isa, dahil hindi umiiral ang dalawang estado na pupunuin sana ng dynamics. Sa cutoff na dalawa, nawawala ito, na siyang Hong-Ou-Mandel effect. Kaya ang max_occupation ay required na argument na walang default: isa itong desisyon sa modeling at isang convergence parameter na inaasahang i-sweep mo.
Hindi nagko-commute ang mapping sa normal ordering. Magkapantay ang mga operator na at bago ang truncation, pero iba ang qubit operator na kinalalabasan nila kapag na-map:
>>> 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]
Magkapareho ang dalawa sa bawat level maliban sa pinakamataas. Kapag inilapat sa pinakamataas na level, walang mapupuntahan ang at napuputol ang amplitude, kaya zero ang doon, samantalang ang ibinibigay ng . Magdesisyon nang minsan kung naka-define sa normal-ordered na anyo ang Hamiltonian mo, at i-map ang anyong iyon nang pare-pareho.
Tatlong paraan ang tumutulong sa iyong pumili ng cutoff, at lahat ng mga ito ay lower bound. Iniuulat ng operator ang pinakamalaking bilang ng mga creation operator na inilalapat nito sa bawat mode sa loob ng isang term, at ikinukumpara ito ng isang spec sa mga cutoff nito:
>>> 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]
Ang non-zero na entry ay nangangahulugang mas mababa ang cutoff sa bound, at ang value ay kung gaano kalayo. Walang alam ang mga helper na ito sa dynamics, kaya hindi nagiging sapat ang cutoff dahil lang pumasa ang check; ang beamsplitter sa itaas ay may bound na isa pero nangangailangan ng cutoff na dalawa. Puwede rin silang mag-ulat ng kakulangan para sa isang term na zero sa mga natirang level, gaya ng para sa cutoff na isa.
Ilarawan ang truncation gamit ang LevelSpec
Pinagsasama ng isang LevelSpec ang cutoff ng bawat mode at ang encoding, at hinuhugot nito mula sa mga iyon kung ilang qubit ang sinasakop ng bawat mode at kung saan nagsisimula ang mga qubit nito. Ang lahat ng kailangang magkasundo tungkol doon (ang mapper, ang layout ng transpiler, ang state preparation) ay binibigyan ng iisang spec. Required ang encoding, at ganoon din ang cutoff: walang default ang alinman sa dalawa.
Ang cutoff ay alinman sa isang integer na ibinabahagi ng lahat ng mode, o isang integer para sa bawat mode:
>>> shared = LevelSpec(2, "gray")
>>> per_mode = LevelSpec([4, 2, 2], "gray")
>>> shared.num_modes is None, per_mode.num_modes
(True, 3)
Ang spec na binuo mula sa iisang integer ay nag-iiwan na bukas ang bilang ng mga mode, kaya kailangan mo itong i-resolve bago humingi ng mga lapad. Hindi kailanman ibino-broadcast ang isang sequence, anuman ang haba nito, at itinatakda nito ang bilang ng mga mode:
>>> 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
Sinasakop ng mode ang mga qubit mula offsets[m] hanggang offsets[m] + widths[m] - 1, kaya ang mga mode na magkakaiba ang cutoff ay puwedeng kumuha ng magkakaibang bilang ng mga qubit, at ang mga offset ay ang running sum ng mga lapad. Hindi mo kailangang ipasa sa mapper ang bilang ng mga mode. Kinukuha nito iyon mula sa spec, o, para sa spec na may iisang cutoff, mula sa operator sa pamamagitan ng min_num_modes(). Puwedeng mas malapad ang register kaysa sa kailangan ng mga mode; ang sobrang mga qubit ay idle at nasa dulo:
>>> 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
Tinatanggihan ang operator na kumikilos sa mode na lampas sa huli ng isang per-mode na spec:
>>> 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
May ilang limitasyon na nagmumula sa kung paano nagbibilang ng mga qubit ang mga encoding. Sa ilalim ng compact encoding, ang mode na may iisang level (max_occupation = 0) ay hindi kumukuha ng kahit isang qubit, dahil walang kailangan ang nag-iisang level nito para maibukod sa iba. Pinapayagan ang ganoong mode at hindi nito ginagalaw ang mga qubit ng mga mode na kasunod nito, pero tinatanggihan ang spec kung saan walang qubit na kukunin ang bawat mode. Ang mga unary encoding ay may hawak na hanggang 128 level kada mode lang, at ang mas malaking cutoff ay nagtataas ng error na nagmumungkahi ng compact encoding:
>>> 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
Pumili ng encoding
Tinutukoy ng isang LevelEncoding kung aling basis state ng isang bloke ng mga qubit ang kumakatawan sa bawat level. Apat ang mayroon, at wala sa mga ito ang pinakamahusay sa pangkalahatan [2]. Ang bilang ng mga qubit kada mode at ang bit pattern ng bawat level ay sumusunod sa encoding:
>>> 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']
Ang mga bit pattern ay mga integer kung saan ang bit ay ang value ng qubit . Ang mga unary encoding ay gumagastos ng isang qubit kada level at isa lang ang itinatakda. Ang mga compact encoding ay gumagastos ng na mga qubit at iniimbak ang numero ng level, sa binary o bilang Gray code kung saan ang magkatabing level ay nagkakaiba sa isang bit. Tinatanggap ang string name ng isang encoding kahit saan tinatanggap ang encoding.
Ang trade-off ay mga qubit laban sa bigat ng operator. Mas kaunting qubit nang exponential ang kailangan ng mga compact encoding, pero ang ladder operator ay nag-uugnay ng mga level na ang mga pattern ay puwedeng magkaiba sa ilang bit, kaya mas maraming qubit ang kinikilusan ng mga term nito. Tinitingnan ng mga sumusunod na seksyon kung magkano ang gastos nito, simula sa dalawang unary encoding, na magkapareho ang layout at ang operator lang ang pagkakaiba.
Ihambing ang dalawang unary encoding
Sa unary encoding, inililipat ng ladder operator ang isang excitation sa pagitan ng dalawang magkatabing qubit. Para maisulat iyon bilang operator sa buong mode, kailangan mong piliin kung ano ang gagawin sa iba pang mga qubit. Magkaiba ang pinipili ng dalawang variant:
-
Naglalagay ang
unary_projectedng projector na sa bawat iba pang qubit ng mode. Ipinapatupad nito ang one-hot constraint sa mismong operator: kumikilos lang ang term kung walang laman ang natitirang bahagi ng mode. -
Hindi ginagalaw ng
unary_unprojectedang mga qubit na iyon, kaya identity ang kilos ng term sa mga ito.
Tingnan ang isang sa sa ilalim ng dalawa. Nagbabalik ang mapper ng isang SparseObservable, na ang mga term ay mga string ng single-qubit na bit term. Narito ang unang apat na term ng bawat isa, kasama ang mga qubit na kinikilusan ng mga ito:
>>> 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
Parehong may labindalawang term na may parehong mga coefficient ang dalawa. Ang mga projected na term ay may 0 na bit term sa dalawang qubit na hindi binabanggit ng mga unprojected na term, kaya ang bawat projected na term ay humahawak sa lahat ng apat na qubit at ang bawat unprojected na term ay sa dalawa. Sa pangkalahatan, ang projected na term ay humahawak sa lahat ng na qubit ng mode, dahil may dala itong dalawang factor, bawat isa ay o , at isang projector sa bawat iba pang qubit.
Magkapantay ang dalawang operator sa mga estadong nilalayon ng encoding, ang mga one-hot basis state (ang mga codeword), at nagkakaiba sa labas ng mga ito. Para masuri, kumuha ng maliit na operator sa dalawang mode sa at ikumpara ang mga matrix sa siyam na codeword:
>>> 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
Parehong pinananatiling invariant ng dalawa ang codeword subspace, kaya ang estadong inihanda sa isang codeword ay nananatili sa mga codeword. Mahalaga lang ang pagpili para sa mga estadong lumalabas sa subspace na iyon, halimbawa dahil sa ingay. Doon, pinipigilan ng projected na operator ang evolution, dahil pumapalya ang mga projector nito, samantalang patuloy na kumikilos ang unprojected. Hindi pisikal na pangangailangan ang alinman sa dalawang gawi. Ang unprojected na operator ay naka-define lang sa one-hot subspace, kaya ang pagpili nito ay desisyon sa modeling at hindi libreng optimization.
Ang gastos ng mga projector ay mga gate. I-evolve ang isang Hermitian na sa isang mode sa maikling panahon, at bilangin ang mga two-qubit gate pagkatapos ng transpilation ng 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
... }
Nakadepende ang eksaktong bilang sa kung paano sini-synthesize ng Qiskit ang evolution gate, na nagbabago sa bawat release, kaya i-plot ang mga ito:
Linear na lumalaki ang bilang ng unprojected kasabay ng , dahil ang bawat isa sa na transition ay isang two-qubit rotation. Mas mabilis na lumalaki ang bilang ng projected, dahil bawat term ay kumikilos sa lahat ng na qubit. Mas kaunting qubit ang kailangan ng mga compact encoding, pero ang bigat ng mga term nila ay nakadepende sa kung paano nagkakaiba ang mga bit pattern ng magkatabing level: ang Gray code, kung saan nagkakaiba sila sa isang bit, ang mas mura sa dalawang compact encoding, at tumatalon ang standard binary kapag maraming bit ang sabay na binabaligtad ng isang carry (). Kung natatalo rin ng compact encoding ang unprojected na unary ay nakadepende sa at sa kung paano sini-synthesize ng Qiskit ang evolution, kaya basahin ang trend at hindi ang ranggo sa isang laki.
Mahalaga rin ang mga projector kapag pinalalawak mo ang na-map na operator sa mga Pauli, na ginagawa ng from_sparse_observable() sa pamamagitan ng pagpapalit sa bawat projector ng kabuuan ng dalawang Pauli operator. Ang term na may na projector ay nagiging na Pauli term. Narito ang isang sa maliit na , na siya lang laki kung saan dapat mong subukan ito:
>>> 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
Pareho ang bilang ng mga term na iniimbak ng dalawang variant, pero exponential sa ang paglaki ng expansion ng projected. Para sa anumang mas malaki sa isang toy example, basahin nang direkta ang mga term ng SparseObservable, gaya ng ginawa ng listahan sa itaas, sa halip na i-convert ito.
Hindi maaaring alisin ng mga compact encoding ang mga projector nila. Doon, bahagi ng numero ng level ang mga zero bit, kaya ang pag-alis ng ay nagbabago sa kung aling mga level ang iniuugnay ng term, at hindi na pareho ang operator sa mga codeword. Kaya unary encoding lang ang may dalawang variant.
Ang produkto ng mga ladder ay tinutruncate muna
May isa pang pagpili sa disenyo na lumalabas sa gastos ng mga diagonal na term. Hindi ima-map ng mapper ang bawat ladder operator at imumultiply ang mga resulta. Kinokolekta nito ang mga ladder operator na inilalapat ng isang term sa isang mode, imumultiply ang mga ito bilang na matrix sa Fock basis, tinutruncate ang produktong iyon nang isang beses, at ine-encode ang resulta [2]. Kaya ang produkto tulad ng number operator na ay nananatiling 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
Ang mga term nito ay ang mga projector na sa qubit ng bawat level na mas mataas sa zero, na tinitimbang ng numero ng level (ang level zero ay may bigat na zero kaya walang term). Ang time evolution nito ay isang set ng mga single-qubit rotation at hindi nangangailangan ng two-qubit gate. Ang pagmu-multiply sa dalawang na-map na ladder operator ay nagbibigay ng parehong operator sa mga codeword, pero ibang operator sa labas ng mga ito, na may mga off-diagonal na string na may gastos na mga gate:
>>> 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
Ang mga diagonal na term tulad ng on-site interaction ng Bose-Hubbard model ay ang mga produktong ito, kaya dito mahalaga ang pagpiling ito.
Sumulat ng custom na mapper
Kung gusto mo ng encoding na hindi ibinibigay ng library, buuin ito mula sa image ng isang ladder operator. Ang map_boson_action_generators() ay kumukuha ng function na nagma-map ng isang BosonAction, isang function na nagbabalik ng identity, at binubuo mula sa mga ito ang mapper ng buong mga operator. Imumultiply nito ang mga image ng mga action ng bawat term ayon sa pagkakasunod-sunod, ini-scale sa coefficient, at isinusuma ang mga term. Ang output ay puwedeng anumang uri na sumusuporta sa scalar multiplication at composition.
Bilang halimbawa, kunin ang encoding para sa na nag-iimbak ng isang mode sa isang qubit. Ang creation operator ay , na sa convention ng Qiskit, at ang annihilation operator ay ang adjoint nito:
>>> 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
Ang hand-written na mapper na ito ay nagmu-multiply ng mga na-map na ladder operator, at sumasang-ayon ito sa library mapper dito dahil may dalawang level ang bawat mode at hawak ng isang qubit ang buong mode. Sa mas malaking , gaya ng ipinakita ng nakaraang seksyon, hindi magkapareho ang operator sa labas ng mga codeword kapag kinomposo muna ang mga na-map na ladder at kapag tinruncate muna ang produkto.
Para gamitin ang ganoong mapper sa isang circuit, balutin ito sa isang function na may signature na (operator, num_qubits) -> SparseObservable at ibigay ito sa MapperFnEvolutionSynthesis, na nagma-map gamit ito sa operator ng bawat Evolution gate. Inilalarawan ang transpiler pipeline sa Mag-transpile ng mga bosonic circuit.
>>> 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)
Dalawang detalye ang dapat malaman. Una, dapat may totoong mga coefficient ang qubit operator, dahil tinatanggihan ng PauliEvolutionGate ang imaginary na bahagi. Ang Hermitian na operator ay nagma-map sa totoo, pero kapag nai-merge na lang ang mga term na nagka-cancel. Hindi ito minemerge ng hand-written na mapper na ito, kaya ang hopping term at ang adjoint nito ay nag-iiwan ng mga imaginary na coefficient hangga't hindi mo pinapasimple ang resulta. Minemerge ng library mapper ang sarili nitong resulta; ang custom ay kailangang gawin ito, halimbawa sa pamamagitan ng pagbabalot dito gamit ang 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']
Ikalawa, sini-synthesize ng product formula ang mga term ayon sa pagkakasunod-sunod ng pagtanggap nito, at hindi garantisadong stable ang pagkakasunod-sunod na ginagawa ng mapper. Ang mga helper na simplify() at group_wise() ay bumabalot sa mapper function para kontrolin ito: ang simplify ay nagme-merge ng mga duplicate na string at inaayos ang mga term sa canonical na pagkakasunod-sunod, samantalang ang group_wise ay nagma-map ng isang group ng operator nang paisa-isa para manatiling magkakatabi ang mga term ng isang group. Huwag pagsamahin ang dalawa, dahil sinisira ng sorting ang grouping. Ipinapakita ng gabay sa grouping kung paano magtalaga ng mga group, at ipinapakita ng gabay sa synthesis kung paano naaapektuhan ng pagkakasunod-sunod ang circuit.
Mga susunod na hakbang
-
Sundan ang Mag-simulate ng Bose-Hubbard dynamics na may pagpili ng encoding para sa buong simulation na nagswi-sweep ng cutoff at naghahambing ng mga encoding laban sa eksaktong dynamics.
-
Tingnan ang Mag-transpile ng mga bosonic circuit para malaman kung paano inilalapat ang parehong
LevelSpecsa isang bosonic circuit. -
Basahin ang Mga prinsipyo ng disenyo ng mga representasyon ng operator para malaman kung paano iniimbak at inaayos ng isang
BosonOperatorang mga term nito, kasama ang contraction rule sa likod ng halimbawa ng normal-ordering sa itaas. -
Basahin ang I-rotate ang mga bosonic mode para sa gate na naglilipat ng mga boson sa pagitan ng mga mode, kung saan pinakamahalaga ang babala tungkol sa maaabot na occupation.
-
Kumonsulta sa API reference ng
qiskit_bosons.mappersatqiskit_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.