Lumaktaw sa pangunahing nilalaman
Ang proyektong ito ay nasa beta. Nakakita ng bug? Mag-bukas ng GitHub issue. May tanong o ideya? Magtanong sa Discussions — gusto naming marinig ang iyong feedback!
Gaano kaganda ang kalidad ng pagsasalin sa pahinang ito?

Mag-synthesize ng Evolution gate sa bosonic space

Ang time evolution na e−itHe^{-itH} ng isang Hamiltonian na kabuuan ng mga pirasong hindi nagko-commute ay hindi maipapatupad nang eksakto gate por gate. Ina-approximate ito ng product formula gamit ang sunod-sunod na maiikling evolution sa ilalim ng bawat pirasong iyon, at ang error ng approximation ay tinatawag na Trotter error.

Maaari kang mag-apply ng product formula sa dalawang lugar, at hindi sila magkapalit:

  1. Sa bosonic space, bago pa mapili ang anumang truncation o encoding. Nakikita ng formula ang mga group ng operator (tingnan ang Mag-group ng mga operator term: gamitin ang estruktura ng operator) at maaari nitong i-interleave ang mga ito.

  2. Sa qubit space, sa loob ng PauliEvolutionGate ng Qiskit, pagkatapos ma-map ang operator. Isang Pauli operator bawat group ang nakikita ng formula at ang mga term lang sa loob nito ang maaari nitong hatiin pa.

Ang una lang ang makakabawas ng error sa pagitan ng mga group na hindi nagko-commute, na kadalasan ang pinakamalaki. Ipinapakita ng gabay na ito kung paano ito gamitin.

Mahalaga

Opsyonal ang dalawang hakbang. Ang isang Evolution gate na hindi kailanman dinecompose sa bosonic space ay buong makakarating sa boson-to-qubit stage at doon ima-map at isi-synthesize. Walang error na mangyayari sa bosonic level, kundi ang ipinapasok lang ng qubit-level na formula.

Setup​

Isang two-site na Bose-Hubbard chain, ang pinakamaliit na halimbawa kung saan hindi nagko-commute ang mga group (tingnan ang I-simulate ang Bose-Hubbard dynamics gamit ang pagpili ng encoding para sa buong modelo). Naka-group ito sa isang Hermitian group para sa bond at isang diagonal group para sa on-site interaction:

>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp, Statevector
>>> from qiskit.transpiler import PassManager
>>> from scipy.sparse import csc_matrix
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> num_sites, max_occupation, total_time = 2, 2, 0.8
>>> bonds = [(0, 1)]
>>> terms = {}
>>> for left, right in bonds:
... terms[(cre(left), ann(right))] = -1.0
... terms[(cre(right), ann(left))] = -1.0
>>> for site in range(num_sites):
... terms[(cre(site), cre(site), ann(site), ann(site))] = 1.0
>>> hamiltonian = BosonOperator.from_dict(terms)
>>> hamiltonian.groups = [
... len(bonds)
... if len({mode for _, mode in term}) == 1
... else bonds.index(tuple(sorted({mode for _, mode in term})))
... for term, _ in hamiltonian.iter_terms()
... ]
>>> hamiltonian.num_groups()
2

Para masukat ang error, kailangan mo ng reference. Ang eksaktong evolution ng Hamiltonian gaya ng na-map sa parehong truncation ang tamang gamitin, dahil hiwalay na approximation ang truncation:

>>> spec = LevelSpec(max_occupation, "unary_projected")
>>> mapped = occupation_encoding(hamiltonian, spec).simplify()
>>> matrix = csc_matrix(SparsePauliOp.from_sparse_observable(mapped).to_matrix(sparse=True))
>>> pm = generate_preset_occupation_pass_manager(spec)
>>> pm.qubit = PassManager() # keep each group as one exact PauliEvolutionGate
>>>
>>> def build(**evolution_kwargs):
... circuit = BosonicCircuit(num_sites)
... circuit.append(InitializeModes([1, 1]), circuit.modes)
... circuit.append(
... Evolution(num_sites, hamiltonian, time=total_time, **evolution_kwargs), circuit.modes
... )
... return circuit
>>>
>>> # Under the unary encodings, occupation ``n`` of mode ``m`` sets qubit ``m * d + n``, with
>>> # ``d = max_occupation + 1`` levels per mode. That places |1, 1> on the basis state below.
>>> levels = max_occupation + 1
>>> index = sum(1 << (mode * levels + n) for mode, n in enumerate([1, 1]))
>>> initial = Statevector.from_int(index, 2**(num_sites * levels)).data
>>> exact = expm_multiply(-1j * total_time * matrix, initial)
>>> def error(circuit):
... """Distance to the exact evolution, of the circuit's product of group exponentials."""
... lowered = pm.run(circuit)
... state = Statevector.from_int(0, 2**lowered.num_qubits).evolve(lowered).data
... return float(np.max(np.abs(state - exact)))
Tandaan

Sadyang sini-simulate ng error ang na-lower na circuit nang hindi dinedecompose ang mga instance nito ng PauliEvolutionGate. Inaaplay ng Statevector ang bawat isa bilang eksaktong exponential ng group nito, kaya ang natitira na lang ay ang product ng mga group exponential na pinili ng bosonic synthesis.

Ang default​

Kapag walang synthesis argument, dinedecompose ang isang Evolution gate gamit ang BosonicLieTrotter, ang first-order na formula: isang factor bawat group, bawat isa para sa buong oras.

>>> dict(build().decompose().count_ops())
{'Evolution': 2, 'InitializeModes': 1}

Kung hindi dedecompose, ang buong gate ay ipapasa sa qubit stage at walang bosonic Trotter error sa lahat:

>>> error(build()) < 1e-6
True

Kapag dinecompose, may lumilitaw na tunay na error, dahil hindi nagko-commute ang dalawang group:

>>> error(build().decompose()) > 0.1
True

Bawasan ang error​

Magpasa ng ibang synthesis method sa gate. Ang mas mataas na order ay nagko-compose ng mga factor nang simetriko para magkansela ang mga pangunahing error term; ang mas maraming repetition ay hinahati ang oras sa mas maiikling hakbang.

>>> from qiskit_bosons.circuit.library.synthesis import BosonicLieTrotter, BosonicSuzukiTrotter
>>>
>>> for label, synthesis in [
... ("Lie-Trotter", BosonicLieTrotter()),
... ("Suzuki, order 2", BosonicSuzukiTrotter(order=2)),
... ("Suzuki, order 4", BosonicSuzukiTrotter(order=4)),
... ("Suzuki, order 4*", BosonicSuzukiTrotter(order=4, merge_adjacent=False)),
... ]:
... circuit = build(synthesis=synthesis).decompose()
... print(f"{label:16s} {circuit.count_ops()['Evolution']:3d} factors")
Lie-Trotter 2 factors
Suzuki, order 2 3 factors
Suzuki, order 4 11 factors
Suzuki, order 4* 15 factors

Bawat pagtaas ng order nang dalawa ay nagko-compose ng limang sweep ng nasa ibaba nito (ang 5(k−2)/25^{(k-2)/2} na paglaki ng recursion ni Suzuki, na inaplay sa 2n−12n - 1 factor ng order 2), na siyang bilang sa huling linya, na may marka ng bituin. Nagtatagpo ang mga sweep sa iisang factor, kaya ang magkakatabing gate ay nag-eevolve sa ilalim ng iisang operator at pinagsasama sa isang gate na may pinagsamang oras. Nakakatipid ito ng isang gate bawat seam at eksakto ito. Ipasa ang merge_adjacent=False para panatilihin ang textbook na sequence. Sa alinmang paraan, ang mas mataas na order ay nagbibigay ng mas maliit na error:

>>> lie = error(build(synthesis=BosonicLieTrotter()).decompose())
>>> second = error(build(synthesis=BosonicSuzukiTrotter(order=2)).decompose())
>>> fourth = error(build(synthesis=BosonicSuzukiTrotter(order=4)).decompose())
>>> lie > second > fourth
True

