Skip to main content
IBM Quantum Platform

Sintetizzare un gate di evoluzione nello spazio fermionico

Importante

I concetti illustrati in questa guida sono attualmente disponibili solo nell'API " Python ". In una versione futura, l'API C offrirà funzionalità equivalenti.

Un gate Evolution trasporta l'evoluzione temporale e−itHe^{-itH} di un operatore intero. Una sintesi da fermione a fermione riscrive quel gate in un insieme di FermionicCircuit gate fermionici più piccoli. Entrambe le parti della riscrittura rimangono nello spazio fermionico, ed è proprio questo che la distingue dalla sintesi da fermione a qubit che segue: gli operatori ( groups vedi Termini relativi agli operatori di gruppo: utilizzare la struttura dell’operatore) sopravvivono alla trasformazione, mentre una mappatura da fermione a qubit restituisce un SparseObservable, che non prevede il concetto di raggruppamento.

Due manopole consentono di selezionare il metodo. Evolution.synthesis lo imposta su un singolo gate, mentre il passaggio del transpiler FermionicTrotterization lo imposta su un intero circuito. Entrambe accettano un’interfaccia FermionicEvolutionSynthesische tale riscrittura implementa. I metodi attualmente forniti da questo pacchetto sono formule di prodotto, che sono quelle utilizzate nel resto di questa guida: FermionicLieTrotter per una formula di primo ordine (impostazione predefinita) e FermionicSuzukiTrotter per un ordine pari superiore. Qualsiasi altro elemento che implementi l'interfaccia viene selezionato allo stesso modo, e le sezioni relative alle due manopole si applicano ad esso senza modifiche.

Nota

Questo passaggio da fermione a fermione è facoltativo. Un gate Evolution può passare direttamente alla fase di conversione da fermioni a qubit, che lo mappa e lo sintetizza nella sua interezza, indipendentemente dal numero di termini che contiene. La scelta di suddividerlo in primo luogo deriva dal fatto che i gate risultanti sono singolarmente più economici oppure perché la suddivisione mette in evidenza una struttura che potrà essere utilizzata in una fase successiva.

Le due fasi si integrano, per cui un’evoluzione può essere approssimata a uno dei due livelli, a entrambi o a nessuno dei due. Una formula di prodotto fornisce un'approssimazione a meno che i suoi fattori non commutino; un metodo di sintesi, invece, non richiede alcuna approssimazione.

Nota

Saltare questo passaggio non significa necessariamente perdere del tutto il raggruppamento. Avvolgendo il mapper in si mappa group_wise() l'operatore un gruppo alla volta e si sommano i risultati, in modo che i termini di Pauli mappati arrivino raggruppati anziché intercalati. Una formula di prodotto laterale per qubit li sintetizza nell'ordine in cui li riceve, in modo che i termini commutativi che risultano adiacenti possano essere elaborati insieme.

Per quanto riguarda l'Hamiltoniano riportato di seguito, che consente di recuperare l'intero vantaggio in termini di profondità a due qubit derivante dalla suddivisione: 18 anziché 30, a parità di numero di gate. La differenza sta nel punto in cui la struttura viene espressa. La suddivisione nello spazio fermionico trasforma ciascun gruppo in un gate separato, che ogni stadio successivo può rilevare e su cui può agire; group_wise lascia un unico gate e si limita a riorganizzare i termini al suo interno, il che è sufficiente per la pianificazione ma non lascia nulla su cui un passaggio fermionico possa operare. Usala quando l'evoluzione deve rimanere a un solo gate; in caso contrario, ricorri a una formula di prodotto fermionico.


Costruire un hamiltoniano raggruppato

Entrambe le formule del prodotto scompongono un operatore in fattori e, nel caso di un operatore raggruppato, tali fattori corrispondono ai suoi gruppi. È quindi il raggruppamento a determinare sia il costo che la precisione del risultato. Si consideri una catena di Fermi-Hubbard a sei siti, raggruppati in base alla commutatività dei relativi termini: i termini di salto sui legami pari, i termini di salto sui legami dispari e l’interazione diagonale.

