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.
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 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 , 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]
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.
-
Sintetiza una puerta de evolución en el espacio bosónico muestra cómo usarla para aplicar Trotter a una evolución, y por qué eso reduce un error que una fórmula de producto a nivel de qubits no puede reducir.
-
Principios de diseño de las representaciones de operadores explica la estructura de datos del operador que lleva los índices de grupo, y cómo se almacenan y ordenan sus términos.
-
2. Agrupar antes de mapear aplica una agrupación a un modelo completo.