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?

Trabaja con circuitos fermiónicos

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.

Esta guía explica cómo usar FermionicCircuit para implementar algoritmos cuánticos directamente en el espacio fermiónico, en lugar de mapear primero a qubits.

Por qué usar circuitos fermiónicos​

Considera el flujo de trabajo tradicional para implementar la evolución temporal de un hamiltoniano fermiónico en una arquitectura basada en qubits:

  1. Define el hamiltoniano fermiónico (por ejemplo, H=∑pqhpqcp†cqH = \sum_{pq} h_{pq} c^\dagger_p c_q).

  2. Mapéalo a un operador de qubits (por ejemplo, mediante el mapeo de Jordan-Wigner).

  3. Implementa la evolución temporal en el espacio de qubits (usando la trotterización u otros métodos).

Este enfoque tiene una limitación fundamental: el mapeo temprano a qubits descarta el conocimiento del problema sobre la estructura fermiónica. Los pases de optimización estándar no pueden aprovechar las simetrías fermiónicas ni los reordenamientos permitidos de términos, lo que da como resultado circuitos subóptimos con profundidades y números de puertas innecesarios.

El FermionicCircuit permite un mejor flujo de trabajo:

  1. Define el hamiltoniano fermiónico (por ejemplo, H=∑pqhpqcp†cqH = \sum_{pq} h_{pq} c^\dagger_p c_q).

  2. Implementa la evolución temporal directamente en el espacio fermiónico usando puertas fermiónicas.

  3. Mapea al espacio de qubits como parte del proceso de transpilación.

Este enfoque resuelve la limitación al mantener el conocimiento del problema en el espacio fermiónico, donde los pases de optimización pueden aprovechar la estructura fermiónica y las relaciones de conmutación para reducir la profundidad del circuito. El circuito fermiónico describe qué cálculo realizar, mientras que la transpilación se encarga de cómo mapearlo a qubits, lo que proporciona una separación de responsabilidades más limpia.

Indexación genérica de modos​

Tanto el módulo operators como el FermionicCircuit usan una indexación genérica basada en modos que no hace suposiciones sobre la naturaleza de los modos. Un modo es simplemente un índice abstracto que etiqueta un grado de libertad fermiónico, sin semántica inherente. Esta decisión de diseño refleja la del módulo operators y garantiza la máxima flexibilidad. Por ejemplo, puedes implementar la evolución temporal de cualquier operador que implemente el protocolo OperatorTrait, independientemente de su representación matemática.

Construye un circuito fermiónico​

El siguiente ejemplo implementa la evolución temporal descrita en la sección anterior. Construye un FermionicCircuit especificando el número de modos fermiónicos y luego añade puertas fermiónicas de qiskit_fermions.circuit.library para implementar la evolución temporal.

El ejemplo también muestra cómo incorporar conocimiento del dominio en un operador mediante la agrupación de términos del operador. Al asignar índices de grupo a los términos del hamiltoniano, conservas información estructural que los pases de optimización pueden aprovechar en toda la pila de transpilación.

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Create a circuit with 4 fermionic modes
>>> circuit = FermionicCircuit(4)
>>>
>>> # Define a simple fermionic Hamiltonian
>>> hamiltonian = FermionOperator.from_terms([
... ([cre(0), ann(2)], 0.5),
... ([cre(2), ann(0)], 0.5),
... ([cre(1), ann(3)], 0.5),
... ([cre(3), ann(1)], 0.5),
... ])
>>> # Add some problem structure by grouping our Hamiltonian terms
>>> hamiltonian.groups = [0, 0, 1, 1]
>>>
>>> # Add an evolution gate to implement exp(-i * t * H)
>>> evolution = Evolution(4, hamiltonian, time=1.0)
>>> circuit.append(evolution, circuit.register)

A simple `FermionicCircuit` with a single time evolution gate.

A decomposed `FermionicCircuit` with several time evolution gates.

Observa cómo la agrupación de términos del operador se conserva incluso en operaciones simples como la descomposición. Esto demuestra cómo fluye la información estructural a través de la pila de circuitos. Para entender la transpilación completa a qubits, consulta la guía Transpilación de circuitos fermiónicos.

Mantén las puertas separadas con una barrera​

Los pases de optimización fusionan puertas vecinas siempre que pueden. Por ejemplo, MergeOrbitalRotations combina una serie de puertas OrbitalRotation consecutivas en una sola, y MergeSlaterDeterminantPreparation incorpora un InitializeModes en la rotación que le sigue.

Usa FermionicCircuit.barrier() cuando quieras evitarlo. Una barrera no tiene efecto unitario, así que no cambia el estado que prepara tu circuito; solo marca un punto a través del cual los pases no fusionan. Llámala sin argumentos para abarcar todos los modos, o pasa los modos a los que quieras restringirla:

>>> from qiskit_fermions.circuit.library import OrbitalRotation
>>> import numpy as np
>>>
>>> rotations = FermionicCircuit(4)
>>> rotations.append(OrbitalRotation(np.eye(4, dtype=complex)), rotations.register)
>>> rotations.barrier()
>>> rotations.append(OrbitalRotation(np.eye(4, dtype=complex)), rotations.register)

A `FermionicCircuit` with a barrier separating two orbital rotations.

Las barreras sobreviven a la transpilación de fermión a qubit, por lo que también restringen la optimización a nivel de qubits que viene después. La guía Transpilar circuitos fermiónicos cubre esa etapa.

