Transpilace bosonických obvodů
Tento průvodce vysvětluje, jak transpilovat BosonicCircuit na standardní QuantumCircuit. Bosonický obvod nenese žádné oříznutí, žádné kódování ani počet qubitů (viz Práce s bosonickými obvody), takže transpilátor je místo, kde se tato rozhodnutí dělají. Oříznutí a kódování popisuje LevelSpec (viz Popis oříznutí pomocí LevelSpec) a mapper, který převádí operátory na qubitové operátory, je popsán v Mapování reprezentací operátorů. Výsledkem je běžný QuantumCircuit, takže ho může spustit jakýkoli simulátor kompatibilní s Qiskitem.
Průvodce používá celou dobu jeden malý příklad: dva bosonické módy, stav s jedním bosonem v každém a jedinou časovou evoluci podle Bose-Hubbardova hamiltoniánu, jehož vazební a on-site členy jsou v oddělených skupinách (viz Seskupování členů operátoru: využití struktury operátoru):
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> terms = {(cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0}
>>> terms.update({(cre(mode), cre(mode), ann(mode), ann(mode)): 0.5 for mode in range(2)})
>>> hamiltonian = BosonOperator.from_dict(terms)
>>> # the bond is group 0 and the on-site interaction is group 1; derive the index from the term,
>>> # because the stored order of a dictionary is not specified
>>> hamiltonian.groups = [
... 0 if len({mode for _, mode in term}) == 2 else 1 for term, _ in hamiltonian.iter_terms()
... ]
>>>
>>> circuit = BosonicCircuit(2)
>>> circuit.append(InitializeModes([1, 1]), circuit.modes)
>>> circuit.append(Evolution(2, hamiltonian, time=0.1), circuit.modes)
>>> dict(circuit.count_ops())
{'InitializeModes': 1, 'Evolution': 1}
Fáze transpilace
Jak je vysvětleno v qiskit_bosons.transpiler, předvolené pass managery v tomto balíčku dělí transpilaci do následujících fází:
Vstup
Převede obvod na datovou strukturu orientovaného acyklického grafu (DAG), reprezentaci, na které pracuje každý pozdější průchod.
Optimalizace
Ponechává obvod v bosonickém prostoru. Tyto průchody využívají strukturu bosonických operátorů, zejména jejich skupiny, zatímco zatím neexistují žádné informace o qubitech. B2BSynthesis je průchod, který zde hradla rozkládá, s pluginem pro každý typ hradla: produktová formule nebo qDRIFT pro Evolution a Clementsova síť pro ModeRotation (jak se metoda vybírá, viz Syntéza hradla Evolution v bosonickém prostoru). RelabelModes přeuspořádá módy. Předvolba v této fázi dekomponuje rotace módů a vše ostatní ponechává tak, jak je.
Rozložení
Mapuje každý registr bosonických módů na kvantový registr. Bosonický mód obecně zabírá několik qubitů ( z nich při unárních kódováních, při kompaktních), takže kvantový registr je širší než registr módů. OccupationB2QLayout odvozuje šířku registru z LevelSpec.
Syntéza
Převádí bosonická hradla na qubitové operace, jeden plugin na typ hradla. V tomto kroku probíhá kódování: plugin pro hradlo Evolution mapuje jeho operátor na qubitový operátor a plugin pro hradlo InitializeModes nastaví qubity, které kódují každé obsazení. Bariéra se přenese na každý qubit každého registru, kterého se dotýkala.
Qubit
Transpiluje výsledný qubitový obvod vlastními metodami Qiskitu, například na bázovou sadu hradel nebo zařízení. Předvolba ji vždy zahrnuje; ve výchozím nastavení je to předvolený pass manager Qiskitu, který syntetizuje každou evoluci na hradla (viz Pokračování na úrovni qubitů).
Výstup
Převede interní datovou strukturu DAG zpět na QuantumCircuit.
Oříznutí a kódování nejsou v obvodu uložena. Předávají se transpilátoru a rozložení, příprava stavu a mapper se na nich musí všechny shodovat. Předvolba to zajišťuje tím, že všem třem předá tentýž LevelSpec.
Praktický příklad
generate_preset_occupation_pass_manager() sestaví pipeline ze specifikace. Snížení příkladu při dvou kódováních mění jen specifikaci. Předvolba končí vlastní transpilací Qiskitu na úrovni qubitů, která by evoluci syntetizovala na hradla. Abys viděl obvod přesně tak, jak ho bosonické fáze vydají, nejprve tuto fázi vyprázdni:
>>> from qiskit.transpiler import PassManager
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> for encoding in ("unary_unprojected", "gray"):
... pass_manager = generate_preset_occupation_pass_manager(LevelSpec(2, encoding))
... pass_manager.qubit = PassManager()
... lowered = pass_manager.run(circuit)
... print(f"{encoding:18s} qubits={lowered.num_qubits} ops={dict(lowered.count_ops())}")
unary_unprojected qubits=6 ops={'x': 2, 'PauliEvolution': 1}
gray qubits=4 ops={'x': 2, 'PauliEvolution': 1}
Obě kódování používají šest a čtyři qubity (tři úrovně na mód při unárním kódování, dva qubity na mód při Grayově kódu), ale struktura obvodu je stejná: dvě operace XGate připraví obsazení a jeden PauliEvolutionGate nese evoluci.
Porovnání s tradičním postupem
Průvodce bosonickými obvody popisuje tradiční postup, který mapuje hamiltonián na qubitový operátor před sestavením QuantumCircuit. Obě cesty dávají ekvivalentní obvody. Liší se tím, co je třeba udělat znovu, když se změní oříznutí. Bosonický obvod se sestaví jednou a sníží při každém cutoffu, zatímco cesta s mapováním napřed vyžaduje operátor zmapovat znovu a obvod znovu sestavit:
>>> from qiskit import QuantumCircuit
>>> from qiskit.circuit.library import PauliEvolutionGate
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>>
>>> basis = ["rz", "sx", "x", "cx"]
>>> cx_counts = []
>>> for max_occupation in (1, 2, 3):
... spec = LevelSpec(max_occupation, "gray")
... # the bosonic circuit is reused as it is
... lowered = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=basis
... ).run(circuit)
... # the map-first route maps the operator and builds a new circuit for every cutoff
... qubit_hamiltonian = occupation_encoding(hamiltonian, spec)
... by_hand = QuantumCircuit(qubit_hamiltonian.num_qubits)
... _ = by_hand.append(PauliEvolutionGate(qubit_hamiltonian, time=0.1), by_hand.qubits)
... cx_counts.append(lowered.count_ops().get("cx", 0))
... print(
... f"d={max_occupation + 1}: {lowered.num_qubits} qubits; map-first: "
... f"{len(qubit_hamiltonian)} Pauli terms to place by hand"
... )
d=2: 2 qubits; map-first: 2 Pauli terms to place by hand
d=3: 4 qubits; map-first: 10 Pauli terms to place by hand
d=4: 4 qubits; map-first: 22 Pauli terms to place by hand
Počet provázávacích hradel závisí na tom, jak Qiskit syntetizuje hradlo evoluce, což se mezi vydáními mění, proto ho vykresli:
Počet s cutoffem rychle roste, což je důvod, proč cutoff ponechat parametrem transpilace, a ne obvodu. Obě cesty jsou zde ekvivalentní, ale jen ta bosonická může také prokládat seskupení operátoru s produktovou formulí (viz Syntéza hradla Evolution v bosonickém prostoru) nebo předat kódování jiné specifikaci beze změny jediného řádku obvodu.
Specifikace s jediným cutoffem ponechává počet módů otevřený, takže jeden pass manager obslouží obvody libovolné šířky. Specifikace s jedním cutoffem na mód šířku pevně určuje:
>>> wide = BosonicCircuit(3)
>>> wide.append(InitializeModes([1, 0, 1]), wide.modes)
>>> shared = generate_preset_occupation_pass_manager(LevelSpec(2, "gray"))
>>> shared.run(wide).num_qubits
6
>>> per_mode = generate_preset_occupation_pass_manager(LevelSpec([2, 1], "gray"))
>>> per_mode.run(circuit).num_qubits
3
>>> try:
... per_mode.run(wide)
... except ValueError as error:
... print(error)
the spec covers 2 modes, but 3 were expected
Sestavení pipeline ručně
Předvolba je MultiStagePassManager s jedním záznamem pro každou fázi. Stejný pipeline můžeš sestavit z jeho částí, což je výchozí bod pro všechna následující přizpůsobení:
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.transpiler import BosonicCircuitToDAG, QuantumDAGToCircuit
>>> from qiskit_bosons.transpiler.passes import (
... B2QSynthesis,
... MapperFnEvolutionSynthesis,
... OccupationB2QLayout,
... OccupationInitializeModesSynthesis,
... )
>>>
>>> spec = LevelSpec(2, "gray")
>>>
>>> def mapper_fn(operator, num_qubits):
... return occupation_encoding(operator, spec, num_qubits)
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(mapper_fn)
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>>
>>> by_hand = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> preset = generate_preset_occupation_pass_manager(spec)
>>> preset.qubit = PassManager()
>>> by_hand.run(circuit) == preset.run(circuit)
True
Funkce mapperu přijímá operátor a šířku registru, kterou odvodilo rozložení, a vrací SparseObservable. Protože dostává šířku, předáním num_qubits do occupation_encoding() mapper zkontroluje, že specifikace odpovídá registru, takže nesoulad mezi rozložením a kódováním skončí jasnou chybovou zprávou.
Pokročilé postupy
Zadání explicitního rozložení
CustomB2QLayout nahradí fázi rozložení mapováním, které zadáš, z každého bosonického registru obvodu na kvantový registr. Hodí se, když musí být registr širší, než specifikace vyžaduje, například aby zůstaly nevyužité qubity pro jiné účely:
>>> from qiskit.circuit import QuantumRegister
>>> from qiskit_bosons.transpiler.passes import CustomB2QLayout
>>>
>>> (mode_register,) = BosonicCircuitToDAG().run(circuit).qregs.values()
>>> custom = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=CustomB2QLayout({mode_register: QuantumRegister(6, "q")}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> custom.run(circuit).num_qubits
6
Vlastní rozložení je tvá zodpovědnost. Syntézní pluginy stále adresují qubity přes specifikaci, takže registr, který neodpovídá šířkám ve specifikaci, vede k chybnému obvodu nebo k chybě hluboko uvnitř pluginu. Vlastní rozložení také přeskakuje kontrolu, kterou provádí OccupationB2QLayout po RelabelModes (viz další část).
Změna pořadí módů
RelabelModes je průchod fáze optimalizace, který mění pořadí módů. Permutace se čte jako mapování z původního na nový index módu a zaznamenává se do metadat obvodu. Změna pořadí ovlivní, které qubity každý mód zabírá, a proto může snížit hloubku převedeného obvodu.
Protože je výsledek vyjádřen v novém pořadí módů, musí se naměřené bitové řetězce před interpretací namapovat zpět. Při více qubitech na mód mapování působí na bloky qubitů, nikoli na jednotlivé bity. Následující příklad přeoznačí tři módy, z nichž každý zabírá dva qubity v Grayově kódu, a z naměřených četností obnoví původní obsazení:
>>> # BasicSimulator is a convenient pure-Python choice; any simulator works
>>> from qiskit.providers.basic_provider import BasicSimulator
>>> from qiskit_bosons.transpiler.passes import RelabelModes
>>>
>>> state = BosonicCircuit(3)
>>> state.append(InitializeModes([2, 0, 1]), state.modes)
>>> spec = LevelSpec(2, "gray") # three levels, two qubits per mode
>>> width = spec.resolve(3).widths[0]
>>> permutation = [2, 0, 1]
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> relabeled = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=RelabelModes(permutation),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... ).run(state)
>>> relabeled.metadata
{'permutation': [2, 0, 1]}
>>>
>>> def restore(bitstring, permutation, width):
... """Map a bitstring in the new mode order back to the original mode order."""
... qubits = bitstring[::-1] # character q is now the value of qubit q
... blocks = [qubits[mode * width : (mode + 1) * width] for mode in range(len(permutation))]
... return "".join(blocks[new] for new in permutation)[::-1]
>>>
>>> relabeled.measure_all()
>>> (measured,) = BasicSimulator().run(relabeled, shots=1).result().get_counts()
>>> measured
'110100'
>>> restore(measured, permutation, width)
'010011'
Obnovený bitový řetězec je to, co obvod vytvoří bez přeoznačení, čteno v little-endian pořadí Qiskitu: mód 0 obsahuje Grayův kód úrovně dva (11), mód 1 úroveň nula (00) a mód 2 úroveň jedna (01).
>>> plain = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... ).run(state)
>>> plain.measure_all()
>>> (reference,) = BasicSimulator().run(plain, shots=1).result().get_counts()
>>> restore(measured, permutation, width) == reference
True
Specifikaci, která módům přiřazuje různé cutoffy, nelze kombinovat s RelabelModes. Cutoffy jsou k módům přiřazeny podle jejich pozice, takže změna pořadí módů by přiřadila cutoff nesprávnému módu. OccupationB2QLayout v takovém případě přeoznačený obvod odmítne:
>>> mixed = LevelSpec([2, 1, 2], "gray")
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(mixed)
>>> refused = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=RelabelModes(permutation),
... layout=OccupationB2QLayout(mixed),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> try:
... refused.run(state)
... except ValueError as error:
... print(str(error).split(",")[0])
the circuit has been relabeled by RelabelModes
Výběr syntézních pluginů
B2QSynthesis nemá žádné výchozí pluginy; předvolba registruje jeden pro každý typ hradla. Dostupné pluginy spravuje B2QSynthesisPluginManager, který je vypisuje podle názvu hradla, na které působí:
>>> from qiskit_bosons.transpiler.passes import B2QSynthesisPluginManager
>>> manager = B2QSynthesisPluginManager()
>>> for gate in manager.op_names():
... print(gate, manager.method_names(gate))
Evolution ['MapperFn']
InitializeModes ['Occupation']
Dvě vestavěné metody jsou Evolution.MapperFn a InitializeModes.Occupation. První syntetizuje evoluci z libovolné funkce mapperu, čímž se zapojuje vlastní kódování (viz Mapování reprezentací operátorů). Knihovna neobsahuje žádný plugin pro hradlo ModeRotation. Předvolba jej nejprve rozloží v bosonickém prostoru na hradla Evolution, která pak zpracuje první metoda, ale plugin zaregistrovaný pro ModeRotation může hradlo převést přímo; viz Rotace bosonických módů a Převod rotace bez rozkladu. Metodu můžeš zaregistrovat podle názvu přes argument config třídy B2QSynthesis, nebo zapsat objekt pluginu přímo do B2QSynthesis.methods, jak to dělají předchozí příklady.
Pokračování na úrovni qubitů
Předvolba vždy končí fází qubit, což je vlastní předvolený pass manager Qiskitu. Argumenty klíčových slov funkce generate_preset_occupation_pass_manager() se předávají do qiskit.transpiler.generate_preset_pass_manager(), takže můžeš překládat do bázové sady hradel nebo cílit přímo na zařízení. Raději použij optimization_level=0, aby obvod zůstal blízko tomu, co vytvořily bosonické fáze:
>>> from qiskit.transpiler import generate_preset_pass_manager
>>>
>>> spec = LevelSpec(2, "gray")
>>> translated = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=["rz", "sx", "x", "cx"]
... ).run(circuit)
>>> sorted(translated.count_ops())
['cx', 'rz', 'sx', 'x']
Časté jsou dvě další volby. Přiřaď fázi qubit prázdný PassManager, když chceš zobrazit obvod přesně tak, jak ho pipeline syntetizovala, jako to dělají předchozí příklady. Nebo sestav pipeline sám a přidej libovolný pass manager Qiskitu jako fázi qubit třídy MultiStagePassManager. Fáze se spouštějí v zadaném pořadí, takže fáze qubit musí být před output, protože průchody Qiskitu očekávají DAG a ne hotový obvod:
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... lambda operator, num_qubits: occupation_encoding(operator, spec, num_qubits)
... )
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> staged = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(
... optimization_level=0, basis_gates=["rz", "sx", "x", "cx"]
... ),
... output=QuantumDAGToCircuit(),
... )
>>> sorted(staged.run(circuit).count_ops())
['cx', 'rz', 'sx', 'x']
Další kroky
-
Mapování reprezentací operátorů o kódováních a o psaní vlastního mapperu.
-
Syntéza hradla Evolution v bosonickém prostoru o výběru součinového vzorce pro evoluční hradla.
-
Rotace bosonických módů o převodu rotací módů.
-
Simulace Bose-Hubbardovy dynamiky s volbou kódování pro úplnou simulaci, která porovnává převedený obvod s přesnou dynamikou.
-
qiskit_bosons.transpilerpro úplný popis fází a dostupných průchodů.