Zum Hauptinhalt springen
Dieses Projekt befindet sich in der Beta-Phase. Einen Fehler gefunden? GitHub Issue erstellen. Frage oder Idee? In Discussions fragen — wir freuen uns über dein Feedback!
Wie ist die Übersetzungsqualität auf dieser Seite?

Bosonische Circuits transpilieren

Dieser Leitfaden erklärt, wie du einen BosonicCircuit in einen standardmäßigen QuantumCircuit transpilierst. Ein bosonischer Circuit trägt keine Abschneidung, keine Kodierung und keine Qubit-Anzahl (siehe Mit bosonischen Circuits arbeiten), daher werden diese Entscheidungen im Transpiler getroffen. Die Abschneidung und die Kodierung werden durch eine LevelSpec beschrieben (siehe Die Abschneidung mit einer LevelSpec beschreiben), und der Mapper, der Operatoren in Qubit-Operatoren überführt, ist in Operatordarstellungen abbilden beschrieben. Das Ergebnis ist ein gewöhnlicher QuantumCircuit, den jeder Qiskit-kompatible Simulator ausführen kann.

Der Leitfaden verwendet durchgehend ein kleines Beispiel: zwei bosonische Moden, einen Zustand mit je einem Boson und eine einzelne Zeitentwicklung unter einem Bose-Hubbard-Hamiltonoperator, dessen Bindungs- und Gitterplatzterme in getrennten Gruppen liegen (siehe Operatorterme gruppieren: die Operatorstruktur nutzen):

>>> 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}

Transpilationsstufen​

Wie in qiskit_bosons.transpiler erklärt, teilen die voreingestellten Pass Manager in diesem Paket die Transpilation in die folgenden Stufen auf:

Eingabe​

Wandelt den Circuit in eine Datenstruktur als gerichteten azyklischen Graphen (DAG) um, die Darstellung, auf der jeder spätere Pass arbeitet.

Optimierung​

Belässt den Circuit im bosonischen Raum. Diese Passes nutzen die Struktur der bosonischen Operatoren, insbesondere ihre Gruppen, während noch keine Qubit-Information existiert. B2BSynthesis ist der Pass, der hier Gates zerlegt, mit einem Plugin für jeden Gate-Typ: eine Produktformel oder qDRIFT für ein Evolution und das Clements-Netz für eine ModeRotation (siehe Ein Evolution-Gate im bosonischen Raum synthetisieren, wie eine Methode gewählt wird). RelabelModes ordnet die Moden neu. Die Voreinstellung zerlegt Moden-Rotationen in dieser Stufe und lässt alles andere unverändert.

Layout​

Bildet jedes bosonische Moden-Register auf ein Quantenregister ab. Eine bosonische Mode belegt im Allgemeinen mehrere Qubits (dd davon bei den unären Kodierungen, ⌈log⁡2d⌉\lceil \log_2 d \rceil bei den kompakten), sodass das Quantenregister breiter ist als das Moden-Register. OccupationB2QLayout leitet die Registerbreite aus einer LevelSpec ab.

Synthese​

Wandelt bosonische Gates in Qubit-Operationen um, ein Plugin pro Gate-Typ. In diesem Schritt findet die Kodierung statt: Das Plugin für ein Evolution-Gate bildet seinen Operator auf einen Qubit-Operator ab, und das Plugin für ein InitializeModes-Gate setzt die Qubits, die jede Besetzung kodieren. Eine Barriere wird auf jedes Qubit jedes Registers übertragen, das sie berührt hat.

Qubit​

Transpiliert den resultierenden Qubit-Circuit mit Qiskits eigenen Methoden, zum Beispiel auf einen Basis-Gate-Satz oder ein Gerät. Die Voreinstellung enthält diese Stufe immer; standardmäßig ist es Qiskits voreingestellter Pass Manager, der jede Evolution zu Gates synthetisiert (siehe Auf Qubit-Ebene fortfahren).

Ausgabe​

Wandelt die interne DAG-Datenstruktur zurück in einen QuantumCircuit.

Die Abschneidung und die Kodierung sind nicht im Circuit gespeichert. Sie werden dem Transpiler übergeben, und das Layout, die Zustandspräparation und der Mapper müssen alle darin übereinstimmen. Die Voreinstellung stellt das sicher, indem sie dieselbe LevelSpec an alle drei weitergibt.

