Vai al contenuto principale
Questo progetto è in fase beta. Hai trovato un bug? Apri una issue su GitHub. Hai una domanda o un'idea? Chiedi su GitHub Discussions — apprezziamo il tuo feedback!
Com'è la qualità della traduzione su questa pagina?

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 (dd di essi con le codifiche unarie, ⌈log⁡2d⌉\lceil \log_2 d \rceil 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:

CX count of the lowered circuit against the number of levels d, growing quickly with d.

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
Avvertenza

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
Avvertenza

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​