Principi di progettazione delle rappresentazioni degli operatori
Questa guida spiega i principi di progettazione comuni e i concetti fondamentali condivisi da tutte le rappresentazioni degli operatori nel modulo operators.
Panoramica
Le rappresentazioni degli operatori fornite da questo modulo condividono diversi principi di progettazione fondamentali:
-
Struttura dati sparsa: gli operatori codificano solo le operazioni non identità. Il layout interno dei dati si ispira in generale ai formati di dati delle matrici sparse, consentendo archiviazione e calcolo efficienti per sistemi con molti modi ma relativamente pochi contributi significativi.
-
Iterazione e ricostruzione dei termini: indipendentemente dall'archiviazione sparsa interna, gli operatori forniscono un'interfaccia di iterazione coerente, così puoi ispezionare, filtrare e trasformare i termini senza comprendere la struttura dati sottostante, per poi ricostruire nuovi operatori dai termini modificati.
-
Indicizzazione basata sui modi: gli operatori usano indici di modo astratti per etichettare i gradi di libertà fermionici, consentendo una mappatura flessibile dai sistemi fisici alla rappresentazione dell'operatore.
-
Raggruppamento dei termini e relazioni di commutazione: gli operatori supportano nativamente informazioni di raggruppamento che associano i termini a indici di gruppo. Questo consente ottimizzazioni e la preservazione della struttura fisica senza richiedere strutture dati separate. Consulta la guida al raggruppamento per l'uso pratico.
-
Operazioni aritmetiche e matematiche: tutti gli operatori implementano un insieme coerente di operazioni aritmetiche (come addizione, moltiplicazione e composizione) e di funzioni matematiche tramite il protocollo
OperatorTrait, consentendo codice uniforme tra diversi tipi di operatore. -
Ordinamento dei termini dell'operatore e forme normali: operatori matematicamente equivalenti possono avere rappresentazioni e comportamenti molto diversi negli algoritmi quantistici. Tutte le rappresentazioni degli operatori supportano varie forme normali (basate su relazioni di commutazione specifiche dell'algebra) per ottenere rappresentazioni canoniche, prevedibili e ottimizzabili.
Struttura dati sparsa
Tutti gli operatori usano una rappresentazione sparsa in cui vengono tracciate solo le operazioni non identità. Ogni operazione non identità è costituita da un coefficiente (numero complesso) e da una sequenza di azioni su modi specifici.
Questo approccio riduce drasticamente l'uso di memoria e il tempo di calcolo, specialmente per sistemi con molti modi ma relativamente pochi contributi significativi. Codificando solo le operazioni non identità, le operazioni si concentrano solo su ciò che conta, consentendo di lavorare con sistemi grandi che sarebbero impraticabili con rappresentazioni dense. Inoltre, gli operatori scalano naturalmente a qualsiasi numero di modi; un operatore che agisce sui modi {0, 1} funziona invariato in sistemi con molti più modi, poiché i modi non coinvolti sono implicitamente identità.
I termini identici vengono mantenuti separati durante le operazioni aritmetiche e devono essere combinati esplicitamente se necessario.
Formato di archiviazione interno
Internamente, gli operatori sono archiviati in array ispirati ai formati di dati delle matrici sparse:
-
Array dei coefficienti: il coefficiente complesso di ciascun termine
-
Array degli indici dei modi: i modi fermionici su cui agisce ciascuna azione
-
Array dei confini: indici che segnano dove iniziano e finiscono i modi di ciascun termine nell'array dei modi
A seconda del tipo di operatore possono essere presenti array aggiuntivi. Ad esempio, le istanze di FermionOperator includono un array delle azioni di booleani che specifica il tipo di azione fermionica che agisce sul rispettivo indice di modo. Al contrario, la classe MajoranaOperator non richiede questa distinzione perché codifica tale informazione nella parità dell'indice del modo. Consulta la documentazione dell'API per il tuo tipo di operatore per comprendere il formato di archiviazione completo.
Gli esempi seguenti mostrano come sono organizzati questi array. Il primo è una costruzione diretta tramite gli array sparsi:
- 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);
Metodi di costruzione pratici
Per gli sviluppatori Python sono disponibili diversi metodi di costruzione pratici che astraggono i dettagli dell'archiviazione sparsa. Rendono più semplice costruire operatori senza doversi preoccupare di gestire gli array di coefficienti, modi e confini:
- 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.
Le singole implementazioni di operatori potrebbero supportare metodi di costruzione aggiuntivi adatti al loro caso d'uso specifico. Consulta la documentazione API del tuo tipo di operatore per vedere tutte le opzioni di costruzione disponibili.
Iterazione sui termini e ricostruzione
Gli operatori forniscono un'interfaccia di iterazione coerente tramite OperatorTrait.iter_terms(), indipendentemente dalla loro rappresentazione sparsa interna. Puoi quindi ispezionare, filtrare o trasformare i termini senza dover comprendere la struttura dati sottostante. Puoi poi ricostruire un nuovo operatore dai termini trasformati usando 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.
Indicizzazione basata sui modi
Tutte le rappresentazioni degli operatori indicano gli indici su cui agiscono i loro termini come modi. Un modo è semplicemente un indice che identifica un grado di libertà fermionico nel tuo sistema. La mappatura dai gradi di libertà fisici (come orbitali spaziali, stati di spin o altri numeri quantici) agli indici dei modi è lasciata all'utente, per garantire la massima flessibilità.
Questa astrazione è presente anche nel modulo qiskit_fermions.circuit, dove FermionicCircuit opera su un registro di modi fermionici. Sia nella rappresentazione degli operatori sia in quella dei circuiti, i modi offrono un modo coerente e indipendente dall'algebra per specificare quali gradi di libertà partecipano a una data operazione.
Le implementazioni attuali usano modi senza spin: tutte le rappresentazioni degli operatori attualmente fornite da questo modulo trattano i modi come gradi di libertà fermionici privi di spin. Questo significa che, se il tuo sistema ha sia elettroni o fermioni con spin up sia con spin down, devi mapparli esplicitamente su modi distinti (ad esempio, i modi 0-3 per lo spin up di quattro orbitali spaziali, i modi 4-7 per lo spin down, o qualsiasi altra convenzione tu scelga).
Questo design mantiene semplici e generali le rappresentazioni di base, evitando di imporre una specifica convenzione di ordinamento dello spin. I moduli di utilità come qiskit_fermions.operators.library forniscono funzioni di comodo (ad esempio, FCIDump.from_file()) che gestiscono tali mappature al posto tuo quando carichi dati di struttura elettronica.
Con l'evoluzione del pacchetto, potrebbero essere aggiunte rappresentazioni di operatori con spin che supportano nativamente i gradi di libertà di spin nel loro modello dati. Queste saranno chiaramente distinte dalle attuali implementazioni senza spin e coesisteranno con esse nel modulo.
Raggruppamento dei termini e relazioni di commutazione
Come i coefficienti e gli indici dei modi, gli operatori possono opzionalmente memorizzare un array di gruppi che associa ogni termine a un indice di gruppo. Integrando il raggruppamento direttamente nella rappresentazione dell'operatore come parte della struttura dati sparsa, le informazioni di raggruppamento accompagnano naturalmente l'operatore attraverso le trasformazioni. Questo consente di sfruttare la struttura in modo sistematico, che derivi da proprietà fisiche, relazioni algebriche o simmetrie specifiche del problema. Le informazioni strutturate possono poi essere usate in operazioni a valle come la sintesi e la decomposizione dei circuiti, tramite metodi come OperatorTrait.split_out_groups().
Per una guida dettagliata su come raggruppare i termini degli operatori nei tuoi flussi di lavoro, consulta la guida al raggruppamento.
- 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);
Operazioni aritmetiche e matematiche
Tutti gli operatori implementano il protocollo OperatorTrait, che fornisce un insieme unificato di operazioni per i diversi tipi di operatori. Questo garantisce che il codice scritto per una rappresentazione di operatore funzioni in modo uniforme con le altre. Consulta qiskit_fermions.protocols per vedere come questo protocollo si relaziona agli altri definiti da questo pacchetto.
Il protocollo comprende operazioni aritmetiche (come addizione, moltiplicazione e composizione), operazioni strutturali (iterazione sui termini, analisi del supporto dei modi, rietichettatura), funzioni matematiche (ordinamento normale, semplificazione, verifica di equivalenza) e altro ancora. Consulta la documentazione di OperatorTrait per un riferimento completo di tutte le operazioni disponibili.
- 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 che riducendo tale differenza a una forma normale e confrontandola con zero(). Espresso in termini delle altre operazioni del protocollo, esegue quanto segue:
>>> difference = (op - op.adjoint()).normal_ordered().simplify(atol=1e-10)
>>> difference.equiv(FermionOperator.zero(), atol=1e-10)
True
L'affidabilità di questo controllo dipende dal tipo di operatore, perché dipende da quanto completamente la forma normale di quel tipo contrae i generatori adiacenti. Un risultato True è sempre affidabile, ma per alcuni tipi un risultato False è prudenziale: consulta is_hermitian() per i dettagli.
L'esempio usa atol=1e-10. Il parametro atol (tolleranza assoluta) specifica una soglia. I coefficienti con modulo inferiore a atol sono trattati come zero e scartati. Questo è essenziale per la stabilità numerica nel confronto tra operatori, poiché l'aritmetica in virgola mobile può introdurre piccoli errori di arrotondamento che altrimenti impedirebbero di riconoscere come tali operatori equivalenti.
Sebbene il protocollo OperatorTrait fornisca un'interfaccia comune, le singole implementazioni di operatori potrebbero offrire metodi di comodo aggiuntivi che non fanno parte del protocollo. Ad esempio, FermionOperator e MajoranaOperator forniscono un metodo max_rank() che restituisce la lunghezza del loro termine più lungo. Consulta sempre la documentazione API del tuo tipo di operatore per scoprire tutte le funzionalità disponibili.
Ordinamento dei termini degli operatori e forme normali
Una sfida fondamentale nell'algebra degli operatori quantistici è che operatori matematicamente equivalenti possono essere rappresentati in molti modi diversi, ciascuno con implicazioni diverse per gli algoritmi quantistici. Lo stesso operatore può essere scritto in forme algebricamente equivalenti (ad esempio, può essere espresso come ), eppure queste rappresentazioni portano a comportamenti diversi nella sintesi dei circuiti, nella semplificazione e negli algoritmi numerici.
Le rappresentazioni degli operatori supportano operazioni di ordinamento normale che trasformano gli operatori in forme canoniche specifiche dell'algebra usando le relazioni di commutazione. Questo consente confronti affidabili (due operatori equivalenti hanno forme ordinate normalmente identiche), rivela semplificazioni (le relazioni di commutazione fanno cancellare o combinare i termini) e supporta algoritmi che richiedono forme specifiche dell'operatore per correttezza ed efficienza.
Rappresentazioni di operatori diverse potrebbero implementare l'ordinamento normale basandosi su diverse relazioni di commutazione appropriate alla loro algebra. Ad esempio, l'ordinamento normale fermionico usa relazioni di anticommutazione (), mentre l'ordinamento normale di Majorana usa convenzioni algebriche diverse (). Consulta sempre la documentazione del tuo tipo di operatore per capire come è implementato l'ordinamento normale.
- 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);
Il metodo di protocollo OperatorTrait.normal_ordered() lascia volutamente non specificati i suoi argomenti posizionali e per parola chiave, consentendo alle implementazioni concrete degli operatori di definire parametri regolabili che controllano l'esatta forma canonica prodotta. Questo permette ottimizzazioni specifiche per operatore e varianti della forma normale adattate alla loro algebra o al caso d'uso. Consulta la documentazione API del tuo tipo di operatore per vedere quali parametri sono disponibili.