Saltar al contenido principal
Este proyecto está en fase beta. ¿Encontraste un error? Abre un issue en GitHub. ¿Tienes una pregunta o una idea? Pregunta en Discussions — ¡nos encantaría tu opinión!
¿Cómo es la calidad de la traducción en esta página?

Transpilar circuitos bosónicos

Esta guía explica cómo transpilar un BosonicCircuit a un QuantumCircuit estándar. Un circuito bosónico no lleva truncamiento, ni codificación, ni número de qubits (consulta Trabajar con circuitos bosónicos), así que el transpilador es donde se toman esas decisiones. El truncamiento y la codificación los describe un LevelSpec (consulta Describir el truncamiento con un LevelSpec), y el mapper que convierte operadores en operadores de qubits se describe en Asignar representaciones de operadores. El resultado es un QuantumCircuit corriente, por lo que cualquier simulador compatible con Qiskit puede ejecutarlo.

La guía usa un pequeño ejemplo de principio a fin: dos modos bosónicos, un estado con un bosón en cada uno y una única evolución temporal bajo un hamiltoniano de Bose-Hubbard cuyos términos de enlace y de sitio están en grupos separados (consulta Agrupar términos de operadores: usar la estructura del operador):

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

Etapas de la transpilación​

Como se explica en qiskit_bosons.transpiler, los pass managers predefinidos de este paquete dividen la transpilación en las siguientes etapas:

Entrada​

Convierte el circuito en una estructura de datos de grafo acíclico dirigido (DAG), la representación sobre la que trabaja cada pasada posterior.

Optimización​

Mantiene el circuito en el espacio bosónico. Estas pasadas usan la estructura de los operadores bosónicos, en particular sus grupos, mientras aún no existe información de qubits. B2BSynthesis es la pasada que descompone aquí las puertas, con un plugin para cada tipo de puerta: una fórmula de producto o qDRIFT para un Evolution, y la malla de Clements para un ModeRotation (consulta Sintetizar una puerta Evolution en el espacio bosónico para ver cómo se elige un método). RelabelModes reordena los modos. El preajuste descompone las rotaciones de modos en esta etapa y deja todo lo demás como está.

Disposición​

Asigna cada registro de modos bosónicos a un registro cuántico. Un modo bosónico generalmente ocupa varios qubits (dd de ellos con las codificaciones unarias, ⌈log⁡2d⌉\lceil \log_2 d \rceil con las compactas), por lo que el registro cuántico es más ancho que el registro de modos. OccupationB2QLayout deriva el ancho del registro a partir de un LevelSpec.

Síntesis​

Convierte las puertas bosónicas en operaciones de qubits, un plugin por tipo de puerta. La codificación ocurre en este paso: el plugin para una puerta Evolution asigna su operador a un operador de qubits, y el plugin para una puerta InitializeModes establece los qubits que codifican cada ocupación. Una barrera se extiende a todos los qubits de cada registro que tocaba.

Qubit​

Transpila el circuito de qubits resultante con los propios métodos de Qiskit, por ejemplo a un conjunto de puertas base o a un dispositivo. El preajuste siempre la incluye; de forma predeterminada es el pass manager predefinido de Qiskit, que sintetiza cada evolución en puertas (consulta Continuar en el nivel de qubits).

Salida​

Convierte la estructura de datos DAG interna de vuelta en un QuantumCircuit.

El truncamiento y la codificación no se almacenan en el circuito. Se entregan al transpilador, y la disposición, la preparación del estado y el mapper deben coincidir en ellos. El preajuste lo garantiza entregando el mismo LevelSpec a los tres.

Un ejemplo práctico​

generate_preset_occupation_pass_manager() construye el pipeline a partir de una especificación. Reducir el ejemplo bajo dos codificaciones solo cambia la especificación. El preajuste termina en la propia transpilación a nivel de qubits de Qiskit, que sintetizaría la evolución en puertas. Para ver el circuito exactamente como lo emiten las etapas bosónicas, vacía primero esa etapa:

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

Las dos codificaciones usan seis y cuatro qubits respectivamente (tres niveles por modo con la codificación unaria, dos qubits por modo con el código de Gray), pero la estructura del circuito es la misma: dos operaciones XGate preparan las ocupaciones, y un PauliEvolutionGate lleva la evolución.

Comparación con el flujo de trabajo tradicional​

La guía de circuitos bosónicos describe el flujo de trabajo tradicional, que asigna el hamiltoniano a un operador de qubits antes de construir el QuantumCircuit. Las dos rutas producen circuitos equivalentes. Se diferencian en lo que hay que rehacer cuando cambia el truncamiento. El circuito bosónico se construye una vez y se reduce con cada corte, mientras que la ruta que asigna primero necesita volver a asignar el operador y reconstruir el 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

El número de puertas entrelazantes depende de cómo sintetiza Qiskit la puerta de evolución, lo cual cambia entre versiones, así que represéntalo en una gráfica:

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

