Transpilar un ansatz LUCJ
Los conceptos de esta guía solo están disponibles en la API de Python.
El ansatz de clúster unitario de Jastrow local (LUCJ) es una parametrización compacta y eficiente en hardware de una función de onda electrónica correlacionada. Es un miembro de la familia más general de clúster unitario de Jastrow (UCJ) y tiene la forma
donde es un estado de referencia (normalmente Hartree-Fock), cada es una rotación orbital y cada es un operador de Coulomb diagonal
con el operador número sobre el orbital espacial con espín .
ffsim construye el operador del ansatz; este paquete lo convierte en un circuito y reduce ese circuito a qubits. Esta guía trata de la segunda mitad de esa frase. La razón para hacer el recorrido es la propia reducción: las puertas de Qiskit propias de ffsim son específicas de la transformación de Jordan-Wigner, mientras que un FermionicCircuit no asume nada sobre su codificación de fermión a qubit, de modo que el mismo ansatz puede sintetizarse con la codificación que mejor se adapte al dispositivo de destino.
Por lo tanto, el operador del ansatz llega aquí ya hecho, y todo lo que sigue es lo que le ocurre después: en qué se descompone la puerta, cómo se reduce mediante Jordan-Wigner, cómo se ajusta a un mapa de acoplamiento de dispositivo y qué necesitaría aún una reducción distinta de Jordan-Wigner. Para construir, parametrizar o simular un ansatz UCJ en primer lugar, consulta las guías propias de ffsim.
La guía de la relación con ffsim explica cómo se reparten el trabajo los dos paquetes, y la guía de transpilación describe las etapas del flujo que se usan a continuación.
1. Obtener un operador LUCJ de ffsim
El ansatz se inicializa a partir de las amplitudes de un cálculo de clúster acoplado de simples y dobles (CCSD). Ejecuta Hartree-Fock restringido seguido de CCSD para una molécula de hidrógeno en la base 6-31g, usando PySCF para la química cuántica, y luego pasa las amplitudes a from_t_amplitudes() de ffsim. Esta función realiza una doble factorización de las amplitudes para obtener las matrices de Coulomb diagonales y las rotaciones orbitales de cada capa, y deriva una rotación orbital final opcional a partir de las amplitudes . El número de repeticiones es el que resulte de esa factorización; n_reps lo trunca, intercambiando precisión por un circuito menos profundo.
>>> import ffsim
>>> import pyscf
>>> import pyscf.cc
>>>
>>> # build the molecule and run Hartree-Fock
>>> mol = pyscf.gto.Mole()
>>> mol.build(
... atom=[["H", (0, 0, 0)], ["H", (0, 0, 0.74)]],
... basis="6-31g",
... symmetry="Dooh",
... verbose=0,
... )
<pyscf.gto.mole.Mole object at ...>
>>> scf = pyscf.scf.RHF(mol).run()
>>>
>>> norb = scf.mo_coeff.shape[1]
>>> nelec = (mol.nelec[0], mol.nelec[1])
>>>
>>> # run CCSD for the t-amplitudes, then factorize them into an ansatz operator
>>> ccsd = pyscf.cc.CCSD(scf).run()
>>> t1, t2 = ccsd.t1, ccsd.t2
>>>
>>> ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(t2, t1=t1, n_reps=2)
Aquí es donde el ansatz deja de ser una preocupación de ffsim. ffsim ofrece opciones que este paquete ni ve ni necesita conocer, como la factorización optimizada de forma variacional («comprimida») que hay detrás de optimize=True, o el empaquetado del vector de parámetros que maneja un optimizador variacional. Todas ellas producen el mismo tipo de operador, y todo lo que sigue funciona sin cambios con cualquiera de ellas.
2. Convertir el operador en un circuito fermiónico
La puerta UCJ envuelve el operador y lo expresa como un circuito sobre modos fermiónicos. La puerta es un unitario puro que no lleva referencia propia, así que antepón una puerta InitializeModes (construida con from_hartree_fock()) para proporcionar la referencia de Hartree-Fock sobre la que se aplica el ansatz.
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, UCJ
>>>
>>> ansatz = UCJ(ucj_op)
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(ansatz, circuit.modes)
Descomponer el circuito revela su anatomía, y esta es la representación sobre la que opera el resto de la guía. La puerta InitializeModes prepara el determinante de referencia, y cada capa del ansatz aporta una OrbitalRotation , luego (una Evolution del operador de Coulomb diagonal ), luego , con una OrbitalRotation final al final. Las rotaciones orbitales actúan por sector de espín, por lo que cada una se coloca de forma independiente sobre los modos alfa 0..norb y los modos beta norb..2*norb.
>>> circuit.decompose().draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