>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> def fermi_hubbard_chain(num_sites, tunneling, interaction):
...     """A 1D Fermi-Hubbard chain grouped into even bonds, odd bonds, and the interaction."""
...     terms, groups = [], []
...     for offset in (0, 1):
...         for j in range(offset, num_sites - 1, 2):
...             terms += [
...                 ([cre(j), ann(j + 1)], -tunneling),
...                 ([cre(j + 1), ann(j)], -tunneling),
...             ]
...             groups += [offset, offset]
...     for j in range(num_sites - 1):
...         terms.append(([cre(j), ann(j), cre(j + 1), ann(j + 1)], interaction))
...         groups.append(2)
...     operator = FermionOperator.from_terms(terms)
...     operator.groups = groups
...     return operator
>>>
>>> num_sites = 6
>>> hamiltonian = fermi_hubbard_chain(num_sites, tunneling=1.0, interaction=2.0)
>>> hamiltonian.num_groups()
3

Quindici termini suddivisi in tre gruppi. Ogni gruppo di salto associa ogni termine al suo partner coniugato, il che è importante: un fattore deve essere ermitiano affinché la sua esponenziale sia unitaria, e ciò non deriva dal fatto che la somma sia ermitiana. groups_are_hermitian() verifica tale convenzione anziché lasciare che sia l'ispezione a farlo:

>>> from qiskit_fermions.operators.terms.grouping import groups_are_hermitian
>>>
>>> groups_are_hermitian(hamiltonian)
[True, True, True]

Separare l’ a0†a1a^\dagger_0 a_1 e dal suo partner a1†a0a^\dagger_1 a_0 lo distruggerebbe, e lasciare l’operatore non raggruppato ha lo stesso effetto, poiché entrambe le formule lo scompongono termine per termine e un ai†aja^\dagger_i a_j isolato non è ermitiano:

>>> ungrouped = fermi_hubbard_chain(num_sites, tunneling=1.0, interaction=2.0)
>>> ungrouped.groups = range(len(list(ungrouped.iter_terms())))  # one group per term
>>> all(groups_are_hermitian(ungrouped))
False

Tale fattore è un operatore fermionico valido e questo pacchetto genererà il gate corrispondente; pertanto, non si verificherà alcun errore finché l’operatore mappato non raggiungerà PauliEvolutionGate, il che richiede coefficienti reali e genera ValueError: Operator contains complex coefficients, which are not supported. Verificare il raggruppamento in fase iniziale permette di trasformare quell’errore tardivo in una soluzione disponibile prima ancora che venga realizzato qualsiasi circuito.


Impostare il metodo su un gate

Passare un metodo di sintesi al gate, quindi scomporlo. Ogni gate che emette ritorna come un gate Evolution a sé stante:

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import (
...     FermionicLieTrotter,
...     FermionicSuzukiTrotter,
... )
>>>
>>> total_time = 1.0
>>>
>>> def factors(synthesis):
...     circuit = FermionicCircuit(num_sites)
...     circuit.append(
...         Evolution(num_sites, hamiltonian, time=total_time, synthesis=synthesis),
...         circuit.modes,
...     )
...     return circuit.decompose().count_ops()["Evolution"]
>>>
>>> factors(FermionicLieTrotter())
3
>>> factors(FermionicSuzukiTrotter(order=2))
5
>>> factors(FermionicSuzukiTrotter(order=2, reps=4))
20

Il primo ordine emette un fattore per ogni gruppo. L'ordine due genera il palindromo simmetrico, quindi i due gruppi esterni compaiono due volte, ciascuno per metà del tempo, per un totale di cinque. Quattro ripetizioni di questo danno venti. Ogni ordine suddivide l'operatore allo stesso modo; ciò che cambia è l'ordine dei fattori e i loro tempi di evoluzione, non la suddivisione.

Nota

