Transpile di circuiti bosonici
Questa guida spiega come eseguire il transpile di un BosonicCircuit in un QuantumCircuit standard. Un circuito bosonico non porta alcun troncamento, alcuna codifica e alcun numero di qubit (vedi Lavora con i circuiti bosonici), quindi il transpiler è il punto in cui queste scelte vengono fatte. Il troncamento e la codifica sono descritti da un LevelSpec (vedi Descrivi il troncamento con un LevelSpec), e il mapper che trasforma gli operatori in operatori di qubit è descritto in Mappa le rappresentazioni degli operatori. Il risultato è un normale QuantumCircuit, quindi qualsiasi simulatore compatibile con Qiskit può eseguirlo.
La guida usa un piccolo esempio per tutta la sua durata: due modi bosonici, uno stato con un bosone in ciascuno e una singola evoluzione temporale sotto un Hamiltoniano di Bose-Hubbard i cui termini di legame e on-site sono in gruppi separati (vedi Raggruppa i termini dell'operatore: usa la struttura dell'operatore):
>>> 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}
Fasi del transpile
Come spiegato in qiskit_bosons.transpiler, i pass manager predefiniti in questo pacchetto dividono il transpile nelle seguenti fasi:
Ingresso
Converte il circuito in una struttura dati a grafo aciclico orientato (DAG), la rappresentazione su cui lavora ogni pass successivo.
Ottimizzazione
Mantiene il circuito nello spazio bosonico. Questi pass usano la struttura degli operatori bosonici, in particolare i loro gruppi, mentre non esiste ancora alcuna informazione sui qubit. B2BSynthesis è il pass che qui scompone i gate, con un plugin per ogni tipo di gate: una formula di prodotto o qDRIFT per un Evolution, e la mesh di Clements per un ModeRotation (vedi Sintetizza un gate Evolution nello spazio bosonico per come viene scelto un metodo). RelabelModes riordina i modi. Il preset decompone le rotazioni di modo in questa fase e lascia tutto il resto così com'è.
Disposizione
Mappa ogni registro di modi bosonici su un registro quantistico. Un modo bosonico generalmente occupa diversi qubit ( di essi con le codifiche unarie, con quelle compatte), quindi il registro quantistico è più largo del registro di modi. OccupationB2QLayout ricava la larghezza del registro da un LevelSpec.
Sintesi
Converte i gate bosonici in operazioni su qubit, un plugin per ogni tipo di gate. La codifica avviene in questo passo: il plugin per un gate Evolution mappa il suo operatore in un operatore di qubit, e il plugin per un gate InitializeModes imposta i qubit che codificano ogni occupazione. Una barriera viene riportata su ogni qubit di ciascun registro che aveva toccato.
Qubit
Esegue il transpile del circuito di qubit risultante con i metodi propri di Qiskit, ad esempio verso un insieme di gate di base o un dispositivo. Il preset lo include sempre; per impostazione predefinita è il pass manager predefinito di Qiskit, che sintetizza ogni evoluzione in gate (vedi Continua a livello di qubit).
Uscita
Riconverte la struttura dati DAG interna in un QuantumCircuit.
Il troncamento e la codifica non sono memorizzati nel circuito. Vengono forniti al transpiler, e il layout, la preparazione dello stato e il mapper devono concordare tutti su di essi. Il preset lo garantisce passando lo stesso LevelSpec a tutti e tre.
Un esempio pratico
generate_preset_occupation_pass_manager() costruisce la pipeline a partire da una specifica. Abbassare l'esempio con due codifiche cambia solo la specifica. Il preset termina con il transpile a livello di qubit di Qiskit stesso, che sintetizzerebbe l'evoluzione in gate. Per vedere il circuito esattamente come lo emettono le fasi bosoniche, svuota prima quella fase:
>>> 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}
Le due codifiche usano rispettivamente sei e quattro qubit (tre livelli per modo con la codifica unaria, due qubit per modo con il codice di Gray), ma la struttura del circuito è la stessa: due operazioni XGate preparano le occupazioni, e un PauliEvolutionGate porta l'evoluzione.
Confronto con il flusso di lavoro tradizionale
La guida sui circuiti bosonici descrive il flusso di lavoro tradizionale, che mappa l'Hamiltoniano in un operatore di qubit prima di costruire il QuantumCircuit. I due percorsi producono circuiti equivalenti. Differiscono in ciò che va rifatto quando il troncamento cambia. Il circuito bosonico viene costruito una volta e abbassato a ogni cutoff, mentre il percorso che mappa per primo richiede di mappare di nuovo l'operatore e ricostruire il circuito:
>>> 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
Il numero di gate di entanglement dipende da come Qiskit sintetizza il gate di evoluzione, che cambia tra una release e l'altra, quindi tracciane il grafico:
Il conteggio cresce rapidamente con il cutoff, ed è per questo che conviene tenere il cutoff come parametro del transpile e non del circuito. I due percorsi qui sono equivalenti, ma solo quello bosonico può anche intercalare il raggruppamento dell'operatore con la formula di prodotto (vedi Sintetizza un gate Evolution nello spazio bosonico) o passare la codifica a una specifica diversa senza cambiare una riga del circuito.
Una specifica con un solo cutoff lascia aperto il numero di modi, quindi un solo pass manager serve circuiti di qualsiasi larghezza. Una specifica con un cutoff per modo fissa la larghezza:
>>> 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
Costruire la pipeline a mano
Il preset è un MultiStagePassManager con una voce per ogni stage. Puoi assemblare la stessa pipeline a partire dai suoi componenti, che è il punto di partenza per ogni personalizzazione descritta di seguito:
>>> 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
La funzione mapper riceve un operatore e la larghezza del registro derivata dal layout, e restituisce un SparseObservable. Poiché riceve la larghezza, passare num_qubits a occupation_encoding() fa sì che il mapper verifichi che la specifica sia compatibile con il registro, così una discrepanza tra il layout e la codifica fallisce con un messaggio chiaro.
Workflow avanzati
Fornire un layout esplicito
CustomB2QLayout sostituisce lo stage di layout con una mappatura che fornisci tu, da ogni registro bosonico del circuito a un registro quantistico. È utile quando il registro deve essere più largo di quanto richieda la specifica, ad esempio per lasciare qubit inattivi per altri scopi:
>>> 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
Un layout personalizzato è una tua responsabilità. I plugin di sintesi continuano a indirizzare i qubit tramite la specifica, quindi un registro che non corrisponde alle larghezze della specifica produce un circuito errato o un errore nelle profondità di un plugin. Un layout personalizzato salta anche il controllo che OccupationB2QLayout esegue dopo RelabelModes (vedi la sezione successiva).
Riordinare i modi
RelabelModes è un pass dello stage di ottimizzazione che riordina i modi. La permutazione viene letta come una mappatura dall'indice di modo originale a quello nuovo e viene registrata nei metadati del circuito. Il riordino cambia i qubit occupati da ciascun modo e può quindi ridurre la profondità del circuito abbassato di livello.
Poiché il risultato è espresso nel nuovo ordine dei modi, le bitstring misurate devono essere rimappate prima di essere interpretate. Con più qubit per modo, la mappatura agisce su blocchi di qubit e non su singoli bit. Il seguente esempio rietichetta tre modi, ciascuno dei quali occupa due qubit con il codice Gray, e recupera l'occupazione originale dai conteggi misurati:
>>> # 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'
La bitstring ripristinata è ciò che il circuito produce senza la rietichettatura, letta nell'ordine little-endian di Qiskit: il modo 0 contiene il codice Gray del livello due (11), il modo 1 contiene il livello zero (00) e il modo 2 contiene il livello uno (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
Una specifica che assegna ai modi cutoff diversi non può essere combinata con RelabelModes. I cutoff sono associati ai modi in base alla loro posizione, quindi riordinare i modi assegnerebbe un cutoff al modo sbagliato. OccupationB2QLayout rifiuta in tal caso un circuito rietichettato:
>>> 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
Scegliere i plugin di sintesi
B2QSynthesis non ha plugin predefiniti; il preset ne registra uno per ogni tipo di Gate. I plugin disponibili sono gestiti da B2QSynthesisPluginManager, che li elenca in base al nome del Gate su cui agiscono:
>>> 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']
I due metodi integrati sono Evolution.MapperFn e InitializeModes.Occupation. Il primo sintetizza un'evoluzione a partire da qualsiasi funzione mapper, ed è il modo in cui si inserisce una codifica personalizzata (vedi Mappare le rappresentazioni degli operatori). La libreria non include alcun plugin per un Gate ModeRotation. Il preset lo scompone prima nello spazio bosonico in Gate Evolution, che poi il primo metodo gestisce, ma un plugin registrato per ModeRotation può invece abbassare direttamente il Gate; vedi Ruotare i modi bosonici e Abbassare una rotazione senza scomporla. Puoi registrare un metodo per nome tramite l'argomento config di B2QSynthesis, oppure scrivere direttamente un oggetto plugin in B2QSynthesis.methods, come fanno gli esempi precedenti.
Proseguire a livello di qubit
Il preset termina sempre con uno stage qubit, che è il pass manager preset di Qiskit. Gli argomenti con parola chiave di generate_preset_occupation_pass_manager() vengono passati a qiskit.transpiler.generate_preset_pass_manager(), quindi puoi tradurre in un insieme di Gate di base o scegliere direttamente come target un dispositivo. Preferisci optimization_level=0 per mantenere il circuito vicino a quello prodotto dagli stage bosonici:
>>> 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']
Esistono altre due scelte comuni. Assegna un PassManager vuoto allo stage qubit quando vuoi mostrare il circuito esattamente come è stato sintetizzato dalla pipeline, come fanno gli esempi precedenti. Oppure assembla tu stesso la pipeline e aggiungi qualsiasi pass manager di Qiskit come stage qubit del MultiStagePassManager. Gli stage vengono eseguiti nell'ordine in cui sono indicati, quindi lo stage qubit deve venire prima di output, perché i pass di Qiskit si aspettano un DAG e non un circuito finito:
>>> 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']
Passi successivi
-
Mappare le rappresentazioni degli operatori per le codifiche e per scrivere un mapper personalizzato.
-
Sintetizzare un Gate Evolution nello spazio bosonico per scegliere una formula di prodotto per i Gate di evoluzione.
-
Ruotare i modi bosonici per abbassare le rotazioni dei modi.
-
Simulare la dinamica di Bose-Hubbard con una scelta di codifica per una simulazione completa che confronta il circuito abbassato con la dinamica esatta.
-
qiskit_bosons.transpilerper la descrizione completa degli stage e dei pass disponibili.