Principios de diseño de las representaciones de operadores
Esta guía explica los principios de diseño comunes y los conceptos básicos compartidos por todas las representaciones de operadores del módulo operators.
Descripción general
Las representaciones de operadores que ofrece este módulo comparten varios principios de diseño fundamentales:
-
Estructura de datos dispersa: los operadores codifican solo las operaciones distintas de la identidad. La disposición 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 en sistemas con muchos modos pero relativamente pocas contribuciones significativas.
-
Iteración y reconstrucción de términos: con independencia del almacenamiento disperso interno, los operadores ofrecen una interfaz de iteración coherente, de modo que puedes inspeccionar, filtrar y transformar términos sin entender la estructura de datos subyacente y luego reconstruir nuevos operadores a partir de los términos modificados.
-
Indexación basada en modos: los operadores usan índices de modo abstractos para etiquetar los grados de libertad fermiónicos, lo que permite una asignación flexible desde los sistemas físicos a la representación del operador.
-
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 optimizaciones y la preservación de la estructura física sin necesidad de estructuras de datos independientes. Consulta la guía de agrupación para ver su uso práctico.
-
Operaciones aritméticas y matemáticas: todos los operadores implementan un conjunto coherente de operaciones aritméticas (como suma, multiplicación y composición) y de funciones matemáticas mediante el protocolo
OperatorTrait, lo que permite un código uniforme para distintos tipos de operadores. -
Orden de términos del operador y formas normales: operadores matemáticamente equivalentes pueden tener representaciones y comportamientos muy distintos en los algoritmos cuánticos. Todas las representaciones de operadores admiten varias formas normales (basadas en relaciones de conmutación propias de cada álgebra) para lograr representaciones de operadores canónicas, predecibles y optimizables.
Estructura de datos dispersa
Todos los operadores usan una representación dispersa en la que solo se registran las operaciones distintas de la identidad. Cada operación distinta de la identidad consta de un coeficiente (número complejo) y una secuencia de acciones sobre modos concretos.
Este enfoque reduce drásticamente el uso de memoria y el tiempo de cálculo, especialmente en sistemas con muchos modos pero relativamente pocas contribuciones significativas. Al codificar solo las operaciones distintas de la identidad, las operaciones se centran únicamente en lo que importa, lo que permite trabajar con sistemas grandes que serían inviables con representaciones densas. Además, los operadores escalan de forma natural a cualquier número de modos; un operador que actúa sobre los modos {0, 1} funciona sin cambios en sistemas con muchos más modos, ya que los modos no afectados son implícitamente la identidad.
Los términos idénticos se conservan por separado durante las operaciones aritméticas y deben combinarse explícitamente si hace falta.
Formato de almacenamiento interno
Internamente, los operadores se almacenan en arreglos inspirados en los formatos de datos de matrices dispersas:
-
Arreglo de coeficientes: el coeficiente complejo de cada término
-
Arreglo de índices de modo: los modos fermiónicos sobre los que actúa cada acción
-
Arreglo de límites: índices que marcan dónde empieza y termina en el arreglo de modos cada término
Según el tipo de operador, puede haber arreglos adicionales. Por ejemplo, las instancias de FermionOperator incluyen un arreglo de acciones de valores booleanos que especifica el tipo de acción fermiónica que actúa sobre el índice de modo correspondiente. En cambio, 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 de tu tipo de operador para conocer el formato de almacenamiento completo.
Los siguientes ejemplos muestran cómo se organizan estos arreglos. Primero, una construcción directa usando los arreglos dispersos:
- PYTHON
- C
>>> 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)
#include <qiskit_fermions.h>
// Construct first operator: 1.0 * +0 -1
QkComplex64 coeff1[1] = {{1.0, 0.0}};
bool actions1[2] = {true, false};
uint32_t modes1[2] = {0, 1};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, actions1, modes1, boundaries1);
// Construct second operator: 1.0 * +2 -3
QkComplex64 coeff2[1] = {{1.0, 0.0}};
bool actions2[2] = {true, false};
uint32_t modes2[2] = {2, 3};
uint32_t boundaries2[2] = {0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(1, 2, coeff2, actions2, modes2, boundaries2);
// Combine the sparse operators. There is no in-place add in the C API, so
// the sum is a new operator that the caller owns.
QfFermionOperator *sum = qf_ferm_op_add(op1, op2);
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);
qf_ferm_op_free(sum);
Métodos de construcción prácticos
Para los desarrolladores de Python, hay varios métodos de construcción prácticos que abstraen los detalles del almacenamiento disperso. Facilitan la construcción de operadores sin tener que preocuparse por gestionar los arreglos de coeficientes, modos y límites:
- PYTHON
- C
>>> 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)
// The C API uses direct array construction; convenience methods are not available.
Las implementaciones individuales de operadores pueden admitir métodos de construcción adicionales adaptados a su caso de uso específico. Consulta la documentación de la API de tu tipo de operador para ver todas las opciones de construcción disponibles.
Iteración y reconstrucción de términos
Los operadores ofrecen una interfaz de iteración coherente mediante OperatorTrait.iter_terms(), independientemente de su representación dispersa interna. Por tanto, puedes inspeccionar, filtrar o transformar términos sin necesidad de entender la estructura de datos subyacente. Después puedes reconstruir un nuevo operador a partir de los términos transformados mediante OperatorTrait.from_terms().
- PYTHON
- C
>>> 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
// WARNING: Term iteration and filtering are not yet available in the C API.
Indexación basada en modos
Todas las representaciones de operadores se refieren a los índices sobre los que actúan sus términos como modos. Un modo es simplemente un índice que identifica un grado de libertad fermiónico de tu sistema. La asignación de los grados de libertad físicos (como orbitales espaciales, estados de espín u otros números cuánticos) a los índices de modo se deja al usuario, lo que permite la máxima flexibilidad.
Esta abstracción también está presente en el módulo qiskit_fermions.circuit, donde FermionicCircuit opera sobre un registro de modos fermiónicos. Tanto en la representación de operadores como en la de circuitos, los modos proporcionan una forma coherente e independiente del álgebra de especificar qué grados de libertad participan en una operación determinada.
Las implementaciones actuales usan 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 tiene electrones o fermiones tanto 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 simples y generales, y evita imponer una convención concreta de ordenación de espín. Módulos de utilidades como qiskit_fermions.operators.library ofrecen funciones prácticas (por ejemplo, FCIDump.from_file()) que se encargan de esas asignaciones por ti al cargar datos de estructura electrónica.
A medida que el paquete evolucione, 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 distinguirán claramente de las implementaciones actuales sin espín 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 opcionalmente un arreglo de grupos que asocia cada término con 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 viaja de forma natural con el operador a través de las transformaciones. Esto permite aprovechar la estructura de forma sistemática, ya provenga de propiedades físicas, relaciones algebraicas o simetrías propias del problema. La información estructurada puede usarse después en operaciones posteriores, como la síntesis y la descomposición de circuitos, mediante métodos como OperatorTrait.split_out_groups().
Para obtener una guía detallada sobre cómo agrupar los términos de un operador en tus flujos de trabajo, consulta la guía de agrupación.
- PYTHON
- C
>>> 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()
#include <qiskit_fermions.h>
// Create operator with 3 terms
QkComplex64 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 operadores funcione de manera uniforme con las demás. Consulta qiskit_fermions.protocols para ver 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 del soporte de modos, reetiquetado), funciones matemáticas (ordenación normal, simplificación, comprobación de equivalencia) y más. Consulta la documentación de OperatorTrait para obtener una referencia completa de todas las operaciones disponibles.
- PYTHON
- C
>>> 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
>>> print(f"Operator is Hermitian: {op.is_hermitian(atol=1e-10)}")
Operator is Hermitian: True
#include <qiskit_fermions.h>
// Construct a Hermitian operator: H = +0 -1 + +1 -0
QkComplex64 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
bool actions[4] = {true, false, true, false};
uint32_t modes[4] = {0, 1, 1, 0};
uint32_t boundaries[3] = {0, 2, 4};
QfFermionOperator *op = qf_ferm_op_new(2, 4, coeffs, actions, modes, boundaries);
// Check if the operator is Hermitian
bool is_hermitian = qf_ferm_op_is_hermitian(op, 1e-10);
printf("Operator is Hermitian: %s\n", is_hermitian ? "true" : "false");
// Clean up
qf_ferm_op_free(op);
is_hermitian() verifica que reduciendo esa diferencia a una forma normal y comparándola con zero(). Expresado en términos de las demás operaciones del protocolo, realiza lo siguiente:
>>> difference = (op - op.adjoint()).normal_ordered().simplify(atol=1e-10)
>>> difference.equiv(FermionOperator.zero(), atol=1e-10)
True
La solidez de esta comprobación depende del tipo de operador, porque depende de en qué medida la forma normal de ese tipo contrae los generadores adyacentes. Un resultado True siempre es fiable, pero para algunos tipos un resultado False es conservador: consulta is_hermitian() para obtener más detalles.
El ejemplo usa atol=1e-10. El parámetro atol (tolerancia absoluta) especifica un umbral. Los coeficientes con una magnitud menor que atol se tratan como cero y se descartan. Esto es esencial para la estabilidad numérica al comparar operadores, ya que la aritmética de punto flotante puede introducir pequeños errores de redondeo que, de otro modo, impedirían reconocer como equivalentes a operadores que lo son.
Aunque el protocolo OperatorTrait ofrece una interfaz común, las implementaciones individuales de operadores pueden ofrecer métodos prácticos adicionales que no forman parte del protocolo. Por ejemplo, FermionOperator y MajoranaOperator ofrecen un método max_rank() que indica la longitud de su término más largo. Consulta siempre la documentación de la API de tu tipo de operador para descubrir toda la funcionalidad disponible.
Ordenación de términos de operadores y formas normales
Un desafío fundamental del álgebra de operadores cuánticos es que operadores matemáticamente equivalentes pueden representarse de muchas formas distintas, cada una con diferentes implicaciones para los algoritmos cuánticos. Un mismo operador puede escribirse en formas algebraicamente equivalentes (por ejemplo, puede expresarse como ), pero estas representaciones conducen a un comportamiento distinto en la síntesis de circuitos, la simplificación y los algoritmos numéricos.
Las representaciones de operadores admiten operaciones de ordenación normal que transforman los operadores en formas canónicas propias de cada álgebra mediante relaciones de conmutación. Esto permite comparaciones fiables (dos operadores equivalentes tienen formas con ordenación normal idénticas), revela simplificaciones (las relaciones de conmutación hacen que los términos se cancelen o se combinen) y respalda algoritmos que requieren formas específicas de los operadores para ser correctos y eficientes.
Distintas representaciones de operadores pueden implementar la ordenación normal basándose en diferentes relaciones de conmutación apropiadas para su álgebra. Por ejemplo, la ordenación normal fermiónica usa relaciones de anticonmutación (), mientras que la ordenación normal de Majorana usa otras convenciones algebraicas (). Consulta siempre la documentación de tu tipo de operador para entender cómo se implementa la ordenación normal.
- PYTHON
- C
>>> 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
#include <qiskit_fermions.h>
#include <stdbool.h>
// Two different representations of the same operator
QkComplex64 coeff1[1] = {{1.0, 0.0}};
bool actions1[2] = {false, true};
uint32_t modes1[2] = {0, 0};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, actions1, modes1, boundaries1);
QkComplex64 coeff2[2] = {{1.0, 0.0}, {-1.0, 0.0}};
bool actions2[2] = {true, false};
uint32_t modes2[2] = {0, 0};
uint32_t boundaries2[3] = {0, 0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(2, 2, coeff2, actions2, 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, NULL);
QfFermionOperator *op2_normal = qf_ferm_op_normal_ordered(op2, NULL);
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 de operadores definan parámetros ajustables que controlen la forma canónica exacta que se produce. Esto habilita optimizaciones específicas de cada operador y variantes de la forma normal adaptadas a su álgebra o caso de uso. Consulta la documentación de la API de tu tipo de operador para ver qué parámetros están disponibles.