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 ( davon bei den unären Kodierungen, 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:
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
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
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
-
Operatordarstellungen abbilden für die Kodierungen und zum Schreiben eines eigenen Mappers.
-
Eine Evolution-Gate im bosonischen Raum synthetisieren zur Wahl einer Produktformel für die Evolution-Gates.
-
Bosonische Moden rotieren zum Herunterbrechen von Moden-Rotationen.
-
Bose-Hubbard-Dynamik mit wählbarer Kodierung simulieren für eine vollständige Simulation, die den heruntergebrochenen Circuit gegen die exakte Dynamik prüft.
-
qiskit_bosons.transpilerfür die vollständige Beschreibung der Stufen und der verfügbaren Passes.