El número crece rápidamente con el corte, que es la razón para mantener el corte como parámetro de la transpilación y no del circuito. Las dos rutas son equivalentes aquí, pero solo la bosónica puede también intercalar la agrupación del operador con la fórmula de producto (consulta Sintetizar una puerta Evolution en el espacio bosónico) o entregar la codificación a una especificación distinta sin cambiar una línea del circuito.

Una especificación con un único corte deja abierto el número de modos, de modo que un solo pass manager sirve para circuitos de cualquier anchura. Una especificación con un corte por modo fija la anchura:

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

Construir el pipeline a mano​

El preset es un MultiStagePassManager con una entrada por etapa. Puedes ensamblar el mismo pipeline a partir de sus partes, que es el punto de partida de todas las personalizaciones que siguen:

>>> 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 función mapeadora recibe un operador y el ancho del registro que derivó el layout, y devuelve un SparseObservable. Como recibe el ancho, pasar num_qubits a occupation_encoding() hace que el mapeador compruebe que la especificación cabe en el registro, de modo que una discrepancia entre el layout y la codificación falla con un mensaje claro.

Flujos de trabajo avanzados​

Proporciona un layout explícito​

CustomB2QLayout sustituye la etapa de layout por un mapeo que tú proporcionas, desde cada registro bosónico del circuito hasta un registro cuántico. Es útil cuando el registro debe ser más ancho de lo que necesita la especificación, por ejemplo para dejar qubits inactivos para otros fines:

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

Un layout personalizado es responsabilidad tuya. Los plugins de síntesis siguen direccionando los qubits a través de la especificación, así que un registro que no coincide con los anchos de la especificación produce un circuito incorrecto o un error en lo profundo de un plugin. Un layout personalizado también omite la comprobación que OccupationB2QLayout realiza después de RelabelModes (consulta la siguiente sección).

Reordena los modos​

RelabelModes es un pase de la etapa de optimización que reordena los modos. La permutación se interpreta como un mapeo del índice de modo original al nuevo, y se registra en los metadatos del circuito. El reordenamiento cambia qué qubits ocupa cada modo y, por tanto, puede reducir la profundidad del circuito reducido.

Como el resultado se expresa en el nuevo orden de modos, las cadenas de bits medidas deben mapearse de vuelta antes de interpretarse. Con varios qubits por modo, el mapeo actúa sobre bloques de qubits y no sobre bits individuales. El siguiente ejemplo reetiqueta tres modos, cada uno ocupando dos qubits con el código Gray, y recupera la ocupación original a partir de los conteos medidos:

>>> # 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 cadena de bits restaurada es lo que produce el circuito sin el reetiquetado, leída en el orden little-endian de Qiskit: el modo 0 contiene el código Gray del nivel dos (11), el modo 1 contiene el nivel cero (00) y el modo 2 contiene el nivel 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
Advertencia

Una especificación que asigna distintos cutoffs a los modos no se puede combinar con RelabelModes. Los cutoffs se asocian a los modos por su posición, así que reordenar los modos pondría un cutoff en el modo equivocado. OccupationB2QLayout rechaza un circuito reetiquetado en ese caso:

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

Elige los plugins de síntesis​

B2QSynthesis no tiene plugins por defecto; el preset registra uno por tipo de compuerta. Los plugins disponibles los gestiona B2QSynthesisPluginManager, que los enumera por el nombre de la compuerta sobre la que actúan:

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

Los dos métodos integrados son Evolution.MapperFn e InitializeModes.Occupation. El primero sintetiza una evolución a partir de cualquier función mapeadora, que es la forma de conectar una codificación personalizada (consulta Mapear representaciones de operadores). La biblioteca no incluye ningún plugin para una compuerta ModeRotation. El preset primero la descompone en el espacio bosónico en compuertas Evolution, que el primer método ya sabe tratar, pero un plugin registrado para ModeRotation puede reducir la compuerta directamente; consulta Rotar modos bosónicos y Reducir una rotación sin descomponerla. Puedes registrar un método por nombre mediante el argumento config de B2QSynthesis, o escribir un objeto plugin directamente en B2QSynthesis.methods, como hacen los ejemplos anteriores.

Continúa a nivel de qubits​

El preset siempre termina en una etapa qubit, que es el pass manager preestablecido propio de Qiskit. Los argumentos de palabra clave de generate_preset_occupation_pass_manager() se pasan a qiskit.transpiler.generate_preset_pass_manager(), de modo que puedes traducir a un conjunto de compuertas base o apuntar directamente a un dispositivo. Es preferible usar optimization_level=0 para mantener el circuito cerca de lo que produjeron las etapas bosónicas:

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

Hay otras dos opciones habituales. Asigna un PassManager vacío a la etapa qubit cuando quieras mostrar el circuito exactamente como lo sintetizó el pipeline, como hacen los ejemplos anteriores. O ensambla tú mismo el pipeline y añade cualquier pass manager de Qiskit como etapa qubit del MultiStagePassManager. Las etapas se ejecutan en el orden en que se indican, así que la etapa qubit debe ir antes de output, porque los pases de Qiskit esperan un DAG y no un circuito terminado:

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

Próximos pasos​