Vai al contenuto principale
Questo progetto è in fase beta. Hai trovato un bug? Apri una issue su GitHub. Hai una domanda o un'idea? Chiedi su GitHub Discussions — apprezziamo il tuo feedback!
Com'è la qualità della traduzione su questa pagina?

Ottimizzare la preparazione dei determinanti di Slater

Importante

I concetti di questa guida sono attualmente disponibili solo nell'API Python. Una funzionalità equivalente sarà resa disponibile nell'API C in una versione futura.

Un gate InitializeModes dichiara un'occupazione dei modi di riferimento; un gate OrbitalRotation ruota la base di singola particella. Posti uno dopo l'altro, i due gate preparano un determinante di Slater; i modi partono dal determinante di riferimento e vengono poi ruotati negli orbitali di destinazione. Il gate PrepareSlaterDeterminant cattura questa composizione e, poiché porta con sé l'occupazione di riferimento, la sua transpilazione può usare la givens_decomposition_slater() rettangolare, che realizza solo gli orbitali occupati, invece della decomposizione quadrata completa richiesta da un solo OrbitalRotation. Si tratta di una sintesi strettamente più economica (vedi il vantaggio alla fine di questa guida).

Il transpiler pass MergeSlaterDeterminantPreparation rileva lo schema InitializeModes seguito da OrbitalRotation in un FermionicCircuit e lo riscrive in gate PrepareSlaterDeterminant, in modo che una successiva fase di sintesi sfrutti automaticamente la decomposizione ridotta. In simulazione, la riscrittura non modifica lo stato: PrepareSlaterDeterminant è validate-then-rotate (convalida l'occupazione di riferimento e poi applica la rotazione, proprio come fanno i due gate separati), quindi la fusione abilita la sintesi più economica senza cambiare lo stato preparato.

In questa guida ogni circuito è disegnato prima e dopo il pass, affiancati: l'input a sinistra e, a destra, il risultato dell'esecuzione del pass su di esso. Due funzioni di supporto si occupano di questo. merge esegue il pass su una copia del circuito (tramite il suo FermionicDAGCircuit) e converte il risultato in un FermionicCircuit disegnabile, e draw_merge disegna entrambe le metà in un'unica figura prima/dopo:

>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>>
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG
>>> from qiskit_fermions.transpiler.converters import FermionicDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import MergeSlaterDeterminantPreparation
>>>
>>> def merge(circuit):
... dag = FermionicCircuitToDAG().run(circuit)
... merged = MergeSlaterDeterminantPreparation().run(dag)
... return FermionicDAGToCircuit().run(merged)
>>>
>>> def natural_size(circuit):
... # the figure size Qiskit picks for a circuit on its own (discard the probe figure)
... probe = circuit.draw("mpl")
... size = probe.get_size_inches()
... plt.close(probe)
... return size
>>>
>>> def draw_merge(circuit):
... merged = merge(circuit)
... # let Qiskit size each half naturally, then lay them out side by side
... (bw, bh), (aw, ah) = natural_size(circuit), natural_size(merged)
... fig, (before, after) = plt.subplots(1, 2, figsize=(bw + aw, max(bh, ah)))
... circuit.draw("mpl", ax=before)
... merged.draw("mpl", ax=after)
... before.set_title("before")
... after.set_title("after")
... return fig

Che cosa viene fuso​

Il pass riconosce tre schemi, tutti basati sulla convenzione dei modi a blocchi di spin usata da InitializeModes e OrbitalRotation. Per un sistema con norb orbitali spaziali, i modi 0..norb sono il settore di spin alfa e norb..2*norb sono il settore di spin beta. La preparazione del determinante di Slater viene eseguita per settore di spin perché ogni settore ha una propria forma (elettroni, orbitali).

Registro completo (o senza spin)​

Lo schema più semplice è l'uso di InitializeModes, seguito immediatamente da un OrbitalRotation sugli stessi modi. È il caso senza spin (un determinante su tutti i modi) e anche quello di un circuito con spin la cui rotazione copre entrambi i settori contemporaneamente. I due gate si fondono in un unico PrepareSlaterDeterminant:

>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A mode initialization and rotation fusing into one Slater determinant preparation.

Per settore di spin​

È la stessa forma, ma limitata a una metà di spin. Qui due coppie indipendenti di inizializzazione e rotazione, una per settore, si fondono ciascuna per conto proprio in un PrepareSlaterDeterminant:

>>> circuit = FermionicCircuit(6) # norb = 3: alpha modes 0..3, beta modes 3..6
>>> circuit.append(InitializeModes([1, 1, 0]), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(InitializeModes([1, 0, 0]), circuit.modes[3:])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

Two per-sector initialize-and-rotate pairs, each fusing into its own preparation.

Inizializzazione globale, rotazioni per spin​

Questo comporta un singolo InitializeModes a registro completo (2 * norb) seguito da due azioni OrbitalRotation, una su ciascuna metà di spin contigua. Il pass divide l'occupazione globale al confine tra i settori ed emette due gate PrepareSlaterDeterminant. Le due rotazioni possono comparire in qualsiasi ordine.

Questa è la forma prodotta dal flusso di lavoro del local unitary cluster Jastrow (LUCJ) (vedi la guida a LUCJ); l'utente colloca un InitializeModes (tipicamente il riferimento di Hartree-Fock tramite from_hartree_fock()) all'inizio del circuito e aggiunge un ansatz UCJ, le cui prime due rotazioni orbitali per spin seguono direttamente l'inizializzazione una volta decomposto l'ansatz.

>>> circuit = FermionicCircuit(6) # norb = 3, nelec = (2, 1)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with two per-spin rotations fusing into two preparations.

Questo schema si attiva anche quando solo una metà di spin viene ruotata da un gate che segue direttamente l'inizializzazione, la forma prodotta quando soltanto la rotazione orbitale di un settore di spin si trova all'inizio del circuito. La metà ruotata diventa un PrepareSlaterDeterminant con la sua rotazione, e l'altra metà viene preparata con una rotazione identità. La preparazione identità si sintetizza nei gate X di riferimento che InitializeModes avrebbe comunque emesso per quella metà, quindi il riempimento non costa gate aggiuntivi e al tempo stesso abilita la sintesi di Slater ridotta sulla metà ruotata:

>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with a single per-spin rotation fusing into two preparations.

Che cosa resta intatto​

Il pass è volutamente conservativo. Fonde solo quando i due gate si compongono davvero in una preparazione di determinante di Slater, e copia tutto il resto senza modifiche. «Immediatamente seguito» va inteso sul grafo del flusso di dati del circuito. L'OrbitalRotation si fonde solo quando InitializeModes è il suo unico predecessore su tutti i suoi modi. Nelle figure seguenti, il prima e il dopo sono identici: non si è fuso nulla.

Un'operazione nel mezzo​

Se un qualsiasi gate tocca i modi tra l'inizializzazione e la rotazione, i due non si compongono più in una preparazione pura, quindi non viene fuso nulla:

>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, ann, cre
>>>
>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> number_op = FermionOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> circuit.append(Evolution(4, number_op, time=0.5), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

An operation between the initialization and rotation blocks the merge.

Una rotazione parziale che non è una metà di spin​

Una rotazione che copre esattamente una metà di spin contigua di un'inizializzazione a registro completo si fonde (vedi la sezione precedente). Ma una rotazione su qualsiasi altro insieme parziale di modi (meno modi dell'inizializzazione e che non sia una delle sue due metà di spin, come un sotto-intervallo a cavallo del confine tra i settori) non è una preparazione per settore. Fonderla farebbe perdere silenziosamente il ruolo dell'inizializzazione sui modi che esclude, quindi il pass lascia al loro posto entrambi i gate:

>>> circuit = FermionicCircuit(4) # norb = 2: the only spin halves are modes [0, 1] and [2, 3]
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A partial rotation that is not a spin half is left unmerged.

Perché è importante: il vantaggio della sintesi​

La fusione è vantaggiosa perché PrepareSlaterDeterminant si sintetizza in molti meno gate dell'OrbitalRotation che assorbe. Un determinante di mm elettroni su nn orbitali richiede al massimo m(n−m)m(n-m) rotazioni a due qubit e nessun gate di fase diagonale, contro le n(n−1)/2n(n-1)/2 rotazioni più nn fasi della rotazione orbitale quadrata completa.

Il pass manager preimpostato di Jordan-Wigner esegue MergeSlaterDeterminantPreparation nella sua fase di ottimizzazione, quindi questa riduzione avviene automaticamente. Transpila una preparazione a due elettroni e sei orbitali fino a un QuantumCircuit e conta i suoi gate:

Nota

Il preset esegue MergeOrbitalRotations immediatamente prima di MergeSlaterDeterminantPreparation. Questo conta quando una sequenza di gate OrbitalRotation consecutivi segue l'inizializzazione. Il pass precedente comprime prima la sequenza in un'unica rotazione, che MergeSlaterDeterminantPreparation contrae poi con l'inizializzazione in un unico PrepareSlaterDeterminant. Senza questo ordinamento, verrebbe assorbita solo la prima rotazione della sequenza e le altre si sintetizzerebbero con la decomposizione quadrata completa.

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> # a reproducible random 6 x 6 orbital rotation
>>> rng = np.random.default_rng(1)
>>> z = rng.standard_normal((6, 6)) + 1j * rng.standard_normal((6, 6))
>>> q, r = np.linalg.qr(z)
>>> rotation = q * (np.diagonal(r) / np.abs(np.diagonal(r)))
>>>
>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes([1, 1, 0, 0, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(rotation), circuit.modes)
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> prepared = pm.run(circuit)
>>> print(dict(sorted(prepared.count_ops().items())))
{'x': 2, 'xx_plus_yy': 8}
>>> prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized merged Slater determinant preparation.

Confrontalo con la sintesi della stessa rotazione orbitale da sola. Senza un'inizializzazione precedente non c'è nulla da fondere, quindi il preset ricade sulla decomposizione quadrata completa, che è più profonda e contiene gate di fase diagonali:

>>> reference = FermionicCircuit(6)
>>> reference.append(OrbitalRotation(rotation), reference.modes)
>>> reference_prepared = pm.run(reference)
>>> print(dict(sorted(reference_prepared.count_ops().items())))
{'rz': 6, 'xx_plus_yy': 15}
>>> reference_prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized bare orbital rotation, with its extra phase gates.

La decomposizione ridotta elimina tutti e sei i gate di fase e dimezza quasi il numero di rotazioni a due qubit. Il determinante preparato è corretto a meno di una fase globale (fisicamente irrilevante per la preparazione di stati), che è ciò che i gate di fase avrebbero sistemato.

Due modi per scomporre il gate​

Un PrepareSlaterDeterminant può essere scomposto in due modi diversi, e non sono intercambiabili:

decompose()​

Espande il gate nella sua definizione: l'InitializeModes del riferimento seguito dall'OrbitalRotation completo. Equivale al gate come operatore, per qualsiasi stato di ingresso.

PrepareSlaterDeterminant.synthesis, applicato da F2FSynthesis​

Sostituisce il gate con la decomposizione ridotta di questa guida. Equivale al gate solo come preparazione di stato: produce lo stesso determinante, a meno di una fase globale, quando i modi partono dal vuoto. È più economica proprio perché lo presuppone.

Usa la sintesi quando il gate prepara uno stato a partire dal vuoto, che è lo scopo del gate e ciò che il transpiler fa per te. Usa decompose() quando il gate deve continuare ad agire come operatore, ad esempio perché altri gate lo precedono sugli stessi modi, oppure quando vuoi ispezionare o simulare i gate semplici. Il pass F2FSynthesis non può sapere in quale caso si trova, quindi spetta al circuito che gli fornisci preparare il determinante a partire dal vuoto.

Simulare un circuito con ffsim non richiede nessuno dei due: ogni gate applica tutta la sua unitaria, quindi un circuito che contiene un PrepareSlaterDeterminant viene simulato così com'è e resta sul percorso veloce per i gate OrbitalRotation che lo circondano. Anche scomporre solo il gate di Slater con circuit.decompose("PrepareSlaterDeterminant") lascia intere quelle rotazioni.

Vedi anche

PrepareSlaterDeterminant, InitializeModes, OrbitalRotation, MergeOrbitalRotations, F2FSynthesis, FermionicSlaterGivensDecomposition e la guida a LUCJ per il flusso di lavoro che produce lo schema di inizializzazione globale.