Principios de diseño de las representaciones de los operadores
Esta guía explica los principios de diseño comunes y los conceptos fundamentales que comparten todas las representaciones de operadores del operators módulo.
Visión general
Las representaciones de operadores que ofrece este módulo comparten varios principios fundamentales de diseño:
- Estructura de datos dispersa : los operadores codifican únicamente las operaciones que no son de identidad. La estructura interna de los datos se inspira, en general, en los formatos de datos de matrices dispersas, lo que permite un almacenamiento y un cálculo eficientes para sistemas con muchos modos pero con relativamente pocas contribuciones significativas.
- Iteración y reconstrucción de términos : Independientemente del almacenamiento interno disperso, los operadores ofrecen una interfaz de iteración coherente, lo que permite inspeccionar, filtrar y transformar términos sin necesidad de comprender la estructura de datos subyacente, para luego reconstruir nuevos operadores a partir de los términos modificados.
- Indexación basada en modos : los operadores utilizan índices de modos abstractos para etiquetar los grados de libertad fermiónicos, lo que permite una correspondencia flexible entre los sistemas físicos y la representación mediante operadores.
- Agrupación de términos y relaciones de conmutación : Los operadores admiten de forma nativa información de agrupación que asocia términos con índices de grupo. Esto permite realizar optimizaciones y conservar la estructura física sin necesidad de recurrir a estructuras de datos independientes. Consulta la guía de agrupación para ver cómo se utiliza en la práctica.
- Operaciones aritméticas y matemáticas : Todos los operadores implementan un conjunto coherente de operaciones aritméticas (como la suma, la multiplicación y la composición) y funciones matemáticas mediante el uso del protocolo
OperatorTrait, lo que permite un código uniforme en los diferentes tipos de operadores. - Orden de los operadores y formas normales : los operadores matemáticamente equivalentes pueden tener representaciones y comportamientos muy diferentes en los algoritmos cuánticos. Todas las representaciones de operadores admiten diversas formas normales (basadas en relaciones de conmutación específicas del álgebra) para lograr representaciones de operadores canónicas, predecibles y optimizables.
Estructura de datos dispersa
Todos los operadores utilizan una representación dispersa en la que solo se registran las operaciones que no son de identidad. Cada operación que no sea de identidad consta de un coeficiente (número complejo) y una secuencia de acciones sobre modos específicos.
Este enfoque reduce drásticamente el consumo de memoria y el tiempo de cálculo, especialmente en sistemas con muchos modos pero con relativamente pocas contribuciones significativas. Al codificar únicamente las operaciones que no son de identidad, las operaciones se centran exclusivamente en lo que realmente importa, lo que permite trabajar con sistemas de gran tamaño, algo que resultaría inviable con representaciones densas. Además, los operadores se adaptan de forma natural a cualquier número de modos; un operador que actúa sobre los modos funciona {0, 1} sin cambios en sistemas con muchos más modos, ya que los modos no afectados se consideran implícitamente como la identidad.
Los términos idénticos se conservan por separado durante las operaciones aritméticas y deben combinarse explícitamente si es necesario.
Formato de almacenamiento interno
A nivel interno, los operadores se almacenan en matrices inspiradas en los formatos de datos de matrices dispersas:
- Matriz de coeficientes : el coeficiente complejo de cada término
- Matriz de índices de modos : los modos fermiónicos sobre los que actúa cada acción
- Matriz de límites : índices que indican dónde comienzan y terminan los modos de cada término en la matriz de modos
Pueden existir matrices adicionales en función del tipo de operador. Por ejemplo, las instancias FermionOperator incluyen un array de acciones de tipo booleano que especifica el tipo de acción fermiónica que actúa sobre el índice de modo correspondiente. Por el contrario, la clase MajoranaOperator no requiere esta distinción, ya que codifica esa información en la paridad del índice de modo. Consulta la documentación de la API correspondiente a tu tipo de operador para conocer el formato de almacenamiento completo.
Los siguientes ejemplos muestran cómo están organizadas estas matrices. En primer lugar, una construcción directa utilizando las matrices dispersas:
[x] PYTHON
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> # Construct operators directly using sparse arrays
>>> # First operator: 1.0 * +0 -1
>>> op1 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[0, 1],
... boundaries=[0, 2],
... )
>>>
>>> # Second operator: 1.0 * +2 -3
>>> op2 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[2, 3],
... boundaries=[0, 2],
... )
>>>
>>> # Combine the sparse operators
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)[] C
#include <qiskit_fermions.h>
// Construct first operator: 1.0 * c_0 a_1
QkComplex67 coeff1[1] = {{1.0, 0.0}};
uint32_t modes1[2] = {0, 1};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, modes1, boundaries1);
// Construct second operator: 1.0 * c_2 a_3
QkComplex67 coeff2[1] = {{1.0, 0.0}};
uint32_t modes2[2] = {2, 3};
uint32_t boundaries2[2] = {0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(1, 2, coeff2, modes2, boundaries2);
// Add operators
qf_ferm_op_add_assign(op1, op2);
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);Métodos de construcción prácticos
Para los desarrolladores de « Python », existen varios métodos de construcción prácticos que ocultan los detalles del almacenamiento de datos dispersos. Esto facilita la creación de operadores sin tener que preocuparse por gestionar las matrices de coeficientes, modos y condiciones de contorno:
[x] PYTHON
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct operators using operator algebra notation
>>> op1 = FermionOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> op2 = FermionOperator.from_dict({(cre(2), ann(3)): 1.0})
>>>
>>> # The result is sparse even when combining them
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)[] C
// The C API uses direct array construction; convenience methods are not available.Es posible que algunas implementaciones concretas de operadores admitan métodos de construcción adicionales adaptados a su caso de uso específico. Consulta la documentación de la API correspondiente a tu tipo de operador para ver todas las opciones de construcción disponibles.
Iteración y reconstrucción de términos
Los operadores proporcionan una interfaz de iteración coherente mediante el uso de, independientemente OperatorTrait.iter_terms() de su representación dispersa interna. Por lo tanto, puedes examinar, filtrar o transformar términos sin necesidad de comprender la estructura de datos subyacente. A continuación, puedes reconstruir un nuevo operador a partir de los términos transformados utilizando OperatorTrait.from_terms().
[x] PYTHON
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct an operator with terms of different orders
>>> op = FermionOperator.from_dict({
... (): 0.5, # constant term (order 0)
... (cre(0), ann(1)): 1.0, # two-body term (order 2)
... (cre(0), cre(1), ann(1), ann(0)): 0.25 # four-body term (order 4)
... })
>>>
>>> # Filter to keep only terms of order 2
>>> order_two_terms = [
... (term, coeff) for term, coeff in op.iter_terms()
... if len(term) == 2
... ]
>>>
>>> # Reconstruct operator from filtered terms
>>> filtered_op = FermionOperator.from_terms(order_two_terms)
>>> print(f"Original operator has {len(op)} terms")
Original operator has 3 terms
>>> print(f"Filtered operator has {len(filtered_op)} term")
Filtered operator has 1 term[] C
// WARNING: Term iteration and filtering are not yet available in the C API.Indexación basada en modos
Todas las representaciones de los operadores se refieren a los índices sobre los que actúan sus términos como modos. Un modo es, sencillamente, un índice que identifica un grado de libertad fermiónico en tu sistema. La correspondencia entre los grados de libertad físicos (como los orbitales espaciales, los estados de espín u otros números cuánticos) y los índices de modo se deja a criterio del usuario, lo que permite la máxima flexibilidad.
Esta abstracción también está presente en el módulo qiskit_fermions.circuit , donde el FermionicCircuit opera sobre un registro de modos fermiónicos. Tanto en la representación por operadores como en la representación por circuitos, los modos ofrecen una forma coherente e independiente del álgebra de especificar qué grados de libertad intervienen en una operación determinada.
Las implementaciones actuales utilizan modos sin espín : todas las representaciones de operadores que ofrece actualmente este módulo tratan los modos como grados de libertad fermiónicos sin espín. Esto significa que, si tu sistema contiene tanto electrones o fermiones con espín hacia arriba como con espín hacia abajo, debes asignarlos explícitamente a modos distintos (por ejemplo, los modos 0-3 para el espín hacia arriba de cuatro orbitales espaciales, los modos 4-7 para el espín hacia abajo, o cualquier otra convención que elijas).
Este diseño mantiene las representaciones básicas sencillas y generales, al tiempo que evita imponer una convención específica para el orden de los espines. Los módulos de utilidades como ofrecen qiskit_fermions.operators.library funciones prácticas (por ejemplo, FCIDump.from_file()) que se encargan de esas correspondencias por ti al cargar datos de estructura electrónica.
A medida que el paquete vaya evolucionando, podrían añadirse representaciones de operadores con espín que admitan de forma nativa los grados de libertad de espín en su modelo de datos. Estas se diferenciarán claramente de las implementaciones actuales sin spin y coexistirán con ellas en el módulo.
Agrupación de términos y relaciones de conmutación
Al igual que los coeficientes y los índices de modo, los operadores pueden almacenar, de forma opcional, una matriz de grupos que asocie cada término a un índice de grupo. Al integrar la agrupación directamente en la representación del operador como parte de la estructura de datos dispersa, la información de agrupación se transmite de forma natural junto con el operador a lo largo de las transformaciones. Esto permite utilizar la estructura de forma sistemática, ya sea a partir de propiedades físicas, relaciones algebraicas o simetrías específicas del problema. La información estructurada puede utilizarse posteriormente en operaciones posteriores, como la síntesis y la descomposición de circuitos, mediante métodos como OperatorTrait.split_out_groups().
Para obtener instrucciones detalladas sobre cómo agrupar términos de operador en tus flujos de trabajo, consulta la guía de agrupación.
[x] PYTHON
>>> from qiskit_fermions.operators import MajoranaOperator, gamma
>>> op = MajoranaOperator.from_dict({
... (gamma(0, False),): 1.0,
... (gamma(1, False),): 1.0,
... (gamma(2, False), gamma(3, False)): 1.0
... })
>>> # Assign group indices to terms
>>> op.groups = [0, 0, 1]
>>> # Partition operator by groups
>>> grouped_ops = op.split_out_groups()[] C
#include <qiskit_fermions.h>
// Create operator with 3 terms
QkComplex67 coeffs[3] = {{1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}};
uint32_t modes[4] = {0, 1, 2, 3};
uint32_t boundaries[4] = {0, 1, 2, 4};
QfMajoranaOperator *op = qf_maj_op_new(3, 4, coeffs, modes, boundaries);
// Assign grouping information
uint32_t groups[3] = {0, 0, 1};
qf_maj_op_set_groups(op, groups, 3);
// Partition operator by groups
QfMajoranaOperator *grouped_ops[2];
qf_maj_op_split_out_groups(op, NULL, 0, grouped_ops);Operaciones aritméticas y matemáticas
Todos los operadores implementan el protocolo OperatorTrait , que ofrece un conjunto unificado de operaciones para los distintos tipos de operadores. Esto garantiza que el código escrito para una representación de operador funcione de manera uniforme con las demás. Consulta qiskit_fermions.protocols para saber cómo se relaciona este protocolo con los demás que define este paquete.
El protocolo incluye operaciones aritméticas (como la suma, la multiplicación y la composición), operaciones estructurales (iteración de términos, análisis de compatibilidad de modos, reetiquetado), funciones matemáticas (ordenación normal, simplificación, comprobación de equivalencias) y mucho más. Consulta la documentación OperatorTrait para obtener una referencia completa de todas las operaciones disponibles.
[x] PYTHON
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct a Hermitian operator: H = +0 -1 + +1 -0
>>> op = FermionOperator.from_dict({
... (cre(0), ann(1)): 1.0,
... (cre(1), ann(0)): 1.0
... })
>>>
>>> # Check if the operator is Hermitian by verifying H - H† = 0
>>> adjoint = op.adjoint()
>>> difference = op - adjoint
>>> difference = difference.normal_ordered()
>>> difference = difference.simplify(atol=1e-10)
>>> is_hermitian = difference.equiv(FermionOperator.zero(), atol=1e-10)
>>> print(f"Operator is Hermitian: {is_hermitian}")
Operator is Hermitian: True[] C
#include <qiskit_fermions.h>
// Construct a Hermitian operator: H = +0 -1 + +1 -0
QkComplex67 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
uint32_t modes[4] = {0, 1, 1, 0};
uint32_t boundaries[3] = {0, 2, 4};
QfFermionOperator *op = qf_ferm_op_new(2, 4, coeffs, modes, boundaries);
// Check if Hermitian: compute H - H†, normal-order, and simplify
QfFermionOperator *adjoint = qf_ferm_op_adjoint(op);
QfFermionOperator *difference = qf_ferm_op_sub(op, adjoint);
QfFermionOperator *normal_ordered = qf_ferm_op_normal_ordered(difference);
qf_ferm_op_ichop(normal_ordered, 1e-10);
QfFermionOperator *zero = qf_ferm_op_zero();
bool is_hermitian = qf_ferm_op_equiv(normal_ordered, zero, 1e-10);
printf("Operator is Hermitian: %s\n", is_hermitian ? "true" : "false");
// Clean up
qf_ferm_op_free(op);
qf_ferm_op_free(adjoint);
qf_ferm_op_free(difference);
qf_ferm_op_free(normal_ordered);
qf_ferm_op_free(zero);En el ejemplo se utiliza tanto atol=1e-10 en simplify() como en equiv(). El parámetro ( atol tolerancia absoluta) especifica un umbral. Los coeficientes cuya magnitud sea inferior a atol se consideran iguales a cero y se descartan. Esto es esencial para la estabilidad numérica a la hora de comparar operadores, ya que la aritmética de coma flotante puede introducir pequeños errores de redondeo que, de otro modo, impedirían que los operadores equivalentes se reconocieran como tales.
Aunque el protocolo OperatorTrait proporciona una interfaz común, es posible que las implementaciones de cada operador ofrezcan métodos de conveniencia adicionales que no formen parte del protocolo. Por ejemplo, algunos operadores ofrecen un método is_hermitian() que lleva a cabo esta comprobación. Consulta siempre la documentación de la API correspondiente a tu tipo de operador para conocer todas las funciones disponibles.
Orden de los términos de los operadores y formas normales
Un reto fundamental en el álgebra de operadores cuánticos es que los operadores matemáticamente equivalentes pueden representarse de muchas formas diferentes, cada una de las cuales tiene implicaciones distintas para los algoritmos cuánticos. El mismo operador puede expresarse en formas algebraicamente equivalentes (por ejemplo, « » puede expresarse como « »), aunque estas representaciones dan lugar a comportamientos diferentes en la síntesis de circuitos, la simplificación y los algoritmos numéricos.
Las representaciones de los operadores admiten operaciones de ordenación normales que transforman los operadores en formas canónicas específicas de cada álgebra mediante relaciones de conmutación. Esto permite realizar comparaciones fiables (dos operadores equivalentes tienen formas ordenadas normalmente idénticas), pone de manifiesto simplificaciones (las relaciones de conmutación hacen que los términos se anulen o se combinen) y respalda los algoritmos que requieren formas específicas de operadores para garantizar su corrección y eficiencia.
Las distintas representaciones de operadores pueden implementar el orden normal basándose en diferentes relaciones de conmutación adecuadas a su álgebra. Por ejemplo, el orden normal fermiónico utiliza relaciones de anticomutación ( ), mientras que el orden normal de Majorana utiliza convenciones algebraicas diferentes ( ). Consulta siempre la documentación correspondiente al tipo de operador que utilices para comprender cómo se implementa el orden normal.
[x] PYTHON
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Two different representations of the same operator
>>> op1 = FermionOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> op2 = FermionOperator.from_dict({(): 1.0, (cre(0), ann(0)): -1.0})
>>>
>>> # Direct comparison fails due to different forms
>>> op1.equiv(op2, atol=1e-10)
False
>>>
>>> # Normal-order both and compare again
>>> op1_normal = op1.normal_ordered()
>>> op2_normal = op2.normal_ordered()
>>> op1_normal.equiv(op2_normal, atol=1e-10)
True[] C
#include <qiskit_fermions.h>
#include <stdbool.h>
// Two different representations of the same operator
QkComplex67 coeff1[1] = {{1.0, 0.0}};
uint32_t modes1[2] = {0, 0};
uint32_t boundaries1[3] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, modes1, boundaries1);
QkComplex67 coeff2[2] = {{1.0, 0.0}, {-1.0, 0.0}};
uint32_t modes2[2] = {0, 0};
uint32_t boundaries2[3] = {0, 0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(2, 2, coeff2, modes2, boundaries2);
// Direct comparison fails due to different forms
bool equiv_before = qf_ferm_op_equiv(op1, op2, 1e-10);
printf("Equivalent before normal ordering: %s\n", equiv_before ? "true" : "false");
// Normal-order both and compare again
QfFermionOperator *op1_normal = qf_ferm_op_normal_ordered(op1);
QfFermionOperator *op2_normal = qf_ferm_op_normal_ordered(op2);
bool equiv_after = qf_ferm_op_equiv(op1_normal, op2_normal, 1e-10);
printf("Equivalent after normal ordering: %s\n", equiv_after ? "true" : "false");
// Clean up
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);
qf_ferm_op_free(op1_normal);
qf_ferm_op_free(op2_normal);El método de protocolo OperatorTrait.normal_ordered() deja deliberadamente sin especificar sus argumentos posicionales y de palabra clave, lo que permite que las implementaciones concretas del operador definan parámetros ajustables que controlen la forma canónica exacta que se genera. Esto permite realizar optimizaciones específicas para cada operador y variantes de la forma normal adaptadas a su álgebra o caso de uso. Consulta la documentación de la API correspondiente a tu tipo de operador para ver qué parámetros están disponibles.