Todas las puertas de esa descomposición siguen siendo fermiónicas: OrbitalRotation y Evolution están definidas sobre modos, y todavía no ha aparecido ningún operador de qubit ni de Pauli. Eso es lo que deja abierta la codificación, y es el estado en el que permanece el ansatz hasta que la etapa de síntesis elige una.
Cada capa termina con y la siguiente comienza con , así que las puertas OrbitalRotation adyacentes podrían fusionarse en una sola rotación. Un paso de transpilación que realice esta fusión es un desarrollo futuro previsto.
3. Elegir una codificación de fermión a qubit
El circuito sigue siendo fermiónico, y ese es el punto en el que este paquete se gana su lugar en el flujo de trabajo. Un FermionicCircuit no asume nada sobre cómo los modos se convierten en qubits, así que la codificación es una decisión de la etapa de síntesis y no algo incorporado en el ansatz. Eso es lo que este paquete añade a las puertas de Qiskit propias de ffsim, que son específicas de la transformación de Jordan-Wigner.
Solo para Jordan-Wigner no necesitarías este desvío: ffsim incluye UCJOpSpinBalancedJW y sus hermanas, que llevan un operador UCJ a un circuito de qubits por sí solas. La razón para pasar el ansatz por un circuito fermiónico es todo lo demás que puede ofrecerte una codificación. Las codificaciones locales, por ejemplo, gastan qubits adicionales para acotar el peso de Pauli de cada término, lo que las guías de conjuntos de flujo 1D y 2D aprovechan para que la profundidad de dos qubits de un paso de Trotter sea independiente del tamaño del sistema.
Para un ansatz UCJ en concreto, la etapa de síntesis tiene que cubrir dos tipos de puerta:
-
La evolución de Coulomb diagonal.
Evolutionya es independiente de la codificación:MapperFnEvolutionSynthesistoma una función de mapeo, de modo que las capas se reducen mediante cualquier codificación que puedas expresar como tal. Las guías de conjuntos de flujo muestran cómo escribir una. -
Las rotaciones orbitales y la preparación del determinante de Slater. Ni una
OrbitalRotationni laPrepareSlaterDeterminanten la que se fusionan laInitializeModesinicial y la rotación (véase más abajo) se mapean a qubits como tales. El pasoF2FSynthesisprimero las descompone, en el espacio fermiónico, en puertasEvolutionde uno o dos modos cada una (y, para la preparación, unaInitializeModesde la referencia), de modo que cualquier codificación que pueda mapear unaEvolutionlas maneja. Con Jordan-Wigner, los factores entre modos adyacentes se convierten en objetosXXPlusYYGateen la etapa de qubits. -
La inicialización. El plugin de
InitializeModesno está parametrizado por una codificación: documenta su suposición de una codificación en base de ocupación con un mapeo de modo a qubit 1 a 1, y emite puertas directamente en esa base. Una codificación distinta necesita su propio plugin para ello, y todavía no se incluye ninguno.
Cómo transpilar circuitos UCJ con una codificación distinta de Jordan-Wigner es una cuestión de investigación abierta, y este paquete ofrece el marco en el que desarrollar un flujo así. El resto de esta guía construye, como ejemplo, el flujo de transpilador para la codificación de Jordan-Wigner, equivalente al que produce generate_preset_jw_pass_manager().
4. Reducirlo mediante Jordan-Wigner
La reducción ocurre en F2QSynthesis, que funciona como HighLevelSynthesis de Qiskit: recorre las instrucciones fermiónicas y entrega cada una a un plugin registrado para ese nombre de puerta. Su atributo F2QSynthesis.methods contiene un F2QSynthesisPlugin por nombre de puerta, así que construir la etapa significa elegir un plugin para cada puerta fermiónica que contiene el circuito:
>>> from qiskit_fermions.mappers.library import jordan_wigner
>>> from qiskit_fermions.transpiler.passes import F2QSynthesis
>>> from qiskit_fermions.transpiler.passes.synthesis import (
... MapperFnEvolutionSynthesis,
... TrivialOccupationInitializeModesSynthesis,
... )
>>>
>>> synthesis = F2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(jordan_wigner)
>>> synthesis.methods["InitializeModes"] = TrivialOccupationInitializeModesSynthesis()
Asignar instancias de esta manera es también como un plugin que hayas escrito tú mismo entra en el flujo, sin tener que registrarlo antes en ningún sitio.
Aquí es donde se fija la codificación, de las dos maneras que describía la sección 3: de forma explícita, entregando jordan_wigner() a MapperFnEvolutionSynthesis, e implícita, porque el otro plugin ya la asume.
Esa etapa encaja luego en un MultiStagePassManager cuyas etapas se ejecutan en un orden fijo:
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.transpiler import generate_preset_pass_manager
>>> from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig
>>>
>>> from qiskit_fermions.transpiler import (
... FermionicCircuitToDAG,
... FermionicPassManager,
... QuantumDAGToCircuit,
... )
>>> from qiskit_fermions.transpiler.passes import (
... F2FSynthesis,
... HopEvolutionSynthesis,
... MergeOrbitalRotations,
... MergeSlaterDeterminantPreparation,
... TrivialF2QLayout,
... )
>>>
>>> pm = MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... optimization=FermionicPassManager(
... [
... MergeOrbitalRotations(),
... MergeSlaterDeterminantPreparation(),
... F2FSynthesis(
... {"OrbitalRotation": "Givens", "PrepareSlaterDeterminant": "Givens"}
... ),
... ]
... ),
... layout=FermionicPassManager(TrivialF2QLayout()),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(
... hls_config=HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
... ),
... output=QuantumDAGToCircuit(),
... )
La etapa de optimización se ejecuta antes de la síntesis, sobre el circuito fermiónico, y eso es lo que la hace valiosa: reescribe puertas mientras todavía son fermiónicas e independientes de la codificación, de modo que los ahorros se trasladan a la codificación que venga después. MergeOrbitalRotations colapsa cada serie de puertas OrbitalRotation consecutivas en una sola, lo cual importa para un ansatz UCJ porque cada capa termina con y la siguiente comienza con . A continuación, MergeSlaterDeterminantPreparation fusiona la InitializeModes inicial y la rotación que la sigue en una sola PrepareSlaterDeterminant, que la síntesis reduce mediante la descomposición de Slater reducida, más económica, en lugar de una rotación cuadrada completa. El orden importa: fusionar primero las rotaciones deja expuesta la única rotación inmediatamente después de la inicialización, que la fusión de Slater contrae luego en ella. F2FSynthesis va al final, porque descompone cualquier rotación que quede, y hacerlo antes dejaría a los dos pasos de fusión sin nada que fusionar.
Las etapas restantes son infraestructura. input y output convierten entre circuitos y DAG, layout asigna modos fermiónicos a qubits (TrivialF2QLayout mapea el modo i al qubit i), y qubit es un gestor de pases de Qiskit corriente que toma el relevo cuando ya no queda nada fermiónico.
Ejecutarlo requiere un paso preparatorio: primero hay que descomponer la puerta compuesta UCJ en sus puertas primitivas, para que la etapa de optimización pueda ver las rotaciones y evoluciones individuales.
>>> transpiled = pm.run(circuit.decompose())
>>> print(dict(sorted(transpiled.count_ops().items())))
{'rz': 14, 'rzz': 12, 'x': 2, 'xx_plus_yy': 28}
Sin un dispositivo de destino, esto se mapea a 2 * norb qubits suponiendo conectividad total; las rotaciones orbitales se sintetizan en objetos XXPlusYYGate y las evoluciones de Coulomb diagonales en objetos RZZGate:
>>> transpiled.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
Este flujo es exactamente lo que ensambla generate_preset_jw_pass_manager(), así que en la práctica recurre al preajuste y construye las etapas a mano cuando necesites cambiar alguna de ellas:
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> preset = generate_preset_jw_pass_manager()
>>> preset.run(circuit.decompose()).count_ops() == transpiled.count_ops()
True
5. Ajustarse a la topología del dispositivo
Un dispositivo real tiene un mapa de acoplamiento de qubits restringido, y el ansatz LUCJ está diseñado para ajustarse a él. Las interacciones del mismo espín (pairs_aa) forman dos cadenas lineales y las interacciones alfa-beta (pairs_ab) las unen. generate_lucj_pass_manager() de ffsim construye un flujo de qubits consciente del dispositivo para esta estructura, y devuelve el subconjunto de pairs_ab que el hardware puede acomodar realmente. Encaja ese flujo en la etapa qubit del preajuste conservando la síntesis de fermión a qubit propia del paquete:
>>> from ffsim.qiskit import generate_lucj_pass_manager
>>> from qiskit.providers.fake_provider import GenericBackendV2
>>> from qiskit.transpiler import CouplingMap
>>>
>>> # a heavy-hex device coupling map (any BackendV2 works, e.g. a real fake_provider backend)
>>> coupling_map = CouplingMap.from_heavy_hex(5)
>>> backend = GenericBackendV2(
... num_qubits=coupling_map.size(),
... basis_gates=["cp", "xx_plus_yy", "p", "x", "swap"],
... coupling_map=coupling_map,
... )
>>>
>>> # nearest-neighbor same-spin chain; let the pass manager choose the alpha-beta pairs
>>> pairs_aa = [(p, p + 1) for p in range(norb - 1)]
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> hls_config = HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
>>> pm.qubit, allowed_pairs_ab = generate_lucj_pass_manager(
... backend,
... norb,
... "heavy-hex",
... (pairs_aa, None),
... optimization_level=3,
... seed_transpiler=0,
... hls_config=hls_config,
... )
>>>
>>> # the alpha-beta interactions the heavy-hex connectivity can implement
>>> print(allowed_pairs_ab)
[(0, 0)]
Esa composición es la clave: la etapa qubit es consciente del dispositivo, mientras que la etapa de síntesis de fermión a qubit que la precede sigue siendo la propia de este paquete y, por tanto, sigue siendo reemplazable. Reemplazar la etapa qubit sí descarta lo que el preajuste registra allí, que es la HopEvolutionSynthesis que convierte los factores de Givens de una rotación orbital en objetos XXPlusYYGate. Pásala a través de hls_config como antes, o bien habrá que alcanzar la base del dispositivo sin ella.
Con pm.qubit ya configurado con el flujo consciente del dispositivo, ejecutar el gestor de pases coloca el circuito sobre los qubits del backend y lo enruta al mapa de acoplamiento. Para el ansatz sin restricciones del paso 1, cuyo operador de Coulomb diagonal aún contiene términos alfa-beta que el hardware no puede alcanzar directamente, el enrutador tiene que insertar muchas puertas SWAP para conectarlos:
>>> naive = pm.run(circuit.decompose())
>>> naive.num_qubits # laid out on the full heavy-hex device register
57
>>> naive_swaps = naive.count_ops()["swap"]
>>> naive_swaps # many SWAPs to bridge the unreachable alpha-beta interactions
33
Restringir el ansatz a las interacciones implementables en el hardware
La solución es devolver allowed_pairs_ab a la construcción del ansatz, mediante el argumento interaction_pairs de from_t_amplitudes(), de modo que el operador de Coulomb diagonal solo contenga términos alfa-beta que el mapa de acoplamiento pueda implementar directamente. Entonces el ansatz se ajusta a la topología del dispositivo y el enrutador apenas tiene que tocarlo:
>>> restricted = UCJ(
... ffsim.UCJOpSpinBalanced.from_t_amplitudes(
... t2, t1=t1, n_reps=2, interaction_pairs=(pairs_aa, allowed_pairs_ab)
... )
... )
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(restricted, circuit.modes)
>>>
>>> transpiled = pm.run(circuit.decompose())
>>> restricted_swaps = transpiled.count_ops()["swap"]
>>> restricted_swaps # far fewer routing SWAPs than the unrestricted ansatz
4
Dibujar solo los qubits activos (idle_wires=False) muestra el circuito restringido a las dos cadenas de espín y al puente alfa-beta, expresado en las puertas base del dispositivo. La disposición y el enrutamiento dispersan los modos lógicos por los qubits físicos del dispositivo, así que pasa un wire_order tomado de la disposición final del circuito (final_index_layout() enumera el qubit físico en el que terminó cada qubit de entrada, en el orden de los qubits de entrada) para dibujar los cables de nuevo en el orden original de los modos:
>>> wire_order = transpiled.layout.final_index_layout(filter_ancillas=False)
>>> transpiled.draw("mpl", idle_wires=False, fold=-1, wire_order=wire_order)
<Figure size ... with 1 Axes>

