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?

Agrupar términos del operador: usar la estructura del operador

Un BosonOperator puede etiquetar cada uno de sus términos con un índice de grupo. Las etiquetas no tienen significado propio: solo indican qué términos van juntos. Lo que significan lo decide quien las consume. En este paquete los consumidores son la puerta de evolución temporal, que se divide en un factor por grupo, y el pase aleatorizado qDRIFT, que muestrea grupos completos.

Esta guía muestra cómo asignar grupos, cómo comprobar las propiedades en las que se apoya un consumidor y cómo organizar el operador para que seleccionar unos pocos grupos de uno grande siga siendo barato.

Nota

Este paquete no incluye ninguna estrategia de agrupación automática. Tú asignas los índices, a partir de lo que sabes de tu modelo.

Asignar grupos​

El ejemplo de referencia es una cadena de Bose-Hubbard de tres sitios. Cada enlace aporta un par de salto, y las interacciones en el sitio forman una pieza diagonal. La forma más sencilla de construir un operador agrupado es from_terms_with_groups(), que toma cada término junto con su índice de grupo como una tripleta (actions, coefficient, group). Decides el grupo en el momento de escribir el término, cuando aún sabes por qué pertenece ahí:

>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> bonds = [(0, 1), (1, 2)]
>>> terms = []
>>> for group, (left, right) in enumerate(bonds): # one group per bond
... terms.append(([cre(left), ann(right)], -1.0, group))
... terms.append(([cre(right), ann(left)], -1.0, group))
>>> for site in range(3): # the on-site interactions share one group
... terms.append(([cre(site), cre(site), ann(site), ann(site)], 1.0, len(bonds)))
>>> hamiltonian = BosonOperator.from_terms_with_groups(terms)
>>> len(hamiltonian), hamiltonian.num_groups()
(7, 3)

Los términos se almacenan en el orden en que los das, así que los índices de grupo no pueden acabar asociados a términos equivocados.

Un operador que ya existe hay que agruparlo a posteriori, asignando el atributo groups. Ahí hay que tener cuidado, porque from_dict() no especifica el orden en que almacena sus términos. Escribir la lista por posición puede entonces asociar un índice al término equivocado. Deriva en su lugar cada índice del propio término:

>>> existing = BosonOperator.from_dict({tuple(actions): coeff for actions, coeff, _ in terms})
>>> existing.groups = [
... len(bonds)
... if len({mode for _, mode in term}) == 1
... else bonds.index(tuple(sorted({mode for _, mode in term})))
... for term, _ in existing.iter_terms()
... ]
>>> existing.num_groups(), existing.equiv(hamiltonian)
(3, True)

El número de índices tiene que coincidir con el número de términos. Cualquier otra cosa se rechaza, porque una lista demasiado corta descartaría en silencio los términos finales allí donde se iteran los términos junto con sus grupos:

>>> try:
... hamiltonian.groups = [0, 1]
... except ValueError as error:
... print(error)
expected one group index per term, but got 2 for 7 terms

Comprobar en qué se apoya el consumidor​

Tres funciones de qiskit_bosons.operators.terms responden cada una a una pregunta concreta sobre una agrupación existente. Ninguna dice que una agrupación sea “correcta”, porque la corrección es relativa al consumidor.

¿Es hermítico cada grupo? Esta es la propiedad que más importa. La evolución temporal de un grupo solo es unitaria si el grupo es hermítico, y un término de salto aislado bi†bjb^\dagger_i b_j no lo es. Solo el par con su adjunto es hermítico:

>>> from qiskit_bosons.operators.terms import groups_are_hermitian
>>> groups_are_hermitian(hamiltonian)
[True, True, True]

Una agrupación que divide un par de salto se detecta de inmediato:

>>> split = BosonOperator.from_terms_with_groups(
... [(actions, coeff, index) for index, (actions, coeff, _) in enumerate(terms)]
... ) # one term per group
>>> [bool(flag) for flag in groups_are_hermitian(split)]
[False, False, False, False, True, True, True]

Los cuatro términos de salto quedan detectados. Las tres entradas True son los términos locales bi†bi†bibib^\dagger_i b^\dagger_i b_i b_i, que son hermíticos por sí mismos. Un modo puede repetirse legítimamente dentro de un término, y por ello no se descarta nada. Las marcas siguen el orden de terms, por lo que cada una puede leerse frente al término que describe.

¿Son uniformes los coeficientes? Una fórmula de producto aleatorizada pondera un grupo según la magnitud media de sus coeficientes, lo cual describe fielmente el grupo solo si las magnitudes coinciden:

>>> from qiskit_bosons.operators.terms import group_coeff_means, groups_have_uniform_coeffs
>>> groups_have_uniform_coeffs(hamiltonian)
[True, True, True]
>>> group_coeff_means(hamiltonian)
[1.0, 1.0, 1.0]

Ninguna de las dos comprobaciones implica la otra: unos coeficientes uniformes no hacen hermítico a un grupo, y un grupo hermítico puede mezclar magnitudes. Por defecto se comparan las magnitudes, porque eso es lo que promedia group_coeff_means(). Un par hermítico con coeficientes complejos tiene magnitudes iguales pero coeficientes distintos, así que solo cumple la forma por defecto:

>>> conjugate = BosonOperator.from_terms_with_groups(
... [([cre(0), ann(1)], 1.0j, 0), ([cre(1), ann(0)], -1.0j, 0)]
... )
>>> groups_have_uniform_coeffs(conjugate)
[True]
>>> groups_have_uniform_coeffs(conjugate, abs=False)
[False]
Nota

groups_are_hermitian() se basa en is_hermitian(), que compara un operador con su adjunto tras el ordenamiento normal. Es una identidad del álgebra sin truncar, por lo que un grupo que la cumple es hermítico como operador bosónico. Consulta Principios de diseño de las representaciones de operadores para saber cómo se ordenan y comparan los productos de operadores de escalera.

Ordena los términos por grupo​

split_out_groups() devuelve un operador por cada grupo solicitado. Cuando los términos de un grupo están dispersos por el operador, tiene que recorrer todos los términos para encontrarlos. Cuando cada grupo forma una serie contigua, en cambio busca los límites del grupo mediante búsqueda binaria, de modo que el coste de una consulta depende del número de grupos solicitados y no del número de términos almacenados.

group_order() produce esa disposición. Es una ordenación estable por índice de grupo: el valor del operador no cambia y los términos de un grupo conservan su orden relativo.

>>> from qiskit_bosons.operators.terms import group_order
>>> scattered = BosonOperator.from_terms_with_groups(
... [(actions, coeff, group) for (actions, coeff, _), group in zip(terms, [1, 0, 1, 0, 2, 2, 2])]
... )
>>> ordered = group_order(scattered)
>>> ordered.groups
[0, 0, 1, 1, 2, 2, 2]
>>> ordered.equiv(scattered)
True

Ambas disposiciones devuelven los mismos operadores; solo difiere el coste:

>>> request = [2, 0]
>>> all(
... a.equiv(b)
... for a, b in zip(
... scattered.split_out_groups(request), ordered.split_out_groups(request), strict=True
... )
... )
True

Próximos pasos​

La agrupación se elige aquí, en el nivel bosónico, mientras la estructura del modelo sigue siendo visible.