Raggruppare i termini dell'operatore: usare la struttura dell'operatore
Un BosonOperator può etichettare ciascuno dei suoi termini con un indice di gruppo. Le etichette non hanno alcun significato proprio: dicono soltanto quali termini appartengono insieme. Cosa le etichette significhino lo decide chi le consuma. In questo pacchetto i consumatori sono il gate di evoluzione temporale, che viene suddiviso in un fattore per gruppo, e il pass randomizzato qDRIFT, che campiona gruppi interi.
Questa guida mostra come assegnare i gruppi, come verificare le proprietà su cui si basa un consumatore e come organizzare l'operatore in modo che scegliere pochi gruppi da uno grande resti economico.
Questo pacchetto non include alcuna strategia di raggruppamento automatico. Gli indici li assegni tu, in base a ciò che sai del tuo modello.
Assegnare i gruppi
L'esempio guida è una catena di Bose-Hubbard a tre siti. Ogni legame contribuisce con una coppia di hopping, e le interazioni on-site formano un pezzo diagonale. Il modo più semplice per costruire un operatore raggruppato è from_terms_with_groups(), che prende ogni termine insieme al suo indice di gruppo come tripla (actions, coefficient, group). Decidi il gruppo nel momento in cui scrivi il termine, quando sai ancora perché vi appartiene:
>>> 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)
I termini sono memorizzati nell'ordine in cui li fornisci, quindi gli indici di gruppo non possono finire associati ai termini sbagliati.
Un operatore che esiste già va raggruppato a posteriori, assegnando l'attributo groups. Qui serve attenzione, perché from_dict() non specifica l'ordine in cui memorizza i suoi termini. Scrivere la lista per posizione può quindi associare un indice al termine sbagliato. Ricava invece ogni indice dal termine stesso:
>>> 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)
Il numero di indici deve corrispondere al numero di termini. Qualsiasi altra cosa viene rifiutata, perché una lista troppo corta farebbe cadere silenziosamente i termini finali ovunque i termini vengano iterati insieme ai loro gruppi:
>>> try:
... hamiltonian.groups = [0, 1]
... except ValueError as error:
... print(error)
expected one group index per term, but got 2 for 7 terms
Verificare ciò su cui si basa il consumatore
Tre funzioni in qiskit_bosons.operators.terms rispondono ciascuna a una domanda ristretta su un raggruppamento esistente. Nessuna di esse dice che un raggruppamento è «corretto», perché la correttezza è relativa al consumatore.
Ogni gruppo è hermitiano? Questa è la proprietà che conta di più. L'evoluzione temporale di un gruppo è unitaria solo se il gruppo è hermitiano, e un singolo termine di hopping non lo è. Solo la coppia con il suo aggiunto è hermitiana:
>>> from qiskit_bosons.operators.terms import groups_are_hermitian
>>> groups_are_hermitian(hamiltonian)
[True, True, True]
Un raggruppamento che separa una coppia di hopping viene individuato subito:
>>> 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]
I quattro termini di hopping vengono rilevati. Le tre voci True sono i termini on-site , che sono hermitiani di per sé. Un modo può legittimamente ripetersi all'interno di un termine, e per questo non viene scartato nulla. I flag seguono l'ordine di terms, ed è per questo che ciascuno può essere letto rispetto al termine che descrive.
I coefficienti sono uniformi? Una formula di prodotto randomizzata pesa un gruppo in base alla magnitudine media dei suoi coefficienti, che è una descrizione fedele del gruppo solo se le magnitudini concordano:
>>> 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]
Nessuno dei due controlli implica l'altro: coefficienti uniformi non rendono hermitiano un gruppo, e un gruppo hermitiano può mescolare magnitudini diverse. Per impostazione predefinita vengono confrontate le magnitudini, perché è ciò che group_coeff_means() calcola come media. Una coppia hermitiana con coefficienti complessi ha magnitudini uguali ma coefficienti diversi, quindi soddisfa solo la forma predefinita:
>>> 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() è costruita su is_hermitian(), che confronta un operatore con il suo aggiunto dopo l'ordinamento normale. Questa è un'identità dell'algebra non troncata, quindi un gruppo che supera il controllo è hermitiano come operatore bosonico. Consulta Principi di progettazione delle rappresentazioni degli operatori per sapere come i prodotti di operatori di scala vengono ordinati e confrontati.
Ordina i termini per gruppo
split_out_groups() restituisce un operatore per ciascun gruppo richiesto. Quando i termini di un gruppo sono sparsi nell'operatore, deve scorrere ogni termine per trovarli. Quando ogni gruppo forma una sequenza contigua, esegue invece una ricerca binaria dei confini dei gruppi, così il costo di una ricerca segue il numero di gruppi richiesti e non il numero di termini contenuti.
group_order() produce questa disposizione. È un ordinamento stabile per indice di gruppo: il valore dell'operatore non cambia e i termini all'interno di un gruppo mantengono il loro ordine 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
Entrambe le disposizioni restituiscono gli stessi operatori; cambia solo il costo:
>>> 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
Passi successivi
Il raggruppamento viene scelto qui, a livello bosonico, mentre la struttura del modello è ancora visibile.
-
Sintetizzare un Evolution gate nello spazio bosonico mostra come usarlo per applicare la formula di Trotter a un'evoluzione, e perché ciò riduce un errore che una formula di prodotto a livello di qubit non può ridurre.
-
Principi di progettazione delle rappresentazioni degli operatori spiega la struttura dati dell'operatore che contiene gli indici di gruppo e come i suoi termini vengono memorizzati e ordinati.
-
2. Raggruppare prima del mapping applica un raggruppamento a un modello completo.