I fattori sono contrassegnati atomic, quindi una nuova decomposizione li lascia al loro posto anziché suddividerli ulteriormente. La decomposizione ha un punto fisso, ed è proprio questo che ne garantisce la sicurezza in caso di chiamate ripetute decompose() .


Imposta il metodo per l'intera pipeline

L'impostazione " synthesis per gate" implica applicare la scelta a tutti gli elementi che ne costituiscono uno, compresi e UCC UCJ, che costruiscono i propri gate internamente. Il pass FermionicTrotterization applica invece un unico metodo a ogni elemento del Evolution circuito, quindi la scelta viene effettuata una sola volta per l'intera pipeline:

>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import FermionicTrotterization
>>>
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
>>>
>>> expanded = FermionicPassManager(
...     FermionicTrotterization(FermionicSuzukiTrotter(order=2, reps=4))
... ).run(circuit)
>>> expanded.count_ops()["Evolution"]
20

Venti porte: cinque fattori per ogni scansione di secondo ordine, per quattro ripetizioni (lo stesso conteggio si ottiene decompose() con il metodo sopra descritto). Il passaggio applica automaticamente la formula, quindi non è necessaria alcuna fase di espansione separata, ed eseguirlo due volte non cambia nulla, poiché i fattori che ne derivano sono e atomic quindi non hanno una definizione propria.

Nota

Passare solo apply=False per selezionare il metodo e lasciare l'espansione a qualcos'altro, come ad esempio Qiskit Decompose (denominare il gate come lo Decompose("Evolution") limita alle evoluzioni, lasciando gli altri gate fermionici ai propri plugin di sintesi).

Tieni presente, però, come funziona un gate non espanso: raggiunge la fase di trasformazione da fermione a qubit nella sua interezza, dove viene mappato senza che venga synthesis consultato. Questo rende facile commettere un errore quando si confrontano i metodi, poiché ognuno di essi produce allora un risultato identico e il confronto, di fatto, non misura nulla. Nell’Hamiltoniano sopra riportato, lasciando il gate di secondo ordine non espanso, il circuito molto più grande sottostante si riduce al costo di una singola evoluzione mappata, indipendentemente dal metodo selezionato.


Valutare il ritorno sull'investimento

Un ordine superiore comporta un maggiore costo in termini di profondità, quindi la questione non è se sia più preciso, ma se lo sia per ogni gate. Per ogni metodo sono necessari due valori: l’errore dello stato che esso genera e il costo in due qubit del circuito che lo ha generato.

L'errore viene misurato con ffsim, che simula un **direttamente **FermionicCircuit (vedi " Come si collega questo pacchetto a ffsim "). Non è coinvolta alcuna mappatura da fermioni a qubit, quindi il confronto isola la formula del prodotto fermionico da qualsiasi contributo che potrebbe derivare dalla mappatura o dalla sintesi lato qubit. Funziona anche nel settore a numero fisso di particelle, che è più piccolo sia dello spazio completo 2N2^N sia dello spazio in cui l’evoluzione si mantiene effettivamente.