El número exacto de puertas y la profundidad tras la disposición dependen de los pasos de enrutamiento y optimización y del dispositivo elegido, por lo que no se reproducen aquí. Lo esencial es el codiseño; expresar el ansatz con una cadena pairs_aa de vecinos más cercanos y puentes pairs_ab filtrados por el hardware mantiene el circuito sintetizado cerca de la topología del dispositivo, minimizando la sobrecarga de enrutamiento (puertas SWAP insertadas). Consulta FermionicSlaterGivensDecomposition para una opción de síntesis relacionada (minimize_2q_gate_count), que intercambia el número de puertas de dos qubits por la profundidad tras el enrutamiento.
Próximos pasos
-
Aprende cómo funcionan las puertas individuales en la documentación de
qiskit_fermions.circuit.libraryy en la guía de circuitos fermiónicos. -
Explora la guía explicativa de operadores para entender cómo construir directamente operadores fermiónicos y hamiltonianos.
-
Consulta cómo se mapea un circuito fermiónico a qubits en la guía de transpilación.
-
Lee la documentación de
F2QSynthesispara conocer la otra manera de configurar la etapa de síntesis: un diccionarioconfigque selecciona plugins por los nombres cortos con los que se registran en sus puntos de entradaqiskit_fermions.transpiler.synthesis, en lugar de por instancia. -
Lee la guía de la relación con ffsim para entender cómo se reparten el trabajo los dos paquetes y qué protocolos permiten que ffsim simule los circuitos de este paquete.
