Sinteza unei porți Evolution în spațiul bosonic
Evoluția în timp a unui Hamiltonian care este o sumă de componente necomutative nu poate fi implementată exact poartă cu poartă. O formulă de produs o aproximează printr-o secvență de evoluții scurte sub componentele individuale, iar eroarea aproximării se numește eroare Trotter.
Poți aplica o formulă de produs în două locuri, iar acestea nu sunt interschimbabile:
-
În spațiul bosonic, înainte ca vreo trunchiere sau codificare să fi fost aleasă. Formula vede grupurile operatorului (vezi Gruparea termenilor operatorului: folosește structura operatorului) și le poate intercala.
-
În spațiul qubiților, în interiorul
PauliEvolutionGatedin Qiskit, după ce operatorul a fost mapat. Formula vede câte un operator Pauli pe grup și poate subdivide doar termenii din interiorul lui.
Doar prima poate reduce eroarea dintre grupurile care nu comută, care este de obicei cea dominantă. Acest ghid arată cum să o folosești.
Ambii pași sunt opționali. O poartă Evolution care nu este niciodată descompusă în spațiul bosonic ajunge întreagă la etapa boson-către-qubit și este mapată și sintetizată acolo. Aceasta nu introduce nicio eroare la nivel bosonic, ci doar pe cea introdusă de formula de la nivelul qubiților.
Configurare
Un lanț Bose-Hubbard cu două situri, cel mai mic exemplu în care grupurile nu comută (vezi Simulează dinamica Bose-Hubbard cu o codificare la alegere pentru modelul complet). Este grupat într-un grup hermitian pentru legătură și un grup diagonal pentru interacțiunea locală:
>>> 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
Pentru a măsura o eroare ai nevoie de o referință. Evoluția exactă a Hamiltonianului așa cum este mapat la aceeași trunchiere este cea potrivită, deoarece trunchierea este o aproximare separată:
>>> 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)))
error simulează în mod deliberat circuitul coborât fără a-i descompune instanțele PauliEvolutionGate. Statevector aplică fiecare instanță ca exponențiala exactă a grupului său, deci singurul lucru rămas este produsul exponențialelor de grup pe care l-a ales sinteza bosonică.
Varianta implicită
Fără argumentul synthesis, o poartă Evolution se descompune cu BosonicLieTrotter, formula de ordinul întâi: un factor pe grup, fiecare pentru întreg timpul.
>>> dict(build().decompose().count_ops())
{'Evolution': 2, 'InitializeModes': 1}
Fără descompunere, întreaga poartă este predată etapei qubiților și nu există deloc eroare Trotter bosonică:
>>> error(build()) < 1e-6
True
Descompusă, apare o eroare reală, deoarece cele două grupuri nu comută:
>>> error(build().decompose()) > 0.1
True
Reducerea erorii
Transmite porții o altă metodă de sinteză. Un ordin mai mare compune factorii simetric, astfel încât termenii dominanți ai erorii se anulează; mai multe repetări împart timpul în pași mai scurți.
>>> 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
Fiecare creștere a ordinului cu doi compune cinci treceri ale celui de dedesubt (creșterea a recursiei lui Suzuki, aplicată celor factori de ordinul 2), ceea ce reprezintă numărul din ultima linie, marcat cu o stea. Trecerile se întâlnesc pe același factor, deci porțile vecine evoluează sub un singur operator și sunt fuzionate într-o singură poartă cu timpul însumat. Aceasta economisește o poartă pe îmbinare și este exactă. Transmite merge_adjacent=False pentru a păstra secvența din manual. Oricum, un ordin mai mare aduce o eroare mai mică:
>>> 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
Repetările schimbă adâncimea pe acuratețe într-un ritm diferit. Eroarea de ordinul întâi scade liniar cu mărimea pasului, cea de ordinul doi pătratic:
>>> 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
Care schimb este mai bun depinde de operator. Reține că un operator cu un singur grup este exact la orice ordin, deoarece un factor singur comută cu el însuși, iar formula se reduce atunci la o singură poartă în loc să o împartă din nou.
Măsurarea costului
O eroare mai mică costă mai mulți factori, iar fiecare factor costă porți de încâlcire odată ce etapa qubiților l-a sintetizat. Câte, depinde de codificare, așa că numără-le prin pipeline-ul presetat, cu baza fixată și fără optimizare la nivel de qubiți (optimization_level=0), pentru a vedea prețul fiecărei metode:
>>> 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
Numărul exact depinde de felul în care Qiskit sintetizează porțile de evoluție, ceea ce se schimbă între versiuni, așa că graficul le arată în locul unui tabel:
Fiecare grup de bare este o metodă: Lie-Trotter, Suzuki de ordinul 2 și Suzuki de ordinul 4. Ordinul aduce acuratețe la un preț care crește cu factorul recursiei, iar codificarea unară plătește mai mult decât codul Gray pentru fiecare metodă, deoarece fiecare dintre factorii ei se mapează pe mai multe porți de încâlcire. Alege metoda după acuratețea de care ai nevoie și codificarea după hardware și ține cele două alegeri separate: circuitul bosonic de mai sus nu se schimbă niciodată.
Aplicarea unei metode pe un întreg circuit
Pentru a seta synthesis pe fiecare poartă, trebuie să transmiți alegerea prin tot ce construiește una. Pasul B2BSynthesis face alegerea o singură dată pentru un pipeline. config denumește metoda pentru fiecare tip de poartă, iar pasul atât selectează metoda, cât și expandează porțile:
>>> 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}
O metodă poate fi și denumită, cu argumentele pentru __init__, în loc să fie construită: B2BSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}). Numele disponibile sunt cele ale B2BSynthesisPluginManager, iar o metodă care nu este înregistrată nicăieri poate fi predată ca instanță, ca mai sus.
Cu apply=False, pasul doar selectează metoda și lasă expandarea pentru momentul în care este dorită, de exemplu pentru a inspecta mai întâi circuitul. O poartă atomică nu este niciodată atinsă, nici măcar etichetată:
>>> 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
O poartă neexpandată ajunge întreagă la etapa boson-către-qubit, unde este mapată fără ca synthesis să fie consultat, deci orice metodă produce atunci un rezultat identic.
Un filter opțional îl restrânge la porțile care beneficiază. Evoluțiile ale căror grupuri comută toate, cum ar fi un Hamiltonian format doar din operatori de număr, sunt sintetizate exact la orice ordin, deci un ordin mai mare ar adăuga acolo doar adâncime.
Restrângerea pasului la porțile care beneficiază
Un operator pur diagonal ilustrează concret ideea, deoarece operatorii de număr comută între ei. Grupurile lui sunt exacte la ordinul întâi, iar ordinul doi consumă porți de încâlcire degeaba:
>>> # 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
Ambele circuite sunt exacte, deci al doilea consumă mai multe porți de încâlcire fără niciun câștig. (Formula simetrică de ordinul doi aplică primul grup de două ori, la jumătate din timp de fiecare dată, și astfel costul depinde de care grup vine primul.) filter primește un predicat peste DAGOpNode și lasă neatinse porțile respinse, deci un singur pas poate ridica ordinul pe porțile care beneficiază și le poate păstra pe restul la ordinul întâi. Predicatul de mai jos acceptă o poartă doar când operatorul ei mută bosoni între moduri, ceea ce este cazul în care grupurile nu comută:
>>> 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
Dacă rulezi din nou pasul, nu se schimbă nimic: factorii pe care îi emite sunt atomici.
Factori atomici
O Evolution marcată atomic nu are definiție, deci decompose() o lasă pe loc oricât de des este aplicat. O sinteză marchează astfel factorii pe care îi emite, la fel și BosonicQDrift pentru termenii pe care îi eșantionează.
Aceasta este mai mult decât ordine. Împărțirea în continuare a unui grup hermitian, termen cu termen, produce factori care individual nu sunt hermitieni, iar exponențiala unui operator nehermitian nu este unitară:
>>> circuit = build()
>>> once = circuit.decompose()
>>> dict(circuit.decompose(reps=5).count_ops()) == dict(once.count_ops())
True
Fiecare factor trebuie să fie hermitian de unul singur. Un operator hermitian împărțit în grupuri poate produce grupuri nehermitiene, cum ar fi un termen de salt separat de adjunctul său. Verifică cu groups_are_hermitian(). Un operator fără grupuri este împărțit termen cu termen, iar etapa qubiților respinge atunci factorii cu Operator contains complex coefficients. Simetrizarea unui ordin par anulează parțial eroarea unui factor nehermitian, deci un operator grupat greșit poate arăta mai bine la ordinul 2 decât la ordinul 1, fiind totuși greșit.
Repetarea unui pas scurt
Pentru o alternativă la nivel de circuit, construiește un singur pas Trotter scurt și repetă-l. repeat() aplatizează repetările într-un singur circuit și lasă limitele adiacente, astfel încât pașii de optimizare pot acționa peste ele:
>>> step = BosonicCircuit(num_sites)
>>> step.append(Evolution(num_sites, hamiltonian, time=total_time / 4), step.modes)
>>> dict(step.repeat(4).count_ops())
{'Evolution': 4}
Transmite insert_barriers=True pentru a ține repetările separate. O barieră este mapată pe fiecare qubit al registrelor pe care le atinge, deoarece o codificare necesită mai mulți qubiți per mod, iar un mod nu are o imagine proprie per qubit:
>>> [inst.operation.name for inst in step.repeat(3, insert_barriers=True)._inner.data]
['Evolution', 'barrier', 'Evolution', 'barrier', 'Evolution']
repeat nu are legătură cu reps al unei metode de sinteză. O sinteză subdivide o evoluție singură în mai mulți factori, mai scurți. Repetarea unui circuit care conține o evoluție doar îi înmulțește timpul total.
Combinarea cu o formulă la nivel de qubiți
Cele două niveluri se compun, iar acolo unde ambele aproximează, acuratețea rezultatului este dictată de cel mai slab dintre ele. Ridicarea ordinului bosonic în timp ce formula de pe partea qubiților rămâne de ordinul întâi aduce puțin. Vezi MapperFnEvolutionSynthesis pentru formula de pe partea qubiților. Aceasta păstrează ordinea termenilor Pauli aleasă de mapper, și astfel group_wise() poate emite termenii fiecărui grup adiacent unii de alții, iar simplify() poate fixa în schimb o ordine canonică.
Pașii următori
-
Transpilarea circuitelor bosonice explică etapele care urmează după sinteza bosonică și locul în care se află
B2BSynthesisprintre ele. -
Gruparea termenilor operatorului: folosește structura operatorului explică cum se atribuie și se verifică grupurile pe care le intercalează o formulă de produs.
-
Simulează dinamica Bose-Hubbard cu o codificare la alegere aplică schema de repetare pe un exemplu complet.