Ipinagpapalit ng mga repetition ang depth para sa accuracy sa ibang rate. Ang first-order na error ay bumababa nang linear ayon sa laki ng hakbang, at ang second-order ay quadratic:

>>> lie_errors = [error(build(synthesis=BosonicLieTrotter(reps=r)).decompose()) for r in (1, 2, 4, 8)]
>>> bool(all(a > b for a, b in zip(lie_errors, lie_errors[1:])))
True
>>> bool(lie_errors[0] / lie_errors[-1] > 6) # about 8, i.e. linear in 1 / reps
True
>>> suzuki_errors = [
... error(build(synthesis=BosonicSuzukiTrotter(order=2, reps=r)).decompose()) for r in (1, 2, 4)
... ]
>>> bool(suzuki_errors[0] / suzuki_errors[-1] > 10) # about 16, i.e. quadratic in 1 / reps
True

Nakadepende sa operator kung alin ang mas magandang kapalit. Tandaan na ang operator na may iisang group ay eksakto sa anumang order, dahil ang nag-iisang factor ay nagko-commute sa sarili nito, at ang formula ay nagiging isang gate na lang sa halip na hatiin ulit.

Sukatin ang gastos​

Ang mas maliit na error ay mas maraming factor ang gastos, at bawat factor ay may gastos na mga entangling gate kapag na-synthesize na ito ng qubit stage. Nakadepende sa encoding kung ilan ang mga iyon, kaya bilangin ang mga ito sa preset pipeline, na naka-fix ang basis at walang qubit-level na optimization (optimization_level=0), para makita ang presyo ng bawat method:

>>> basis = ["rz", "sx", "x", "cx"]
>>>
>>> def cx_count(circuit, encoding):
... spec = LevelSpec(max_occupation, encoding)
... lowered = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=basis
... ).run(circuit)
... return lowered.count_ops().get("cx", 0)
>>>
>>> methods = [
... ("Lie-Trotter", BosonicLieTrotter()),
... ("Suzuki, order 2", BosonicSuzukiTrotter(order=2)),
... ("Suzuki, order 4", BosonicSuzukiTrotter(order=4)),
... ]
>>> encodings = ("gray", "unary_unprojected")
>>> counts = {
... encoding: [
... cx_count(build(synthesis=method).decompose(), encoding) for _, method in methods
... ]
... for encoding in encodings
... }
>>> # the order buys accuracy at a price that grows with it
>>> bool(counts["gray"][0] < counts["gray"][1] < counts["gray"][2])
True

Nakadepende ang eksaktong bilang sa kung paano sini-synthesize ng Qiskit ang mga evolution gate, na nagbabago sa pagitan ng mga release, kaya plot ang ipinapakita sa halip na table:

Grouped bar chart of the CX count for Lie-Trotter and Suzuki of order 2 and 4, for the Gray and the unary encoding. The counts grow with the order, and the unary bars are higher.

Ang bawat grupo ng mga bar ay isang method: Lie-Trotter, Suzuki na order 2, at Suzuki na order 4. Ang order ay nagbibili ng accuracy sa presyong lumalaki ayon sa factor ng recursion, at mas malaki ang binabayaran ng unary encoding kaysa sa Gray code sa bawat method dahil mas maraming entangling gate ang naima-map sa bawat factor nito. Piliin ang method ayon sa accuracy na kailangan mo at ang encoding ayon sa hardware, at panatilihing magkahiwalay ang dalawang pagpili: hindi kailanman nagbabago ang bosonic circuit sa itaas.

Mag-apply ng isang method sa buong circuit​

Para itakda ang synthesis sa bawat gate, kailangan mong ipasa ang pagpili sa lahat ng bumubuo ng isa. Ang B2BSynthesis pass ay minsanang ginagawa ang pagpili para sa buong pipeline. Pinapangalanan ng config nito ang method para sa bawat uri ng gate, at pareho nitong pinipili ang method at ine-expand ang mga gate:

>>> from qiskit_bosons.transpiler import BosonicPassManager
>>> from qiskit_bosons.transpiler.passes import B2BSynthesis
>>>
>>> trotterize = BosonicPassManager([B2BSynthesis({"Evolution": BosonicSuzukiTrotter(order=2)})])
>>> dict(trotterize.run(build()).count_ops())
{'Evolution': 3, 'InitializeModes': 1}

Maaari ring pangalanan ang isang method, kasama ang mga argument para sa __init__ nito, sa halip na buuin: B2BSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}). Ang mga available na pangalan ay yaong sa B2BSynthesisPluginManager, at ang method na hindi rehistrado kahit saan ay maaaring ipasa bilang instance, gaya sa itaas.

Sa apply=False, pinipili lang ng pass ang method at iniiwan ang expansion sa punto kung saan ito kailangan, halimbawa para siyasatin muna ang circuit. Hindi kailanman ginagalaw ang isang atomic na gate, kahit i-tag man lang:

>>> selected = BosonicPassManager(
... [B2BSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}, apply=False)]
... ).run(build())
>>> selected.count_ops()["Evolution"] # selected, not yet expanded
1
>>> selected.decompose().count_ops()["Evolution"] # expanded on demand
3

Ang hindi pa na-expand na gate ay buong makakarating sa boson-to-qubit stage, kung saan ito ima-map nang hindi kinokonsulta ang synthesis nito, kaya magkapareho ang output ng lahat ng method.

Ang opsyonal na filter ay naglilimita nito sa mga gate na makikinabang. Ang mga Evolution na lahat ng group ay nagko-commute, gaya ng Hamiltonian na binubuo lang ng mga number operator, ay eksaktong sini-synthesize sa anumang order, kaya dagdag na depth lang ang idudulot ng mas mataas na order doon.

Limitahan ang pass sa mga gate na makikinabang​

Ang purong diagonal na operator ang nagpapakonkreto ng punto, dahil nagko-commute ang mga number operator sa isa't isa. Eksakto na ang mga group nito sa first order, at ang second order ay gumagastos ng mga entangling gate nang walang kapalit:

>>> # one group per term, each Hermitian; the order is fixed because order two evolves the first
>>> # group twice, so which group comes first changes the cost
>>> diagonal = BosonOperator.from_terms_with_groups(
... [([cre(0), ann(0), cre(1), ann(1)], 1.0, 0), ([cre(0), ann(0)], 0.5, 1)]
... )
>>>
>>> def diagonal_cost(synthesis):
... circuit = BosonicCircuit(num_sites)
... circuit.append(Evolution(num_sites, diagonal, time=total_time, synthesis=synthesis), circuit.modes)
... return cx_count(circuit.decompose(), "gray")
>>>
>>> bool(diagonal_cost(BosonicSuzukiTrotter(order=2)) > diagonal_cost(BosonicLieTrotter()))
True

Parehong eksakto ang dalawang circuit, kaya ang pangalawa ay gumagastos ng mas maraming entangling gate nang walang napapala. (Ang simetrikong order-two na formula ay inaaplay ang unang group nang dalawang beses sa kalahating oras bawat isa, kaya nakadepende ang gastos kung aling group ang nauuna.) Ang filter ay kumukuha ng predicate sa DAGOpNode at iniiwang hindi ginagalaw ang mga tinanggihang gate, kaya ang isang pass ay maaaring magtaas ng order sa mga gate na makikinabang at panatilihin ang iba sa first order. Tumatanggap lang ng gate ang predicate sa ibaba kapag inililipat ng operator nito ang mga boson sa pagitan ng mga mode, na siyang kaso kung saan hindi nagko-commute ang mga group:

>>> def is_diagonal(operator):
... """Whether every term only counts bosons, rather than moving them between modes."""
... return all(
... sorted(mode for created, mode in actions if created)
... == sorted(mode for created, mode in actions if not created)
... for actions, _ in operator.iter_terms()
... )
>>>
>>> def mixed_circuit():
... circuit = BosonicCircuit(num_sites)
... circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
... circuit.append(Evolution(num_sites, diagonal, time=total_time), circuit.modes)
... return circuit
>>>
>>> order_2 = BosonicSuzukiTrotter(order=2)
>>> everywhere = cx_count(
... BosonicPassManager([B2BSynthesis({"Evolution": order_2})]).run(mixed_circuit()), "gray"
... )
>>> restricted = cx_count(
... BosonicPassManager(
... [
... B2BSynthesis(
... {"Evolution": order_2},
... filter=lambda node: not is_diagonal(node.op.operator),
... )
... ]
... ).run(mixed_circuit()),
... "gray",
... )
>>> bool(everywhere > restricted) # the filter saves entangling gates
True

