Aller au contenu principal
Ce projet est en phase bêta. Tu as trouvé un bug ? Ouvrir un ticket GitHub. Une question ou une idée ? Pose ta question dans Discussions — tes retours sont les bienvenus !
Quelle est la qualité de la traduction sur cette page ?

Transpiler des circuits bosoniques

Ce guide explique comment transpiler un BosonicCircuit en un QuantumCircuit standard. Un circuit bosonique ne comporte ni troncature, ni encodage, ni nombre de qubits (voir Travailler avec des circuits bosoniques), donc le transpileur est l'endroit où ces choix sont faits. La troncature et l'encodage sont décrits par une LevelSpec (voir Décrire la troncature avec une LevelSpec), et le mapper qui transforme les opérateurs en opérateurs de qubits est décrit dans Faire correspondre les représentations d'opérateurs. Le résultat est un QuantumCircuit ordinaire, donc tout simulateur compatible avec Qiskit peut l'exécuter.

Le guide utilise un petit exemple tout du long : deux modes bosoniques, un état avec un boson dans chacun, et une seule évolution temporelle sous un hamiltonien de Bose-Hubbard dont les termes de liaison et les termes sur site sont dans des groupes séparés (voir Regrouper les termes d'un opérateur : utiliser la structure de l'opérateur) :

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

Étapes de transpilation​

Comme expliqué dans qiskit_bosons.transpiler, les gestionnaires de passes prédéfinis de ce paquet découpent la transpilation en les étapes suivantes :

Entrée​

Convertit le circuit en une structure de données de graphe orienté acyclique (DAG), la représentation sur laquelle toutes les passes suivantes travaillent.

Optimisation​

Garde le circuit dans l'espace bosonique. Ces passes utilisent la structure des opérateurs bosoniques, en particulier leurs groupes, alors qu'aucune information de qubit n'existe encore. B2BSynthesis est la passe qui décompose ici les portes, avec un plugin pour chaque type de porte : une formule de produit ou qDRIFT pour une Evolution, et le maillage de Clements pour une ModeRotation (voir Synthétiser une porte Evolution dans l'espace bosonique pour savoir comment une méthode est choisie). RelabelModes réordonne les modes. Le préréglage décompose les rotations de modes à cette étape et laisse tout le reste tel quel.

Disposition​

