Principi di progettazione delle rappresentazioni degli operatori
Questa guida spiega come è organizzato un BosonOperator e come lavorarci: come costruirne uno, come ispezionare e combinare operatori e come portare due operatori in una forma in cui possano essere confrontati. Parti da qui quando scrivi un Hamiltoniano.
Un BosonOperator è una somma esatta di prodotti di operatori bosonici di creazione e annichilazione con coefficienti complessi. Non porta alcun cutoff di occupazione. Un modo bosonico contiene un numero arbitrario di bosoni, quindi il suo spazio di Fock ha dimensione infinita, e un troncamento interviene solo quando l'operatore viene mappato su qubit (vedi Tronca lo spazio di Fock). Tutto ciò che accade in questa guida avviene quindi nell'algebra esatta delle relazioni di commutazione canoniche,
Due conseguenze plasmano l'interfaccia. Un indice di modo può ripetersi all'interno di un singolo termine, perché è un operatore valido che crea due bosoni. Inoltre, riordinare due fattori non introduce mai un segno, anche se spostare un operatore di annichilazione oltre un operatore di creazione sullo stesso modo aggiunge un termine, .
Archiviazione
Un operatore conserva i suoi termini in quattro array piatti, simili al formato compressed sparse row di una matrice sparsa:
coeffs | Un coefficiente complesso per termine. |
actions | Un booleano per fattore: True per un operatore di creazione e False per uno di annichilazione. |
modes | Un indice di modo per fattore. |
boundaries | Le posizioni in actions e modes in cui ogni termine inizia e finisce. |
Il costruttore accetta direttamente questi quattro array. L'operatore qui sotto ha tre termini, cioè , e la costante , e puoi rileggere gli array con i metodi get_*:
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> op = BosonOperator(
... coeffs=[2.0, -1.0j, 0.5],
... actions=[True, False, True, True],
... modes=[0, 1, 1, 1],
... boundaries=[0, 2, 4, 4],
... )
>>> op.get_coeffs()
[(2+0j), (-0-1j), (0.5+0j)]
>>> op.get_actions()
[True, False, True, True]
>>> op.get_modes()
[0, 1, 1, 1]
>>> op.get_boundaries()
[0, 2, 4, 4]
>>> len(op)
3
Il primo termine si estende dalle posizioni 0 a 2 di actions e modes, il secondo da 2 a 4 e il terzo da 4 a 4, ed è così che viene memorizzato il termine costante, un termine senza fattori. Il modo ripetuto 1 nel secondo termine viene preservato come scritto; un termine non viene mai scartato o fuso perché un indice di modo compare due volte.
Un operatore costruito con from_dict() non memorizza i suoi termini nell'ordine del dizionario, quindi i suoi array possono elencare i termini in un ordine diverso da un'esecuzione all'altra. L'operatore stesso è lo stesso, e nulla di ciò che segue che confronti o stampi operatori dipende dall'ordine memorizzato.
Questa struttura rende economiche la costruzione e l'aritmetica, con una conseguenza: un operatore può contenere lo stesso termine più volte contemporaneamente. Somme e prodotti aggiungono termini senza fonderli, quindi chiama simplify() quando ti servono termini unici (vedi Fai aritmetica).
Costruire operatori
Il modo più diretto per costruire un operatore è from_dict(), che accetta un dizionario da una tupla di fattori a un coefficiente. Le funzioni cre() e ann() creano i fattori, e la tupla vuota è il termine costante:
>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dag = BosonOperator.from_dict({(cre(0),): 1.0})
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> print(format(number))
1.000000e0 +0.000000e0j * (+0 -0)
Nella forma stampata, +0 sta per e -0 per . I costruttori zero() e one() restituiscono l'identità additiva e quella moltiplicativa, e from_terms() ricostruisce un operatore a partire dall'iteratore di iter_terms().
La libreria di operatori fornisce le quadrature adimensionali di posizione e impulso,
come position() e momentum(). Insieme alle funzioni di commutatore dello stesso modulo, puoi verificare direttamente l'algebra:
>>> from qiskit_bosons.operators.library import commutator, momentum, position
>>> q, p = position(0), momentum(0)
>>> q.is_hermitian() and p.is_hermitian()
True
>>> commutator(q, p).equiv(1j * BosonOperator.one())
True
>>> commutator(b, b_dag).equiv(BosonOperator.one())
True
Il modulo fornisce anche anti_commutator() e double_commutator(). Tutte e tre accettano operatori dello stesso tipo e restituiscono un nuovo operatore.
Attenzione all'ordine di un prodotto
Due operatori possono essere moltiplicati in due modi, ed è facile confonderli. L'operatore @ è il prodotto matematico, quindi a @ b è . L'operatore & è la composizione, che segue la convenzione di Qiskit di leggere da sinistra a destra: a & b applica prima a e poi b, quindi è . Le funzioni di commutatore usano @, quindi commutator(a, b) è . Costruire tu stesso la parentesi con & la nega silenziosamente:
>>> (b @ b_dag).equiv(b_dag @ b + BosonOperator.one())
True
>>> (b & b_dag).equiv(b_dag @ b)
True
>>> ((b @ b_dag) - (b_dag @ b)).equiv(BosonOperator.one())
True
>>> ((b & b_dag) - (b_dag & b)).equiv(-BosonOperator.one())
True
Ispezionare un operatore
Un operatore non è direttamente iterabile, perché i suoi termini risiedono negli array piatti descritti sopra. Usa invece iter_terms(), che restituisce per ogni termine una lista di coppie (is_creation, mode) e un coefficiente. L'iteratore restituisce copie, quindi modificarle non modifica l'operatore.
>>> hopping = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(2), cre(2), ann(2), ann(2)): 0.5,
... }
... )
>>> len(hopping)
3
>>> sorted(hopping.iter_terms())
[([(True, 0), (False, 1)], (-1+0j)), ([(True, 1), (False, 0)], (-1+0j)), ([(True, 2), (True, 2), (False, 2), (False, 2)], (0.5+0j))]
Diversi metodi riassumono la struttura di un operatore:
>>> hopping.get_support()
{0, 1, 2}
>>> hopping.min_num_modes()
3
>>> hopping.max_rank()
4
get_support() è l'insieme dei modi su cui l'operatore agisce, min_num_modes() è il registro più piccolo che può contenerlo e max_rank() è la lunghezza del suo termine più lungo.
Un modo è un indice astratto che etichetta un grado di libertà bosonico. Non ha un significato proprio: sei tu a determinare quale sito di un reticolo, quale coordinata vibrazionale o quale porta ottica rappresenti un modo. Il BosonicCircuit usa gli stessi indici (vedi Lavora con i circuiti bosonici), quindi un operatore e i gate che lo trasportano concordano su cosa sia il modo 2 senza che nessuno dei due sappia cosa rappresenti. Nulla nell'operatore fissa quanti livelli abbia un modo, ed è anche per questo che la dimensione del registro è solo un limite inferiore; min_num_modes() è il registro più piccolo che contiene l'operatore, non una sua proprietà.
I metodi di occupazione riportano quanti bosoni un singolo termine può collocare in un modo. Questo è un limite inferiore al cutoff di cui una mappatura ha bisogno. Non è un cutoff sufficiente, perché applicazioni ripetute dell'operatore, come in un'evoluzione temporale, possono popolare livelli più alti (vedi Tronca lo spazio di Fock):
>>> hopping.mode_occupations()
[1, 1, 2]
>>> hopping.mode_occupations([2, 0])
[2, 1]
>>> hopping.max_occupation()
2
Altre due verifiche riguardano le quantità conservate. Un operatore conserva il numero di particelle quando ogni termine ha tanti operatori di creazione quanti di annichilazione. Passando una lista di dimensioni di blocco si verifica la stessa condizione all'interno di ogni blocco di modi consecutivi:
>>> hopping.conserves_particle_number()
True
>>> move = BosonOperator.from_dict({(cre(0), ann(2)): 1.0})
>>> move.conserves_sector([3])
True
>>> move.conserves_sector([2, 1])
False
Infine, relabel_modes() rinomina i modi: il modo i diventa permutation[i]. La lista deve essere abbastanza lunga da indicizzare ogni modo dell'operatore e non deve ripetere alcuna voce:
>>> print(format(move.relabel_modes([5, 6, 7])))
1.000000e0 +0.000000e0j * (+5 -7)
Fare aritmetica
Gli operatori supportano addizione, sottrazione, negazione, e moltiplicazione e divisione per un numero, nella forma usuale tra operatori o sul posto:
>>> total = 2 * b_dag + b_dag - 3j * b
>>> len(total)
3
>>> len(total.simplify())
2
>>> total.simplify().equiv(3 * b_dag - 3j * b)
True
I tre termini di total non sono mai stati fusi, quindi l'operatore conteneva b_dag due volte finché simplify() non li ha combinati. Il metodo somma i coefficienti dei termini identici ed elimina quelli la cui grandezza totale è inferiore a una tolleranza. È un'operazione strutturale: confronta i fattori scritti e non applica le relazioni di commutazione. Il metodo correlato ichop() rimuove sul posto i coefficienti piccoli senza prima fondere, quindi chiama simplify() prima di ichop() quando un operatore può contenere termini duplicati.
I prodotti usano @ o & come descritto sopra, e ** eleva un operatore a una potenza intera:
>>> x = b_dag + b
>>> print(format((x ** 2).simplify()))
1.000000e0 +0.000000e0j * (-0 -0)
1.000000e0 +0.000000e0j * (-0 +0)
1.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0)
Il risultato elenca i quattro ordinamenti così come scritti. Ridurli a un'unica forma richiede le relazioni di commutazione, che la sezione successiva applica.
L'aggiunto inverte l'ordine dei fattori in ogni termine, scambia gli operatori di creazione e annichilazione e coniuga i coefficienti. Il metodo is_hermitian() verifica se un operatore è uguale al proprio aggiunto:
>>> hop = BosonOperator.from_dict({(cre(0), ann(1)): 1j})
>>> print(format(hop.adjoint()))
0.000000e0 -1.000000e0j * (+1 -0)
>>> hop.is_hermitian()
False
>>> (hop + hop.adjoint()).is_hermitian()
True
Un termine di hopping isolato non è hermitiano, e questo conta quando raggruppi i termini per un'evoluzione temporale: ogni gruppo deve essere hermitiano da solo (vedi Raggruppa i termini dell'operatore: usa la struttura dell'operatore).
Ordine normale e confronto degli operatori
Due prodotti di operatori di scala possono essere lo stesso operatore e tuttavia apparire diversi, perché e sono uguali nell'algebra. Operatori uguali in questo senso non devono necessariamente essere memorizzati allo stesso modo, quindi confrontarli richiede le relazioni di commutazione. Il metodo normal_ordered() le applica: sposta ogni operatore di creazione a sinistra di ogni operatore di annichilazione e aggiunge un termine ogni volta che una coppia sullo stesso modo viene scambiata.
>>> product = BosonOperator.from_dict({(ann(0), cre(0), ann(0), cre(0)): 1.0})
>>> ordered = product.normal_ordered()
>>> len(ordered)
5
>>> print(format(ordered.simplify()))
1.000000e0 +0.000000e0j * ()
3.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0 -0 -0)
L'operatore ordinato viene restituito come somma non fusa, con cinque termini memorizzati che si riducono a tre dopo che simplify() li fonde. L'ordine normale non fa mai scomparire un termine: e ogni altro fattore ripetuto restano.
L'argomento sandwich stabilisce come sono disposti i modi all'interno dei due gruppi di fattori. Per impostazione predefinita entrambi i gruppi sono ordinati in modo decrescente. Con sandwich=True, gli operatori di creazione sono crescenti e quelli di annichilazione decrescenti, il che porta gli indici più grandi verso il centro:
>>> pair = BosonOperator.from_dict({(cre(0), cre(1), ann(0), ann(1)): 1.0})
>>> list(pair.normal_ordered().iter_terms())
[([(True, 1), (True, 0), (False, 1), (False, 0)], (1+0j))]
>>> list(pair.normal_ordered(sandwich=True).iter_terms())
[([(True, 0), (True, 1), (False, 1), (False, 0)], (1+0j))]
L'operatore == verifica l'uguaglianza esatta dei termini memorizzati, senza tolleranza e senza semplificazione, quindi può segnalare una differenza tra due operatori matematicamente uguali. Il metodo equiv() porta prima la differenza dei due operatori in ordine normale e poi verifica che ogni coefficiente sia inferiore a una tolleranza. È il confronto che quasi sempre vuoi:
>>> reordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0, (): 1.0})
>>> contracted = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> contracted == reordered
False
>>> contracted.simplify() == reordered.simplify()
False
>>> contracted.equiv(reordered)
True
Il solo metodo simplify lascia separate le due forme, perché fonde solo i termini scritti identici. Allo stesso modo, is_hermitian() confronta un operatore con il suo aggiunto in ordine normale, quindi riconosce come hermitiano senza ulteriore lavoro.
L'ordine normale è un'identità esatta nell'algebra non troncata, e equiv si basa su di essa. Non è un'identità dopo un troncamento. Un operatore mappato dipende dalla forma che mappi, quindi le immagini mappate di contracted e reordered sopra differiscono, solo sul livello più alto mantenuto. Vedi Tronca lo spazio di Fock per la dimensione della differenza e come scegliere una forma.
Ordinare e filtrare i termini
Riordinare i termini di un operatore, a differenza dei fattori all'interno di un termine, non cambia l'operatore ma può cambiare il comportamento degli algoritmi che leggono i termini in ordine, come una formula di prodotto randomizzata. Le funzioni di qiskit_bosons.operators.terms restituiscono un nuovo operatore e mantengono eventuali indici di gruppo associati ai loro termini.
canonical_order() ordina i termini in un ordine fisso che dipende solo dai fattori scritti, quindi un operatore si legge allo stesso modo comunque sia stato assemblato. order_terms() ordina secondo una chiave che fornisci tu, la quale riceve ogni termine come la tupla restituita da iter_terms():
>>> from qiskit_bosons.operators.terms import canonical_order, order_terms
>>> mixed = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> list(canonical_order(mixed).iter_terms())
[([], (2+0j)), ([(True, 0), (False, 1)], (3+0j)), ([(True, 1), (False, 0)], (1+0j))]
>>> by_weight = order_terms(mixed, key=lambda term: abs(term[1]), reverse=True)
>>> [coeff for _, coeff in by_weight.iter_terms()]
[(3+0j), (2+0j), (1+0j)]
Se un operatore porta indici di gruppo (vedi Raggruppa i termini dell'operatore: usa la struttura dell'operatore), la funzione group_order() rende ogni gruppo una sequenza contigua di termini:
>>> from qiskit_bosons.operators.terms import group_order
>>> grouped = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> grouped.groups = [1, 0, 1]
>>> group_order(grouped).groups
[0, 1, 1]
L'ultimo helper rimuove termini anziché riordinarli. Un termine che è un prodotto di operatori numero, , moltiplica solo ogni stato di Fock per una fase, quindi filter_diagonal_terms() può eliminare la costante, gli operatori numero e i loro prodotti quando quella fase è irrilevante per ciò che calcoli. Modifica l'operatore sul posto e richiede un operatore in ordine normale, cosa che non verifica:
>>> from qiskit_bosons.operators.terms import filter_diagonal_terms
>>> model = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(0), cre(0), ann(0), ann(0)): 0.5,
... (cre(0), ann(0)): 0.2,
... (): 0.1,
... }
... )
>>> filter_diagonal_terms(model)
>>> len(model)
2
>>> model.is_hermitian()
True
Restano solo i due termini di hopping. L'interazione on-site e l'operatore numero sono diagonali nella base di occupazione, quindi vengono rimossi insieme alla costante. Il modo ripetuto nell'interazione è gestito correttamente: è diagonale, mentre non lo sarebbe.
Passi successivi
-
Raggruppa i termini dell'operatore: usa la struttura dell'operatore mostra come etichettare i termini con indici di gruppo.
-
Mappa le rappresentazioni degli operatori trasforma un operatore in un operatore di qubit, includendo la scelta della codifica e l'effetto di un troncamento.
-
Lavora con i circuiti bosonici inserisce un operatore in un circuito come gate
Evolution. -
Simula la dinamica di Bose-Hubbard con una scelta di codifica costruisce l'Hamiltoniano di una catena di Bose-Hubbard e la simula end to end.
-
I moduli
qiskit_bosons.operatorseqiskit_bosons.operators.termsdocumentano l'interfaccia completa.