>>> import ffsim
>>> import numpy as np
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> norb, nelec = num_sites, (3, 0)  # three spinless fermions on six modes
>>> initial = ffsim.hartree_fock_state(norb, nelec)
>>>
>>> hamiltonian_operator = ffsim.linear_operator(hamiltonian, norb=norb, nelec=nelec)
>>> exact = expm_multiply(
...     -1j * total_time * hamiltonian_operator,
...     initial,
...     traceA=-1j * total_time * ffsim.trace(hamiltonian, norb=norb, nelec=nelec),
... )
>>>
>>> def cost_and_error(synthesis):
...     circuit = FermionicCircuit(num_sites)
...     circuit.append(
...         Evolution(num_sites, hamiltonian, time=total_time, synthesis=synthesis),
...         circuit.modes,
...     )
...     # the method is set on the gate, so decompose() is what expands it here; a pipeline
...     # would reach for FermionicTrotterization instead (see the note above)
...     circuit = circuit.decompose()
...     evolved = ffsim.apply_unitary(initial, circuit, norb=norb, nelec=nelec)
...     error = 1 - abs(np.vdot(evolved, exact)) ** 2
...     qubit_circuit = generate_preset_jw_pass_manager().run(circuit).decompose(reps=6)
...     two_qubit = lambda instruction: len(instruction.qubits) == 2
...     return error, qubit_circuit.count_ops()["cx"], qubit_circuit.depth(two_qubit)
>>>
>>> methods = [
...     ("order 1, reps 1 ", FermionicLieTrotter()),
...     ("order 2, reps 1 ", FermionicSuzukiTrotter(order=2)),
...     ("order 1, reps 10", FermionicLieTrotter(reps=10)),
...     ("order 2, reps 4 ", FermionicSuzukiTrotter(order=2, reps=4)),
... ]
>>> costs = {label: cost_and_error(synthesis) for label, synthesis in methods}
>>> labels, counts, depths = [], [], []
>>> for label, (error, gates, depth) in costs.items():
...     labels.append(label.strip().replace(", ", "\n"))
...     counts.append(gates)
...     depths.append(depth)
...     print(f"{label}  error {error:.6f}")
order 1, reps 1   error 0.515997
order 2, reps 1   error 0.180958
order 1, reps 10  error 0.004026
order 2, reps 4   error 0.000421

Il costo dei due qubit è stato volutamente omesso da quella tabella. Dipende dal metodo di sintesi lato qubit scelto da Qiskit, che viene migliorato di versione in versione, quindi i numeri riportati qui rappresentano un’istantanea piuttosto che una caratteristica intrinseca del prodotto. Il grafico sottostante li rappresenta invece, e ciò su cui si basa il confronto viene affermato come una relazione:

Barre raggruppate relative al numero e alla profondità dei CX per ciascun metodo; quelle relative all’ordine due con quattro ripetizioni risultano chiaramente più corte rispetto a quelle relative all’ordine uno con dieci ripetizioni.

Leggi gli ultimi due metodi insieme, perché è proprio il confronto tra loro che conta. L’ordine 2 con quattro ripetizioni è circa dieci volte più preciso dell’ordine 1 con dieci ripetizioni, e raggiunge tale precisione utilizzando un numero minore di porte di entanglement anziché un numero maggiore, come mostra la figura. In questo caso, destinare il budget per i gate a un ordine superiore è preferibile rispetto a destinarlo a più fasi di primo ordine.

Questo risultato non è valido in tutti i casi. L'aumento reps e l'aumento order riducono l'errore a ritmi diversi a parità di costo, e quale dei due sia più efficace dipende dall'operatore e dal periodo di tempo considerato. Le due righe precedenti spiegano come procedere nel caso di un problema specifico.

Nota

La formula fermionica si integra con quella scelta per la fase di conversione da fermioni a qubit (vedi MapperFnEvolutionSynthesis.product_formula). Quando entrambe sono approssimative, l’accuratezza del risultato è determinata dalla più debole delle due; pertanto, aumentare l’ordine in questo caso, mentre la formula relativa ai qubit rimane di primo ordine, porta pochi vantaggi.


Limitare l'accesso ai cancelli che offrono vantaggi

Un ordine superiore è utile solo quando i fattori non commutano. Se tutti fanno i pendolari, la formula di primo ordine è già esatta e i fattori aggiuntivi rappresentano solo un sovraccarico. Un operatore puramente diagonale lo rende evidente, poiché gli operatori numerici commutano tra loro:

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> diagonal = FermionOperator.from_terms(
...     [([cre(j), ann(j), cre(j + 1), ann(j + 1)], 1.0 + 0.1 * j) for j in range(num_sites - 1)]
... )
>>> diagonal.groups = list(range(num_sites - 1))
>>>
>>> def diagonal_cost(synthesis):
...     circuit = FermionicCircuit(num_sites)
...     circuit.append(
...         Evolution(num_sites, diagonal, time=total_time, synthesis=synthesis), circuit.modes
...     )
...     transpiled = generate_preset_jw_pass_manager().run(circuit.decompose())
...     return transpiled.decompose(reps=6).count_ops()["cx"]
>>>
>>> diagonal_cost(FermionicLieTrotter())
10
>>> diagonal_cost(FermionicSuzukiTrotter(order=2))
18

