Optimizează pregătirea determinantului Slater
Conceptele din acest ghid sunt disponibile în prezent doar în API-ul Python. Funcționalitatea echivalentă va fi pusă la dispoziție în API-ul C într-o versiune viitoare.
O poartă InitializeModes declară o ocupare de referință a modurilor; o poartă OrbitalRotation rotește baza cu o singură particulă. Puse una după alta, cele două porți pregătesc un determinant Slater; modurile pornesc în determinantul de referință și sunt apoi rotite în orbitalii țintă. Poarta PrepareSlaterDeterminant surprinde această compoziție și, pentru că poartă ocuparea de referință, transpilarea ei poate folosi givens_decomposition_slater() dreptunghiulară, care realizează doar orbitalii ocupați, în loc de descompunerea pătrată completă pe care o necesită doar un OrbitalRotation. Aceasta este o sinteză strict mai ieftină (vezi beneficiul la finalul acestui ghid).
Pasul de transpilare MergeSlaterDeterminantPreparation detectează tiparul InitializeModes urmat de OrbitalRotation într-un FermionicCircuit și îl rescrie în porți PrepareSlaterDeterminant, astfel încât o etapă de sinteză ulterioară preia automat descompunerea redusă. În simulare, rescrierea nu modifică starea: PrepareSlaterDeterminant este validate-then-rotate (validează ocuparea de referință și apoi aplică rotația, exact cum fac cele două porți separate), așa că fuzionarea deblochează sinteza mai ieftină fără a schimba starea pregătită.
În tot acest ghid, fiecare circuit este desenat înainte și după pas, unul lângă altul: intrarea în stânga, iar în dreapta rezultatul rulării pasului asupra ei. Două funcții ajutătoare fac asta. merge rulează pasul pe o copie a circuitului (prin FermionicDAGCircuit) și convertește rezultatul înapoi într-un FermionicCircuit ce poate fi desenat, iar draw_merge desenează ambele jumătăți într-o singură figură înainte/după:
>>> 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
Ce se fuzionează
Pasul recunoaște trei tipare, toate bazate pe convenția modurilor cu spini în blocuri pe care o folosesc InitializeModes și OrbitalRotation. Pentru un sistem cu norb orbitali spațiali, modurile 0..norb formează sectorul de spin alfa, iar norb..2*norb sectorul de spin beta. Pregătirea determinantului Slater se face pe fiecare sector de spin, deoarece fiecare sector are propria formă (electroni, orbitali).
Registru complet (sau fără spin)
Cel mai simplu tipar este folosirea unui InitializeModes, urmat imediat de un OrbitalRotation pe aceleași moduri. Acesta este cazul fără spin (un singur determinant peste toate modurile) și, totodată, un circuit cu spin a cărui rotație acoperă ambele sectoare deodată. Cele două porți fuzionează într-un singur 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>
Pe sector de spin
Aceasta este aceeași formă, dar restrânsă la o jumătate de spin. Aici, două perechi independente inițializare-apoi-rotație, câte una pe sector, fuzionează fiecare separat într-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>
Inițializare globală, rotații pe spin
Aceasta presupune un singur InitializeModes pe registrul complet (2 * norb), urmat de două acțiuni OrbitalRotation, câte una pe fiecare jumătate contiguă de spin. Pasul împarte ocuparea globală la granița dintre sectoare și emite două porți PrepareSlaterDeterminant. Cele două rotații pot apărea în oricare ordine.
Aceasta este forma produsă de fluxul de lucru local unitary cluster Jastrow (LUCJ) (vezi ghidul LUCJ); un utilizator plasează un InitializeModes (de obicei referința Hartree-Fock, folosind from_hartree_fock()) la începutul circuitului și adaugă un ansatz UCJ, ale cărui primele două rotații de orbitali pe spin urmează direct inițializării după ce ansatzul este descompus.
>>> 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>
Acest tipar se declanșează și când doar o jumătate de spin este rotită de o poartă ce urmează direct inițializării, forma produsă atunci când rotația de orbitali a unui singur sector de spin se află la începutul circuitului. Jumătatea rotită devine un PrepareSlaterDeterminant cu rotația ei, iar cealaltă jumătate este pregătită cu o rotație identitate. Pregătirea identitate se sintetizează în porțile X de referință pe care InitializeModes le-ar fi emis oricum pentru acea jumătate, deci completarea ei nu costă porți suplimentare, deblocând totodată sinteza Slater redusă pe jumătatea rotită:
>>> 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>
Ce rămâne neatins
Pasul este deliberat conservator. Fuzionează doar când cele două porți se compun cu adevărat într-o pregătire de determinant Slater și copiază neschimbat tot restul. „Imediat urmat” se înțelege peste graful de flux de date al circuitului. OrbitalRotation fuzionează doar când InitializeModes este singurul său predecesor pe toate modurile sale. În figurile de mai jos, înainte și după sunt identice: nimic nu a fuzionat.
O operație între ele
Dacă vreo poartă atinge modurile dintre inițializare și rotație, acestea nu se mai compun într-o pregătire simplă, așa că nu se fuzionează nimic:
>>> 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>
O rotație parțială care nu este o jumătate de spin
O rotație care acoperă exact o jumătate contiguă de spin a unei inițializări pe registrul complet fuzionează (vezi secțiunea anterioară). Dar o rotație pe orice altă mulțime parțială de moduri (mai puține moduri decât inițializarea și nu una dintre cele două jumătăți de spin ale ei, cum ar fi un subinterval care traversează granița dintre sectoare) nu este o pregătire pe sector. Fuzionarea ei ar elimina în tăcere rolul inițializării asupra modurilor lăsate pe dinafară, așa că pasul lasă ambele porți pe loc:
>>> 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>
De ce contează: beneficiul sintezei
Fuzionarea merită pentru că PrepareSlaterDeterminant se sintetizează în mult mai puține porți decât OrbitalRotation pe care îl absoarbe. Un determinant cu electroni peste orbitali necesită cel mult rotații pe doi qubiți și nicio poartă de fază diagonală, față de cele rotații plus faze ale rotației complete pătrate de orbitali.
Managerul de pași Jordan-Wigner prestabilit rulează MergeSlaterDeterminantPreparation în etapa sa de optimizare, deci această reducere are loc automat. Transpilează o pregătire cu doi electroni și șase orbitali până la un QuantumCircuit și numără-i porțile:
Presetul rulează MergeOrbitalRotations imediat înainte de MergeSlaterDeterminantPreparation. Acest lucru contează când o serie de porți OrbitalRotation consecutive urmează inițializării. Pasul anterior comprimă mai întâi seria într-o singură rotație, pe care MergeSlaterDeterminantPreparation o contractă apoi cu inițializarea într-un singur PrepareSlaterDeterminant. Fără această ordine, doar prima rotație din serie ar fi absorbită, iar restul s-ar sintetiza cu descompunerea pătrată completă.
>>> 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>
Compară asta cu sintetizarea aceleiași rotații de orbitali de una singură. Fără o inițializare precedentă nu există nimic de fuzionat, așa că presetul revine la descompunerea pătrată completă, care este mai adâncă și conține porți de fază diagonale:
>>> 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>

