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?

Optimiza la preparación de determinantes de Slater

Importante

Los conceptos de esta guía solo están disponibles actualmente en la API de Python. Una funcionalidad equivalente estará disponible en la API de C en una versión futura.

Una puerta InitializeModes declara una ocupación de modos de referencia; una puerta OrbitalRotation rota la base de una sola partícula. Colocadas una a continuación de la otra, las dos puertas preparan un determinante de Slater; los modos parten del determinante de referencia y luego se rotan hacia los orbitales de destino. La puerta PrepareSlaterDeterminant recoge esa composición y, como lleva la ocupación de referencia, al transpilarla se puede usar la givens_decomposition_slater() rectangular, que realiza solo los orbitales ocupados, en lugar de la descomposición cuadrada completa que requiere una OrbitalRotation por sí sola. Es una síntesis estrictamente más económica (consulta el beneficio al final de esta guía).

El pase de transpilador MergeSlaterDeterminantPreparation detecta el patrón InitializeModes seguido de OrbitalRotation en un FermionicCircuit y lo reescribe como puertas PrepareSlaterDeterminant, de modo que una etapa de síntesis posterior aproveche automáticamente la descomposición reducida. En simulación, la reescritura no cambia el estado: PrepareSlaterDeterminant es validar y luego rotar (valida la ocupación de referencia y después aplica la rotación, igual que hacen las dos puertas por separado), así que la fusión habilita la síntesis más económica sin cambiar el estado preparado.

A lo largo de esta guía, cada circuito se dibuja antes y después del pase, lado a lado: la entrada a la izquierda y, a la derecha, el resultado de ejecutar el pase sobre ella. Dos funciones auxiliares se encargan de ello. merge ejecuta el pase sobre una copia del circuito (a través de su FermionicDAGCircuit) y convierte el resultado de nuevo en un FermionicCircuit dibujable, y draw_merge dibuja ambas mitades en una única figura de antes y después:

>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>>
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG
>>> from qiskit_fermions.transpiler.converters import FermionicDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import MergeSlaterDeterminantPreparation
>>>
>>> def merge(circuit):
... dag = FermionicCircuitToDAG().run(circuit)
... merged = MergeSlaterDeterminantPreparation().run(dag)
... return FermionicDAGToCircuit().run(merged)
>>>
>>> def natural_size(circuit):
... # the figure size Qiskit picks for a circuit on its own (discard the probe figure)
... probe = circuit.draw("mpl")
... size = probe.get_size_inches()
... plt.close(probe)
... return size
>>>
>>> def draw_merge(circuit):
... merged = merge(circuit)
... # let Qiskit size each half naturally, then lay them out side by side
... (bw, bh), (aw, ah) = natural_size(circuit), natural_size(merged)
... fig, (before, after) = plt.subplots(1, 2, figsize=(bw + aw, max(bh, ah)))
... circuit.draw("mpl", ax=before)
... merged.draw("mpl", ax=after)
... before.set_title("before")
... after.set_title("after")
... return fig

Qué se fusiona​

El pase reconoce tres patrones, todos basados en la convención de modos de espín por bloques que usan InitializeModes y OrbitalRotation. Para un sistema con norb orbitales espaciales, los modos 0..norb son el sector de espín alfa y norb..2*norb son el sector de espín beta. La preparación del determinante de Slater se hace por sector de espín porque cada sector tiene su propia forma (electrones, orbitales).

Registro completo (o sin espín)​

El patrón más simple es usar InitializeModes, seguido inmediatamente de una OrbitalRotation sobre los mismos modos. Este es el caso sin espín (un determinante sobre todos los modos) y también un circuito con espín cuya rotación abarca ambos sectores a la vez. Las dos puertas se fusionan en una única PrepareSlaterDeterminant:

>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A mode initialization and rotation fusing into one Slater determinant preparation.

Por sector de espín​

Es la misma forma, pero restringida a una mitad de espín. Aquí, dos pares independientes de inicializar y luego rotar, uno por sector, se fusionan cada uno por su cuenta en una PrepareSlaterDeterminant:

>>> circuit = FermionicCircuit(6) # norb = 3: alpha modes 0..3, beta modes 3..6
>>> circuit.append(InitializeModes([1, 1, 0]), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(InitializeModes([1, 0, 0]), circuit.modes[3:])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

Two per-sector initialize-and-rotate pairs, each fusing into its own preparation.

Inicialización global, rotaciones por espín​

Esto implica una única InitializeModes de registro completo (2 * norb) seguida de dos acciones OrbitalRotation, una sobre cada mitad de espín contigua. El pase divide la ocupación global en el límite entre sectores y emite dos puertas PrepareSlaterDeterminant. Las dos rotaciones pueden aparecer en cualquier orden.

Esta es la forma que produce el flujo de trabajo de clúster unitario local de Jastrow (LUCJ) (consulta la guía de LUCJ); el usuario coloca una InitializeModes (normalmente la referencia de Hartree-Fock mediante from_hartree_fock()) al principio del circuito y añade un ansatz UCJ, cuyas dos primeras rotaciones de orbitales por espín siguen directamente a la inicialización una vez descompuesto el ansatz.

>>> circuit = FermionicCircuit(6) # norb = 3, nelec = (2, 1)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with two per-spin rotations fusing into two preparations.

Este patrón también se activa cuando solo se rota una mitad de espín mediante una puerta que sigue directamente a la inicialización, la forma que resulta cuando la rotación de orbitales de un solo sector de espín queda al principio del circuito. La mitad rotada se convierte en una PrepareSlaterDeterminant con su rotación, y la otra mitad se prepara con una rotación identidad. La preparación identidad se sintetiza en las puertas X de referencia que la InitializeModes habría emitido de todos modos para esa mitad, así que rellenarla no cuesta puertas adicionales y aun así habilita la síntesis de Slater reducida en la mitad rotada:

>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with a single per-spin rotation fusing into two preparations.

Qué queda sin tocar​

El pase es deliberadamente conservador. Solo fusiona cuando las dos puertas realmente se componen en una preparación de determinante de Slater, y copia todo lo demás sin cambios. «Seguida inmediatamente» se entiende sobre el grafo de flujo de datos del circuito. La OrbitalRotation se fusiona solo cuando la InitializeModes es su único predecesor en todos sus modos. En las figuras siguientes, el antes y el después son idénticos: no se fusionó nada.

Una operación intermedia​

Si alguna puerta toca los modos entre la inicialización y la rotación, ya no se componen en una preparación pura, así que no se fusiona nada:

>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, ann, cre
>>>
>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> number_op = FermionOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> circuit.append(Evolution(4, number_op, time=0.5), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

An operation between the initialization and rotation blocks the merge.

Una rotación parcial que no es una mitad de espín​

Una rotación que cubre exactamente una mitad de espín contigua de una inicialización de registro completo sí se fusiona (consulta la sección anterior). Pero una rotación sobre cualquier otro conjunto parcial de modos (menos modos que la inicialización y que no sea una de sus dos mitades de espín, como un subrango que cruza el límite entre sectores) no es una preparación por sector. Fusionarla descartaría silenciosamente el papel de la inicialización sobre los modos que deja fuera, así que el pase deja ambas puertas en su sitio:

>>> circuit = FermionicCircuit(4) # norb = 2: the only spin halves are modes [0, 1] and [2, 3]
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A partial rotation that is not a spin half is left unmerged.

Por qué importa: el beneficio de la síntesis​

La fusión merece la pena porque PrepareSlaterDeterminant se sintetiza en muchas menos puertas que la OrbitalRotation que absorbe. Un determinante de mm electrones sobre nn orbitales necesita como máximo m(n−m)m(n-m) rotaciones de dos qubits y ninguna puerta de fase diagonal, frente a las n(n−1)/2n(n-1)/2 rotaciones más nn fases de la rotación de orbitales cuadrada completa.

El gestor de pases preestablecido de Jordan-Wigner ejecuta MergeSlaterDeterminantPreparation en su etapa de optimización, así que esta reducción ocurre automáticamente. Transpila una preparación de dos electrones y seis orbitales hasta un QuantumCircuit y cuenta sus puertas:

Nota

El preset ejecuta MergeOrbitalRotations inmediatamente antes de MergeSlaterDeterminantPreparation. Esto importa cuando una serie de puertas OrbitalRotation consecutivas sigue a la inicialización. El pase anterior primero colapsa la serie en una sola rotación, que MergeSlaterDeterminantPreparation contrae luego con la inicialización en una única PrepareSlaterDeterminant. Sin ese orden, solo se absorbería la primera rotación de la serie y el resto se sintetizaría con la descomposición cuadrada completa.

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> # a reproducible random 6 x 6 orbital rotation
>>> rng = np.random.default_rng(1)
>>> z = rng.standard_normal((6, 6)) + 1j * rng.standard_normal((6, 6))
>>> q, r = np.linalg.qr(z)
>>> rotation = q * (np.diagonal(r) / np.abs(np.diagonal(r)))
>>>
>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes([1, 1, 0, 0, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(rotation), circuit.modes)
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> prepared = pm.run(circuit)
>>> print(dict(sorted(prepared.count_ops().items())))
{'x': 2, 'xx_plus_yy': 8}
>>> prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized merged Slater determinant preparation.

Compáralo con sintetizar la misma rotación de orbitales por sí sola. Sin una inicialización previa no hay nada que fusionar, así que el preset recurre a la descomposición cuadrada completa, que es más profunda y lleva puertas de fase diagonales:

>>> reference = FermionicCircuit(6)
>>> reference.append(OrbitalRotation(rotation), reference.modes)
>>> reference_prepared = pm.run(reference)
>>> print(dict(sorted(reference_prepared.count_ops().items())))
{'rz': 6, 'xx_plus_yy': 15}
>>> reference_prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized bare orbital rotation, with its extra phase gates.

La descomposición reducida elimina las seis puertas de fase y casi reduce a la mitad el número de rotaciones de dos qubits. El determinante preparado es correcto salvo una fase global (físicamente irrelevante para la preparación de estados), que es lo que habrían fijado las puertas de fase.

Dos formas de descomponer la puerta​

Una PrepareSlaterDeterminant se puede descomponer de dos maneras distintas, y no son intercambiables:

decompose()​

Expande la puerta en su definición: la InitializeModes de la referencia seguida de la OrbitalRotation completa. Esto equivale a la puerta como operador, para cualquier estado de entrada.

PrepareSlaterDeterminant.synthesis, aplicado por F2FSynthesis​

Sustituye la puerta por la descomposición reducida de esta guía. Esto equivale a la puerta solo como preparación de estado: produce el mismo determinante, salvo una fase global, cuando los modos parten del vacío. Es más económica precisamente porque lo asume.

Usa la síntesis cuando la puerta prepare un estado sobre el vacío, que es para lo que sirve la puerta y lo que hace el transpilador por ti. Usa decompose() cuando la puerta tenga que seguir actuando como operador, por ejemplo porque otras puertas la preceden sobre los mismos modos, o cuando quieras inspeccionar o simular las puertas simples. El pase F2FSynthesis no puede saber en qué caso se encuentra, así que corresponde al circuito que le des preparar el determinante a partir del vacío.

Simular un circuito con ffsim no necesita ninguna de las dos: cada puerta aplica su unitaria completa, así que un circuito que contiene una PrepareSlaterDeterminant se simula tal cual y se mantiene en la ruta rápida para las puertas OrbitalRotation que la rodean. Descomponer solo la puerta de Slater con circuit.decompose("PrepareSlaterDeterminant") también deja esas rotaciones enteras.

Véase también

PrepareSlaterDeterminant, InitializeModes, OrbitalRotation, MergeOrbitalRotations, F2FSynthesis, FermionicSlaterGivensDecomposition, y la guía de LUCJ para el flujo de trabajo que produce el patrón de inicialización global.