Entrambi i circuiti sono esatti, quindi il secondo impiega l’80% in più di porte di entanglement senza alcun motivo. Gli operatori di Coulomb diagonali di hanno UCJ questa forma, ed è per questo che FermionicTrotterization.filter esiste: applica un predicato su e DAGOpNode lascia inalterati i gate scartati, quindi un unico passaggio può aumentare l’ordine dei gate che ne traggono beneficio e lasciare gli altri al primo ordine.

Un circuito che comprende entrambi i tipi di evoluzione mostra il risparmio che ne deriva. Il predicato riportato di seguito accetta un gate solo quando il suo operatore sposta le particelle da una modalità all’altra, ovvero proprio quando i gruppi non commutano e un ordine superiore ha qualcosa da offrire:

>>> def is_diagonal(operator):
...     """Whether every term only counts particles, rather than moving them between modes."""
...     return all(
...         sorted(mode for created, mode in actions if created)
...         == sorted(mode for created, mode in actions if not created)
...         for actions, _ in operator.iter_terms()
...     )
>>>
>>> def mixed_circuit():
...     circuit = FermionicCircuit(num_sites)
...     circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
...     circuit.append(Evolution(num_sites, diagonal, time=total_time), circuit.modes)
...     return circuit
>>>
>>> def two_qubit_cost(circuit):
...     transpiled = generate_preset_jw_pass_manager().run(circuit).decompose(reps=6)
...     two_qubit = lambda instruction: len(instruction.qubits) == 2
...     return transpiled.count_ops()["cx"], transpiled.depth(two_qubit)
>>>
>>> order_2 = FermionicSuzukiTrotter(order=2)
>>>
>>> everywhere = two_qubit_cost(
...     FermionicPassManager(FermionicTrotterization(order_2)).run(mixed_circuit())
... )
>>> restricted = two_qubit_cost(
...     FermionicPassManager(
...         FermionicTrotterization(
...             order_2, filter=lambda node: not is_diagonal(node.op.operator)
...         )
...     ).run(mixed_circuit())
... )
>>> everywhere[0] - restricted[0]  # the gates the filter saved
8
>>> restricted[0] < everywhere[0] and restricted[1] < everywhere[1]
True

I gate che il filtro consente di risparmiare sono esattamente quelli che Order Two ha impiegato nell’evoluzione diagonale, che nel confronto diagonale sopra riportato ha un valore di otto (18 contro 10). Lì non hanno modificato nulla, poiché quell’evoluzione è già esatta al primo ordine, quindi ometterle non comporta alcuna perdita di precisione.

Nota

Confronta i due circuiti di secondo ordine tra loro, non con quello non espanso. Se entrambi i gate vengono lasciati non espansi, ciascuno di essi viene inviato integro allo stadio di conversione da fermione a qubit: questo è l’errore descritto nella nota apply=False sopra riportata. Infatti, il circuito può avere lo stesso numero di gate di entanglement del circuito filtrato pur essendo un circuito diverso con una profondità diversa; pertanto, dedurre il vantaggio del filtro da quel numero non fornisce alcuna informazione significativa.

Attenzione

Nessuna delle due tecniche verifica che i fattori siano ermitiani, e un operatore raggruppato in modo errato può sembrare corretto a un ordine pari piuttosto che al primo ordine, pur rimanendo errato, poiché la simmetrizzazione annulla parzialmente l'errore. Raggruppa i partner coniugati, come fatto sopra fermi_hubbard_chain .


Passi successivi

Questa pagina è stata utile?
Segnala un bug, un errore di battitura o richiedi contenuti su GitHub.