Repite un circuito​

Muchos algoritmos aplican el mismo bloque de puertas muchas veces. La evolución temporal es el caso habitual: un paso de Trotter se construye una vez y luego se repite hasta alcanzar el tiempo objetivo. Usa repeat() para construir el circuito repetido y compose() para unirlo a un prefijo de preparación de estado.

El siguiente ejemplo construye un paso de Trotter de segundo orden para un modelo de Fermi-Hubbard. El paso es simétrico: divide el término de salto en dos puertas OrbitalRotation de media duración que rodean la interacción local.

>>> import scipy.linalg
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>>
>>> norb, reps, time = 2, 3, 1.0
>>> num_modes = 2 * norb
>>> dt = time / reps
>>>
>>> hopping = np.zeros((norb, norb))
>>> hopping[0, 1] = hopping[1, 0] = -1.0
>>> interaction = FermionOperator.from_dict(
... {(cre(p), ann(p), cre(p + norb), ann(p + norb)): 2.0 for p in range(norb)}
... )
>>>
>>> def rotation(duration):
... propagator = scipy.linalg.expm(-1j * duration * hopping)
... # the same rotation acts on the alpha and beta halves of the register
... return scipy.linalg.block_diag(propagator, propagator)
>>>
>>> trotter_step = FermionicCircuit(num_modes)
>>> trotter_step.append(OrbitalRotation(rotation(dt / 2)), trotter_step.modes)
>>> trotter_step.append(
... Evolution(num_modes, interaction, dt, atomic=True), trotter_step.modes
... )
>>> trotter_step.append(OrbitalRotation(rotation(dt / 2)), trotter_step.modes)

Repetir el paso y componerlo sobre una referencia de Hartree-Fock da la evolución completa:

>>> evolution = FermionicCircuit(num_modes)
>>> evolution.append(InitializeModes([True, False, True, False]), evolution.modes)
>>> evolution.compose(trotter_step.repeat(reps), inplace=True)
>>> evolution.count_ops()["OrbitalRotation"]
6

A `FermionicCircuit` with repeated steps.

Las repeticiones se aplanan en el circuito devuelto en lugar de envolverse en una sola puerta, por lo que count_ops() cuenta dos rotaciones por paso. Ese aplanamiento es lo que hace visible para el transpilador la redundancia en los límites de los pasos: la rotación final de media duración de un paso y la inicial del siguiente son adyacentes, por lo que MergeOrbitalRotations las combina en una sola rotación.

>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import MergeOrbitalRotations
>>>
>>> merged = FermionicPassManager([MergeOrbitalRotations()]).run(evolution)
>>> merged.count_ops()["OrbitalRotation"]
4

A `FermionicCircuit` with repeated steps and merged orbital rotations.

El resultado cuesta una rotación por paso más una, que es lo que obtienes al fusionar esos medios pasos a mano. Escribir el paso en su forma simétrica natural y repetirlo produce el mismo circuito, así que la fusión se deja a la etapa de optimización.

Cuando necesites mantener separadas las repeticiones, pasa insert_barriers=True para colocar una barrier() entre ellas. Los pases de optimización no fusionan puertas a través de una barrera, así que se conservan todas las rotaciones:

>>> separated = trotter_step.repeat(reps, insert_barriers=True)
>>> FermionicPassManager([MergeOrbitalRotations()]).run(separated).count_ops()[
... "OrbitalRotation"
... ]
6

A `FermionicCircuit` with repeated steps separated by barriers.

Nota

El argumento reps de FermionicSuzukiTrotter es un concepto distinto. Subdivide una sola puerta Evolution en más factores, más cortos, mientras que repeat() duplica circuitos completos. Repetir un circuito que contiene una sola Evolution solo multiplica su tiempo total de evolución.

Mide modos fermiónicos​

measure_all() lee cada modo en un bit clásico propio, emparejando el modo i con el bit i. Usa measure() con tu propio ClassicalRegister para leer solo algunos de ellos.

>>> measured = trotter_step.measure_all(inplace=False)
>>> measured.count_ops()["measure"]
4

Pasar inplace=False devuelve una copia medida y deja intacto el original, lo que importa porque una medición no es unitaria: un circuito que contiene una ya no se puede aplicar a un vector de estado, así que conserva un circuito sin medir si también quieres simularlo.

Medir aquí, en lugar de en el QuantumCircuit transpilado, es lo que mantiene fáciles de leer las cadenas de bits muestreadas. Una medición fermiónica nombra el modo que lee, por lo que cada etapa que mueve modos (RelabelModes a nivel fermiónico, el layout y el enrutamiento a nivel de qubits) lleva consigo la medición, y la cadena de bits sigue indexada por el modo que preguntaste.

Nota

A diferencia de measure_all(), no se inserta ninguna barrier() antes de las mediciones. Llama tú mismo primero a barrier() si quieres una ahí.

Que un bit medido signifique lo que esperas depende de la codificación de fermión a qubit, que se elige durante la transpilación y no aquí. Con una codificación en la base de ocupación como Jordan-Wigner, cada modo lo porta un qubit y el bit es la ocupación de ese modo; consulta FermionicMeasure.

Transpila circuitos fermiónicos​

Para implementar el algoritmo cuántico representado por tu FermionicCircuit, debe transpilarse a un QuantumCircuit. Puedes aprender cómo hacerlo en la guía Transpilar circuitos fermiónicos.

Próximos pasos​