Ein praktisches Beispiel​

generate_preset_occupation_pass_manager() baut die Pipeline aus einer Spezifikation. Das Absenken des Beispiels unter zwei Kodierungen ändert nur die Spezifikation. Die Voreinstellung endet in Qiskits eigener Transpilation auf Qubit-Ebene, die die Evolution zu Gates synthetisieren würde. Um den Circuit genau so zu sehen, wie ihn die bosonischen Stufen ausgeben, leere zuerst diese Stufe:

>>> 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}

Die beiden Kodierungen verwenden sechs bzw. vier Qubits (drei Niveaus pro Mode bei der unären Kodierung, zwei Qubits pro Mode beim Gray-Code), aber die Struktur des Circuits ist dieselbe: Zwei XGate-Operationen bereiten die Besetzungen vor, und ein PauliEvolutionGate trägt die Evolution.

Mit dem traditionellen Workflow vergleichen​

Der Leitfaden zu bosonischen Circuits beschreibt den traditionellen Workflow, der den Hamiltonoperator auf einen Qubit-Operator abbildet, bevor er den QuantumCircuit baut. Die beiden Wege erzeugen äquivalente Circuits. Sie unterscheiden sich darin, was neu gemacht werden muss, wenn sich die Abschneidung ändert. Der bosonische Circuit wird einmal gebaut und bei jedem Cutoff abgesenkt, wohingegen der Weg mit vorheriger Abbildung erfordert, dass der Operator erneut abgebildet und der Circuit neu gebaut wird:

>>> 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

Die Anzahl der verschränkenden Gates hängt davon ab, wie Qiskit das Evolution-Gate synthetisiert, was sich zwischen Releases ändert, also stelle sie grafisch dar:

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

Die Anzahl wächst schnell mit dem Cutoff, und das ist der Grund, den Cutoff zu einem Parameter der Transpilation und nicht des Circuits zu machen. Die beiden Wege sind hier äquivalent, aber nur der bosonische kann außerdem die Gruppierung des Operators mit der Produktformel verzahnen (siehe Ein Evolution-Gate im bosonischen Raum synthetisieren) oder die Kodierung an eine andere Spezifikation übergeben, ohne eine Zeile des Circuits zu ändern.

Eine Spezifikation mit einem einzelnen Cutoff lässt die Anzahl der Moden offen, sodass ein Pass Manager Circuits beliebiger Breite bedient. Eine Spezifikation mit einem Cutoff pro Mode legt die Breite fest:

>>> 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

Die Pipeline von Hand bauen​

Das Preset ist ein MultiStagePassManager mit einem Eintrag pro Stufe. Du kannst dieselbe Pipeline aus ihren Einzelteilen zusammensetzen, was der Ausgangspunkt für alle folgenden Anpassungen ist:

>>> 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

Die Mapper-Funktion erhält einen Operator und die Breite des Registers, die das Layout abgeleitet hat, und gibt eine SparseObservable zurück. Da ihr die Breite übergeben wird, sorgt die Weitergabe von num_qubits an occupation_encoding() dafür, dass der Mapper prüft, ob die Spezifikation in das Register passt. Eine Abweichung zwischen Layout und Kodierung schlägt so mit einer klaren Meldung fehl.

Fortgeschrittene Workflows​

Ein explizites Layout angeben​

CustomB2QLayout ersetzt die Layout-Stufe durch eine von dir vorgegebene Zuordnung von jedem bosonischen Register des Circuits zu einem Quantenregister. Das ist nützlich, wenn das Register breiter sein muss, als die Spezifikation verlangt, zum Beispiel um ungenutzte Qubits für andere Zwecke freizuhalten:

>>> 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
Warnung

Ein benutzerdefiniertes Layout liegt in deiner Verantwortung. Die Synthese-Plugins adressieren Qubits weiterhin über die Spezifikation. Ein Register, das nicht zu den Breiten der Spezifikation passt, führt daher zu einem falschen Circuit oder zu einem Fehler tief in einem Plugin. Ein benutzerdefiniertes Layout überspringt außerdem die Prüfung, die OccupationB2QLayout nach RelabelModes durchführt (siehe nächster Abschnitt).