Fait correspondre chaque registre de modes bosoniques à un registre quantique. Un mode bosonique occupe généralement plusieurs qubits (dd d'entre eux sous les encodages unaires, ⌈log⁡2d⌉\lceil \log_2 d \rceil sous les encodages compacts), donc le registre quantique est plus large que le registre de modes. OccupationB2QLayout déduit la largeur du registre d'une LevelSpec.

Synthèse​

Convertit les portes bosoniques en opérations sur qubits, un plugin par type de porte. L'encodage se produit à cette étape : le plugin d'une porte Evolution fait correspondre son opérateur à un opérateur de qubits, et le plugin d'une porte InitializeModes fixe les qubits qui encodent chaque occupation. Une barrière est reportée sur chaque qubit de chaque registre qu'elle touchait.

Qubit​

Transpile le circuit de qubits obtenu avec les méthodes propres à Qiskit, par exemple vers un jeu de portes de base ou un dispositif. Le préréglage l'inclut toujours ; par défaut, c'est le gestionnaire de passes prédéfini de Qiskit, qui synthétise chaque évolution en portes (voir Continuer au niveau des qubits).

Sortie​

Reconvertit la structure de données DAG interne en un QuantumCircuit.

La troncature et l'encodage ne sont pas stockés dans le circuit. Ils sont donnés au transpileur, et la disposition, la préparation d'état et le mapper doivent tous s'accorder sur eux. Le préréglage l'assure en transmettant la même LevelSpec aux trois.

Un exemple pratique​

generate_preset_occupation_pass_manager() construit le pipeline à partir d'une spécification. Abaisser l'exemple sous deux encodages ne change que la spécification. Le préréglage se termine par la transpilation propre à Qiskit au niveau des qubits, qui synthétiserait l'évolution en portes. Pour voir le circuit exactement tel que les étapes bosoniques l'émettent, vide d'abord cette étape :

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

Les deux encodages utilisent respectivement six et quatre qubits (trois niveaux par mode sous l'encodage unaire, deux qubits par mode sous le code de Gray), mais la structure du circuit est la même : deux opérations XGate préparent les occupations, et un PauliEvolutionGate porte l'évolution.

Comparaison avec le flux de travail traditionnel​

Le guide sur les circuits bosoniques décrit le flux de travail traditionnel, qui fait correspondre le hamiltonien à un opérateur de qubits avant de construire le QuantumCircuit. Les deux voies produisent des circuits équivalents. Elles diffèrent par ce qu'il faut refaire lorsque la troncature change. Le circuit bosonique est construit une fois et abaissé à chaque coupure, alors que la voie qui applique d'abord la correspondance exige de refaire la correspondance de l'opérateur et de reconstruire le circuit :

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

Le nombre de portes intriquantes dépend de la façon dont Qiskit synthétise la porte d'évolution, ce qui change d'une version à l'autre, donc trace-le :

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

Le nombre croît rapidement avec la coupure, ce qui justifie de garder la coupure comme paramètre de la transpilation et non du circuit. Les deux voies sont équivalentes ici, mais seule la voie bosonique peut aussi entrelacer le regroupement de l'opérateur avec la formule de produit (voir Synthétiser une porte Evolution dans l'espace bosonique) ou confier l'encodage à une autre spécification sans changer une ligne du circuit.

Une spécification avec une seule coupure laisse le nombre de modes ouvert, donc un seul gestionnaire de passes sert des circuits de n'importe quelle largeur. Une spécification avec une coupure par mode fixe la largeur :

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

Construire le pipeline à la main​

Le preset est un MultiStagePassManager avec une entrée par étape. Tu peux assembler le même pipeline à partir de ses éléments, ce qui est le point de départ de toutes les personnalisations ci-dessous :

>>> 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 fonction de mapper reçoit un opérateur et la largeur du registre que le layout a déduite, et renvoie un SparseObservable. Comme la largeur lui est fournie, transmettre num_qubits à occupation_encoding() fait vérifier au mapper que la spécification tient dans le registre, de sorte qu'une incohérence entre le layout et l'encodage échoue avec un message clair.

Workflows avancés​

Fournir un layout explicite​

CustomB2QLayout remplace l'étape de layout par une correspondance que tu fournis, de chaque registre bosonique du circuit vers un registre quantique. Elle est utile lorsque le registre doit être plus large que ce dont la spécification a besoin, par exemple pour laisser des qubits inactifs à d'autres fins :

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

Un layout personnalisé est de ta responsabilité. Les plugins de synthèse adressent toujours les qubits via la spécification ; un registre qui ne correspond pas aux largeurs de la spécification produit donc un circuit erroné ou une erreur au fond d'un plugin. Un layout personnalisé ignore aussi la vérification que OccupationB2QLayout effectue après RelabelModes (voir la section suivante).

Réordonner les modes​

RelabelModes est une passe de l'étape d'optimisation qui réordonne les modes. La permutation est lue comme une correspondance de l'indice de mode d'origine vers le nouvel indice, et elle est enregistrée dans les métadonnées du circuit. Le réordonnancement change les qubits qu'occupe chaque mode, et peut donc réduire la profondeur du circuit abaissé.

Comme le résultat est exprimé dans le nouvel ordre des modes, les chaînes de bits mesurées doivent être remises dans l'ordre d'origine avant d'être interprétées. Avec plusieurs qubits par mode, la correspondance agit sur des blocs de qubits et non sur des bits isolés. L'exemple suivant renomme trois modes, chacun occupant deux qubits sous le code de Gray, et retrouve l'occupation d'origine à partir des comptages mesurés :

>>> # 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 chaîne de bits restaurée est ce que le circuit produit sans le renommage, lue dans l'ordre little-endian de Qiskit : le mode 0 contient le code de Gray du niveau deux (11), le mode 1 le niveau zéro (00) et le mode 2 le niveau un (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
Avertissement

Une spécification qui donne aux modes des cutoffs différents ne peut pas être combinée avec RelabelModes. Les cutoffs sont rattachés aux modes par leur position ; réordonner les modes placerait donc un cutoff sur le mauvais mode. OccupationB2QLayout refuse un circuit renommé dans ce cas :

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

Choisir les plugins de synthèse​

B2QSynthesis n'a aucun plugin par défaut ; le preset en enregistre un par type de porte. Les plugins disponibles sont gérés par B2QSynthesisPluginManager, qui les liste par le nom de la porte sur laquelle ils agissent :

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

Les deux méthodes intégrées sont Evolution.MapperFn et InitializeModes.Occupation. La première synthétise une évolution à partir de n'importe quelle fonction de mapper, ce qui permet de brancher un encodage personnalisé (voir Mapper les représentations d'opérateurs). La bibliothèque ne fournit aucun plugin pour une porte ModeRotation. Le preset la décompose d'abord dans l'espace bosonique en portes Evolution, que la première méthode traite ensuite, mais un plugin enregistré pour ModeRotation peut abaisser la porte directement ; voir Faire pivoter les modes bosoniques et Abaisser une rotation sans la décomposer. Tu peux enregistrer une méthode par son nom via l'argument config de B2QSynthesis, ou écrire directement un objet plugin dans B2QSynthesis.methods, comme le font les exemples précédents.

Poursuivre au niveau des qubits​

Le preset se termine toujours par une étape qubit, qui est le pass manager prédéfini propre à Qiskit. Les arguments nommés de generate_preset_occupation_pass_manager() sont transmis à qiskit.transpiler.generate_preset_pass_manager(), ce qui te permet de traduire vers un ensemble de portes de base ou de cibler directement un dispositif. Préfère optimization_level=0 pour garder le circuit proche de ce que les étapes bosoniques ont produit :

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

Deux autres choix sont courants. Affecte un PassManager vide à l'étape qubit lorsque tu veux montrer le circuit exactement tel que le pipeline l'a synthétisé, comme le font les exemples ci-dessus. Ou assemble toi-même le pipeline et ajoute n'importe quel pass manager de Qiskit comme étape qubit du MultiStagePassManager. Les étapes s'exécutent dans l'ordre où elles sont données ; l'étape qubit doit donc venir avant output, car les passes de Qiskit attendent un DAG et non un circuit terminé :

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

Prochaines étapes​