Descompunerea redusă elimină toate cele șase porți de fază și înjumătățește aproape numărul de rotații pe doi qubiți. Determinantul pregătit este corect până la o fază globală (irelevantă fizic pentru pregătirea stării), exact ceea ce ar fi fixat porțile de fază.
Două moduri de a descompune poarta
Un PrepareSlaterDeterminant poate fi descompus în două moduri diferite, iar acestea nu sunt interschimbabile:
decompose()
Extinde poarta în definiția ei: InitializeModes al referinței urmat de OrbitalRotation complet. Aceasta este echivalentă cu poarta ca operator, pentru orice stare de intrare.
PrepareSlaterDeterminant.synthesis, aplicată de F2FSynthesis
Înlocuiește poarta cu descompunerea redusă din acest ghid. Aceasta este echivalentă cu poarta doar ca pregătire de stare: produce același determinant, până la o fază globală, când modurile pornesc din vid. Este mai ieftină tocmai pentru că presupune acest lucru.
Folosește sinteza când poarta pregătește o stare pe vid, ceea ce este scopul porții și ceea ce face transpilerul pentru tine. Folosește decompose() când poarta trebuie să continue să acționeze ca operator, de exemplu pentru că alte porți o preced pe aceleași moduri, sau când vrei să inspectezi ori să simulezi porțile simple. Pasul F2FSynthesis nu poate ști în ce caz se află, deci depinde de circuitul pe care i-l dai să pregătească determinantul din vid.
Simularea unui circuit cu ffsim nu are nevoie de niciuna: fiecare poartă își aplică întreaga unitară, deci un circuit care conține un PrepareSlaterDeterminant este simulat ca atare și rămâne pe calea rapidă pentru porțile OrbitalRotation din jurul lui. Descompunerea doar a porții Slater cu circuit.decompose("PrepareSlaterDeterminant") lasă și acele rotații întregi.
PrepareSlaterDeterminant, InitializeModes, OrbitalRotation, MergeOrbitalRotations, F2FSynthesis, FermionicSlaterGivensDecomposition, și ghidul LUCJ pentru fluxul de lucru care produce tiparul de inițializare globală.