Die Moden umordnen​

RelabelModes ist ein Pass der Optimierungsstufe, der die Moden umordnet. Die Permutation wird als Zuordnung vom ursprünglichen zum neuen Modenindex gelesen und in den Metadaten des Circuits festgehalten. Die Umordnung ändert, welche Qubits jede Mode belegt, und kann so die Tiefe des heruntergebrochenen Circuits verringern.

Da das Ergebnis in der neuen Modenreihenfolge ausgedrückt wird, müssen gemessene Bitstrings zurückabgebildet werden, bevor man sie interpretiert. Bei mehreren Qubits pro Mode wirkt die Abbildung auf Blöcke von Qubits und nicht auf einzelne Bits. Das folgende Beispiel ordnet drei Moden um, die unter dem Gray-Code jeweils zwei Qubits belegen, und stellt aus den gemessenen Counts die ursprüngliche Besetzung wieder her:

>>> # 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'

Der wiederhergestellte Bitstring ist das, was der Circuit ohne die Umbenennung erzeugt, gelesen in der Little-Endian-Reihenfolge von Qiskit: Mode 0 hält den Gray-Code von Niveau zwei (11), Mode 1 hält Niveau null (00) und Mode 2 hält Niveau eins (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
Warnung

Eine Spezifikation, die den Moden unterschiedliche Cutoffs gibt, lässt sich nicht mit RelabelModes kombinieren. Die Cutoffs sind den Moden über ihre Position zugeordnet, daher würde das Umordnen der Moden einen Cutoff an die falsche Mode setzen. OccupationB2QLayout lehnt in diesem Fall einen umbenannten Circuit ab:

>>> 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

Die Synthese-Plugins auswählen​

B2QSynthesis hat keine Standard-Plugins; das Preset registriert eines pro Gate-Typ. Die verfügbaren Plugins werden von B2QSynthesisPluginManager verwaltet, der sie nach dem Namen des Gates auflistet, auf das sie wirken:

>>> 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']

Die beiden eingebauten Methoden sind Evolution.MapperFn und InitializeModes.Occupation. Die erste synthetisiert eine Evolution aus einer beliebigen Mapper-Funktion, wodurch sich eine eigene Kodierung einbinden lässt (siehe Operatordarstellungen abbilden). Die Bibliothek liefert kein Plugin für ein ModeRotation-Gate mit. Das Preset zerlegt es im bosonischen Raum zunächst in Evolution-Gates, die dann die erste Methode behandelt. Ein für ModeRotation registriertes Plugin kann das Gate stattdessen direkt herunterbrechen; siehe Bosonische Moden rotieren und Eine Rotation ohne Zerlegung herunterbrechen. Du kannst eine Methode über das Argument config von B2QSynthesis namentlich registrieren oder ein Plugin-Objekt direkt in B2QSynthesis.methods schreiben, wie es die vorangehenden Beispiele tun.

Auf der Qubit-Ebene fortfahren​

Das Preset endet immer mit einer qubit-Stufe, die der eigene Preset-Pass-Manager von Qiskit ist. Schlüsselwortargumente von generate_preset_occupation_pass_manager() werden an qiskit.transpiler.generate_preset_pass_manager() weitergereicht, sodass du in einen Basis-Gate-Satz übersetzen oder direkt ein Gerät anvisieren kannst. Bevorzuge optimization_level=0, damit der Circuit nah an dem bleibt, was die bosonischen Stufen erzeugt haben:

>>> 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']

Zwei weitere Möglichkeiten sind üblich. Weise der qubit-Stufe einen leeren PassManager zu, wenn du den Circuit genau so zeigen möchtest, wie die Pipeline ihn synthetisiert hat, wie es die obigen Beispiele tun. Oder setze die Pipeline selbst zusammen und füge einen beliebigen Qiskit-Pass-Manager als qubit-Stufe des MultiStagePassManager hinzu. Die Stufen laufen in der angegebenen Reihenfolge, daher muss die qubit-Stufe vor output stehen, weil die Passes von Qiskit einen DAG erwarten und keinen fertigen Circuit:

>>> 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']

Nächste Schritte​