Kung patatakbuhin mong muli ang pass, walang magbabago: atomic ang mga factor na inilalabas nito.

Mga atomic na factor​

Ang isang Evolution na may markang atomic ay walang definition, kaya iniiwan ito ng decompose() sa kinalalagyan kahit ilang beses pa itong i-apply. Minamarkahan ng synthesis ang mga factor na inilalabas nito sa ganitong paraan, at gayundin ang BosonicQDrift para sa mga term na sine-sample nito.

Higit pa ito sa pagiging maayos. Ang paghahati pa sa Hermitian group, term por term, ay nagbubunga ng mga factor na bawat isa ay hindi Hermitian, at ang exponential ng non-Hermitian na operator ay hindi unitary:

>>> circuit = build()
>>> once = circuit.decompose()
>>> dict(circuit.decompose(reps=5).count_ops()) == dict(once.count_ops())
True
Babala

Kailangang Hermitian ang bawat factor nang mag-isa. Ang Hermitian na operator na hinati sa mga group ay maaaring magbunga ng mga non-Hermitian, gaya ng hopping term na hiwalay sa adjoint nito. Suriin gamit ang groups_are_hermitian(). Ang operator na walang group ay hinahati term por term, at ang qubit stage ay tatanggihan ang mga factor gamit ang Operator contains complex coefficients. Ang symmetrization ng even order ay bahagyang kinakansela ang error ng non-Hermitian na factor, kaya ang maling na-group na operator ay maaaring magmukhang mas maganda sa order 2 kaysa sa order 1 habang mali pa rin.

Ulitin ang maikling hakbang​

Bilang alternatibo sa circuit level, bumuo ng isang maikling Trotter step at ulitin ito. Pinapatag ng repeat() ang mga repetition sa isang circuit at iniiwang magkatabi ang mga hangganan, para makaapekto ang mga pass sa mga iyon:

>>> step = BosonicCircuit(num_sites)
>>> step.append(Evolution(num_sites, hamiltonian, time=total_time / 4), step.modes)
>>> dict(step.repeat(4).count_ops())
{'Evolution': 4}

Ipasa ang insert_barriers=True para panatilihing magkahiwalay ang mga repetition. Ang barrier ay ima-map sa bawat qubit ng mga register na hinahawakan nito, dahil kailangan ng encoding ng ilang qubit bawat mode at walang per-qubit na imahe ang mode:

>>> [inst.operation.name for inst in step.repeat(3, insert_barriers=True)._inner.data]
['Evolution', 'barrier', 'Evolution', 'barrier', 'Evolution']
Tandaan

Walang kaugnayan ang repeat sa reps ng isang synthesis method. Hinahati ng synthesis ang isang evolution sa mas marami at mas maiikling factor. Ang pag-uulit ng circuit na may isang evolution ay pinaparami lang ang kabuuang oras nito.

Pagsamahin sa qubit-level na formula​

Nagko-compose ang dalawang level, at kapag parehong nag-a-approximate, ang accuracy ng resulta ay nakadepende sa mas mahina sa dalawa. Kaunti lang ang napapala sa pagtaas ng bosonic order habang first-order pa rin ang formula sa qubit side. Tingnan ang MapperFnEvolutionSynthesis para sa formula sa qubit side. Pinapanatili nito ang pagkakasunod-sunod ng Pauli term na pinili ng mapper, kaya ang group_wise() ay maaaring maglabas ng mga term ng bawat group na magkakatabi, at ang simplify() ay maaaring mag-pin ng canonical na pagkakasunod-sunod.

Mga susunod na hakbang​