Genera circuiti SqDRIFT
Con la diagonalizzazione quantistica basata su campioni (SQD) devi scegliere un ansatz da cui campionare le bitstring. La variante SqDRIFT usa invece un ensemble di circuiti di evoluzione temporale costruiti direttamente dall'Hamiltoniana bersaglio. Ciò si ottiene sottocampionando dall'Hamiltoniana operatori di evoluzione temporale più piccoli in base ai suoi coefficienti, ed è noto come metodo di trotterizzazione qDRIFT.
Questa guida introduttiva mostra come generare un ensemble di tali circuiti randomizzati.
1. Configurazione dell'Hamiltoniana
Ai fini di questa guida, carica l'Hamiltoniana di struttura elettronica di N2 da un file FCIDUMP. Esistono altri modi per costruire la FermionOperator. Consulta la sua documentazione, così come il modulo qiskit_fermions.operators.library.
- PYTHON
- C
>>> from qiskit_fermions.operators.library import FCIDump
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> fcidump = FCIDump.from_file("docs/guides/n2.fcidump")
>>> num_modes = 2 * fcidump.norb
>>> hamil = FermionOperator.from_fcidump(fcidump)
#include <qiskit_fermions.h>
QfFCIDump* fcidump = NULL;
qf_fcidump_from_file("docs/guides/n2.fcidump", &fcidump);
QfFermionOperator* hamil = qf_ferm_op_from_fcidump(fcidump);
uint32_t num_modes = 2 * qf_fcidump_norb(fcidump);
2. Raggruppa i termini dell'Hamiltoniana
Sfrutta le numerose simmetrie presenti nell'Hamiltoniana di struttura elettronica raggruppando i termini correlati che hanno coefficienti identici. Questa azione modifica la distribuzione dei coefficienti dell'operatore da cui il protocollo qDRIFT campiona, ma non ne influisce le garanzie di convergenza. Fatto cruciale, raggruppare i termini correlati da simmetria produce una cancellazione favorevole dei termini di Pauli, con una profondità complessiva del circuito inferiore quando si fa evolvere nel tempo uno stato sotto la loro azione.
Il modulo qiskit_fermions.operators.terms.grouping fornisce funzioni di comodo per raggruppare i termini di un operatore. Questo è spiegato più in dettaglio in questa guida.
L'implementazione di group_terms_by_electronic_structure() presuppone che i termini dell'Hamiltoniana siano in ordine normale!
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.grouping import group_terms_by_electronic_structure
>>> from qiskit_fermions.operators.terms.ordering import canonical_order
>>>
>>> canon = canonical_order(hamil.normal_ordered().simplify(atol=1e-16))
>>> exit_code = group_terms_by_electronic_structure(canon, num_modes, two_body_physicist_order=False)
>>> assert exit_code is None
>>> print(canon.groups) # the groups attribute now contains some list of group indices
[0, ...]
QfFermionOperator* normal = qf_ferm_op_normal_ordered(hamil, NULL);
QfFermionOperator* simplified = qf_ferm_op_simplify(normal, 1e-16);
QfFermionOperator* canon = qf_ferm_op_canonical_order(simplified);
QfExitCode exit = qf_ferm_op_group_terms_by_electronic_structure(canon, num_modes, false);
assert(exit == QfExitCode_Success);
qf_ferm_op_free(normal);
qf_ferm_op_free(simplified);
L'Hamiltoniana di struttura elettronica completa contiene alcuni termini la cui inclusione in un circuito di evoluzione temporale non ha alcun impatto sulle bitstring osservate e, quindi, comporta solo un maggiore overhead di campionamento. Si raccomanda pertanto di filtrare tali termini dall'Hamiltoniana a questo punto, prima di costruire il gate Evolution nel passo successivo.
I termini che corrispondono a questa descrizione sono quelli diagonali nella base dei numeri di occupazione, cioè i prodotti di operatori numero (). Ciò include l'offset costante di energia, la cui evoluzione temporale introduce solo una fase globale nel circuito, i singoli operatori numero la cui evoluzione temporale equivale a rotazioni Z su singolo qubit, nonché prodotti di ordine superiore come . Nessuno di essi può modificare l'occupazione di un modo, quindi eliminarli non comporta alcuna perdita di contenuto di eccitazione. A differenza del rifiuto per estrazione del passo 5, questo filtraggio avviene una sola volta, sull'Hamiltoniana stessa, quindi la distribuzione dei coefficienti da cui il protocollo campiona viene ricostruita in modo coerente dall'operatore filtrato anziché rinormalizzata a metà estrazione.
La funzione filter_diagonal_terms() rimuove tali termini da un operatore sul posto:
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.filtering import filter_diagonal_terms
>>>
>>> filter_diagonal_terms(canon)
qf_ferm_op_filter_diagonal_terms(canon);
Filtrare qui, una sola volta, è considerevolmente più economico che filtrare ripetutamente. FermionicQDrift viene eseguito una volta per circuito transpilato, quindi filtrare l'Hamiltoniana all'inizio, anziché a ogni chiamata, evita di rifare il lavoro per ogni circuito generato da essa.
Per lo stesso motivo conviene ordinare qui i termini dell'Hamiltoniana per indice di gruppo. Gli indici di gruppo sono un'etichetta per termine che non dice nulla su dove si trovino quei termini, quindi i termini di un gruppo sono in generale sparsi nell'operatore: il raggruppamento applicato sopra assegna gli indici in ordine di prima comparsa, il che lascia il secondo termine di un gruppo lontano dal primo.
FermionicQDrift campiona gruppi interi e cerca ogni gruppo estratto con split_out_groups(). Trovare un gruppo tra termini sparsi significa scorrerli tutti, quindi quella ricerca costa lo stesso che si estragga un gruppo o quaranta. group_order() raccoglie ogni gruppo in un'unica sequenza contigua, dopodiché la ricerca è una ricerca binaria sui confini dei gruppi e costa quanto costano i gruppi estratti anziché i termini posseduti. La ricerca in sé diventa drasticamente più economica; quanto questo faccia risparmiare in una data transpilazione dipende da cos'altro fa la pipeline, poiché mappatura e sintesi dominano una volta eliminata la scansione:
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.ordering import group_order
>>>
>>> canon = group_order(canon)
>>> print(canon.groups[:4])
[0, 0, ...]
QfFermionOperator* grouped = qf_ferm_op_group_order(canon);
Questo riordina i termini senza cambiare il valore dell'operatore, e va fatto una sola volta, indipendentemente da quante randomizzazioni del circuito vengano estratte da esso.
3. Prepara il circuito di evoluzione temporale
Prepara il circuito di evoluzione temporale dell'Hamiltoniana e il circuito di base da cui estrarre i campioni. Il modulo qiskit_fermions.circuit.library contiene tutti i componenti necessari per farlo, nel rispetto delle convenzioni di Qiskit.
Il protocollo qDRIFT è il metodo di sintesi FermionicQDrift, e il gate Evolution lo porta come proprio synthesis. Il metodo introduce i pochi parametri che puoi usare per personalizzare i circuiti da generare:
-
La lunghezza di ciascun circuito in termini di gruppi di eccitazione:
num_groups -
Facoltativamente, un seme per il generatore di numeri casuali:
rng -
Facoltativamente, pesi di campionamento precalcolati, se la transpilazione stessa diventa un collo di bottiglia:
weights(vedi (Facoltativo) Precalcola i pesi di campionamento più avanti)
Poiché il metodo è tenuto dal gate, tutto ciò che descrive l'operatore del gate, come tali pesi, si trova proprio accanto a quell'operatore.
- PYTHON
- C
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import FermionicQDrift
>>>
>>> time = 1.0 # you can choose a desired scaling factor here
>>> num_groups = 10
>>> qdrift = FermionicQDrift(num_groups, rng=19)
>>> evo_gate = Evolution(num_modes, canon, time, synthesis=qdrift)
>>>
>>> circ = FermionicCircuit(num_modes)
>>> circ.append(evo_gate, circ.modes)
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions.
Questo esempio non inizializza i modi fermionici con particelle, né ne misura lo stato finale.
4. Transpila il circuito
Il modulo qiskit_fermions.transpiler si integra direttamente con la pipeline di transpilazione di Qiskit, consentendo di transpilare direttamente in una QuantumCircuit la FermionicCircuit costruita sopra.
Usa la mappatura fermione-qubit jordan_wigner() per convertire l'Hamiltoniana espressa in termini di fermioni in stringhe di Pauli. Questo può essere fatto direttamente come parte del processo di transpilazione usando il pass di transpilazione F2QSynthesis. Usa generate_preset_jw_pass_manager() per costruire FermionicStagedPassManager, che garantisce che la codifica di Jordan-Wigner sia usata in modo coerente per tutte le istruzioni del circuito.
Fatto cruciale, aggiungi il pass Decompose di Qiskit alla fase optimization della pipeline di transpilazione. Scompone il gate Evolution con il metodo di sintesi che il gate porta, ovvero il metodo qDRIFT preparato sopra. Questo garantisce che il circuito non usi l'evoluzione temporale dell'intera Hamiltoniana, la cui profondità supererebbe le capacità dell'hardware di calcolo quantistico attualmente disponibile.
Invece, sottocampiona un numero fisso di groups di termini dell'Hamiltoniana per ciascun circuito, ogni volta che il circuito viene transpilato. In questo modo puoi generare più randomizzazioni del circuito, come richiesto dall'algoritmo SqDRIFT, eseguendo ripetutamente la pipeline di transpilazione. Il numero di circuiti da generare è num_sqdrift_randomizations.
- PYTHON
- C
>>> from qiskit.transpiler.passes import Decompose
>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> pm.optimization = FermionicPassManager([Decompose(["Evolution"])])
>>>
>>> num_sqdrift_randomizations = 10
>>> sqdrift_circuits = [
... pm.run(circ) for _ in range(num_sqdrift_randomizations)
... ]
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Gli esempi precedenti fissano il seme rng del metodo di sintesi FermionicQDrift, in modo che la sequenza di randomizzazioni sia riproducibile.
La stessa pipeline accetta il metodo qDRIFT in altri due punti. Il pass F2FSynthesis prende un metodo per ogni gate Evolution per nome, F2FSynthesis({"Evolution": FermionicQDrift(num_groups)}), che è tutto ciò che serve quando nessun gate contiene qualcosa di specifico per il proprio operatore. Il passo successivo usa il terzo, il pass QDriftTrotterization.
5. Filtra le eccitazioni estratte per occupazione
Oltre ai termini diagonali filtrati nel passo precedente, un'eccitazione estratta può agire interamente all'interno di un insieme di modi la cui occupazione è già fissata (tutti occupati o tutti non occupati), dove non può spostare una particella da un lato all'altro. Una tale eccitazione non ti dice nulla sulle bitstring campionate, quindi uno slot speso per essa è uno slot sprecato. Il pass QDriftTrotterization, che incapsula lo stesso metodo FermionicQDrift, rifiuta tali eccitazioni man mano che vengono estratte e ne estrae una sostitutiva, così che ciascuno dei num_groups slot del circuito risultante porti un'eccitazione che accoppia i due lati.
Questo filtraggio non è gratuito, ed è per questo un pass separato. Ciò che conserva è il budget di num_groups slot campionati, non la profondità del circuito: poiché quel budget è fisso, un'eccitazione rifiutata viene sostituita anziché scartata, e una economica cede il posto a un'eccitazione di accoppiamento che costa di più da sintetizzare. Aspettati che un circuito filtrato sia più profondo di uno non filtrato estratto dallo stesso seme.
Rinormalizzare l'estrazione sulle eccitazioni accettate modifica la distribuzione da cui il protocollo qDRIFT campiona e, a differenza del raggruppamento del passo 2, rompe anche la garanzia di convergenza del protocollo: il prodotto campionato non ha più come media l'evoluzione sotto l'Hamiltoniana che hai passato. Ogni eccitazione mantenuta finisce pesata per il reciproco della probabilità di accettazione, quelle rifiutate non contribuiscono con nulla, e né la magnitudine totale dei coefficienti né il tempo di evoluzione per gate vengono adeguati per compensare.
Anche la regola di accettazione è una sovrapprossimazione. Confronta solo i supporti dei modi, quindi non porta alcuna informazione di ampiezza, e gli insiemi tracciati non fanno che crescere: un'eccitazione confinata all'insieme occupato è davvero inerte su uno stato prodotto, ma lo stesso ragionamento si indebolisce per le estrazioni successive, quando le eccitazioni accettate in precedenza hanno lasciato lo stato entangled e hanno marcato la maggior parte dei modi come “incerti”.
Usa il filtraggio per il flusso di lavoro di campionamento di bitstring descritto in questa guida, dove i circuiti alimentano il post-processing SQD e contano solo le bitstring campionate. Non usarlo se intendi stimare un valore di aspettazione da questi circuiti, o fare affidamento in altro modo sul limite di errore di qDRIFT: quei risultati portano un bias che il pass non corregge.
Questo filtraggio deve sapere quali modi partono occupati. Richiede quindi un gate InitializeModes che preceda i gate Evolution nel circuito; InitializeModes.from_hartree_fock() è un modo comodo per costruirne uno. Aggiungine uno qui per il riferimento di Hartree-Fock di N2 (sette elettroni alfa e sette beta in 14 orbitali spaziali) e confronta le eccitazioni campionate con e senza il filtraggio:
- PYTHON
- C
>>> from qiskit_fermions.circuit.library import InitializeModes
>>> from qiskit_fermions.transpiler.passes import F2FSynthesis, QDriftTrotterization
>>>
>>> init = InitializeModes.from_hartree_fock(fcidump.norb, (7, 7))
>>>
>>> hf_circ = FermionicCircuit(num_modes)
>>> hf_circ.append(init, hf_circ.modes)
>>> hf_circ.append(Evolution(num_modes, canon, time), hf_circ.modes)
>>>
>>> num_groups = 5
>>> qdrift_unfiltered = F2FSynthesis(
... {"Evolution": FermionicQDrift(num_groups, rng=3480)}
... )
>>> qdrift_trivial = QDriftTrotterization(
... FermionicQDrift(num_groups, rng=3480)
... )
>>>
>>> for instruction in FermionicPassManager(qdrift_unfiltered).run(hf_circ)._inner.data:
... if instruction.operation.name == "Evolution":
... print(sorted(instruction.operation.operator.get_support()))
[2, 4]
[41, 45, 52]
[15, 45, 55]
[41, 52, 53]
[10, 16, 37, 38]
>>>
>>> for instruction in FermionicPassManager(qdrift_trivial).run(hf_circ)._inner.data:
... if instruction.operation.name == "Evolution":
... print(sorted(instruction.operation.operator.get_support()))
[0, 1, 6, 7]
[0, 1, 28, 29]
[4, 13, 55]
[13, 20, 40, 41]
[0, 1, 28, 29]
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Nessuna delle eccitazioni estratte senza filtraggio tocca affatto l'insieme occupato (0-6 e 28-34), quindi nessuna di esse può spostare una particella tra un modo occupato e uno non occupato. Con il filtraggio, tutte e cinque vengono rifiutate e sostituite da eccitazioni che accoppiano davvero un modo occupato con uno non occupato. Per esempio, la prima eccitazione accettata [0, 1, 6, 7] sposta una particella tra i modi occupati 0, 1 e 6 e il modo non occupato 7.
Una volta accettata un'eccitazione, ogni modo nel suo supporto diventa “incerto” e, quindi, idoneo a ricoprire entrambi i ruoli per i campioni successivi, così gli insiemi di modi occupati e non occupati continuano a crescere man mano che vengono accettate più eccitazioni. Questo è ciò che rende accettabile la seconda eccitazione, [0, 1, 28, 29]. Tutti e quattro i suoi modi sono tra quelli originariamente occupati, quindi non si accoppia ad alcun modo originariamente non occupato. Viene accettata solo perché i modi 0 e 1 sono diventati incerti (e quindi idonei come lato “non occupato” dell'accoppiamento) una volta toccati dalla prima eccitazione.
Questa crescita è ciò che rende la regola una sovrapprossimazione anziché un test: man mano che più modi diventano incerti, vengono rifiutate meno estrazioni, e il filtraggio incide di più sulla distribuzione di campionamento nelle prime estrazioni.
Per valutare quanto il filtraggio abbia spostato la distribuzione, leggi i conteggi di rifiuto che il pass registra nei metadati del circuito. Viene memorizzata una voce per ogni gate Evolution filtrato, e il rapporto tra accettate e totali stima la probabilità di accettazione, che è il fattore per cui le eccitazioni mantenute sono state sovrapesate:
- PYTHON
- C
>>> # a freshly seeded pass, so the counts do not depend on earlier draws
>>> qdrift_counted = QDriftTrotterization(
... FermionicQDrift(num_groups, rng=3480)
... )
>>> filtered = FermionicPassManager(qdrift_counted).run(hf_circ)
>>> discarded = filtered.metadata.get("filter_trivial.discarded")
>>> emitted = filtered.metadata.get("filter_trivial.emitted")
>>> print(f"{emitted[0]} kept, {discarded[0]} discarded")
5 kept, 7 discarded
>>> print(f"acceptance probability: {emitted[0] / (emitted[0] + discarded[0]):.2f}")
acceptance probability: 0.42
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Il filtraggio ha bisogno di informazioni di occupazione su cui filtrare. Un gate PrepareSlaterDeterminant le fornisce proprio come fa InitializeModes, e qualsiasi OrbitalRotation marca come “incerto” ogni modo su cui agisce. Senza nessuno di essi, o quando i modi inizializzati finiscono tutti su un solo lato (ogni modo occupato, o ogni modo non occupato), il pass emette un UserWarning e lascia il campionamento non filtrato per quel gate Evolution. Su un'Hamiltoniana i cui termini rimanenti non possono più accoppiare i due insiemi tracciati, il pass solleva invece RuntimeError dopo QDriftTrotterization.MAX_SAMPLE_RETRIES rifiuti consecutivi.
(Facoltativo) Precalcola i pesi di campionamento
Ricorri a questo solo se la transpilazione stessa è un collo di bottiglia. Non cambia nulla dei circuiti che ottieni.
FermionicQDrift estrae ogni gruppo con una probabilità proporzionale alla magnitudine del suo coefficiente, e ricava tali magnitudini dall'Hamiltoniana a ogni chiamata: un valore per termine dell'Hamiltoniana, ridotto a uno per gruppo. Poiché il metodo è privo di stato, ripete quella riduzione per ogni randomizzazione, anche se il risultato è ogni volta identico. L'argomento weights ti permette di calcolarlo una volta con group_coeff_means() e di passare il risultato, spostando il costo fuori dal ciclo e lasciando il metodo libero da qualsiasi stato in cache dipendente dall'Hamiltoniana.
Questo è il corrispettivo dell'ordinamento dei termini nel passo 2. Quello porta fuori dal ciclo la ricerca dei gruppi, questo la riduzione dei pesi, e ciascuno è percepibile solo quanto l'altro è assente, quindi conviene applicarli insieme. Poiché canon è già stato riordinato sopra, i due si compongono direttamente.
I pesi descrivono i gruppi di un particolare operatore, quindi appartengono al gate Evolution di quell'operatore: costruisci il metodo per quel gate e passalo come synthesis del gate, esattamente come nel passo 3.
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.grouping import group_coeff_means
>>>
>>> weights = group_coeff_means(canon) # `canon` is group-ordered, from step 2
>>> qdrift_weighted = FermionicQDrift(10, rng=19, weights=weights)
>>> weighted_gate = Evolution(num_modes, canon, time, synthesis=qdrift_weighted)
>>>
>>> weighted_circ = FermionicCircuit(num_modes)
>>> weighted_circ.append(weighted_gate, weighted_circ.modes)
>>>
>>> weighted_circuits = [
... pm.run(weighted_circ) for _ in range(num_sqdrift_randomizations)
... ]
>>> len(weighted_circuits)
10
// WARNING: Qiskit's C API does not yet allow us to implement circuits
// with custom gate definitions, which we therefore also cannot transpile
// via this API.
Poiché questi sono esattamente i pesi che il pass avrebbe calcolato, i circuiti sono gli stessi di prima; scompare solo la riduzione ripetuta.
Si attende una voce per gruppo, non per termine, ogni volta che l'operatore porta dei gruppi, poiché un operatore raggruppato viene campionato per gruppi. Le voci devono essere non negative: un peso è una magnitudine, perché il protocollo qDRIFT scompone con positivi, e il segno di un coefficiente appartiene a , dove il pass lo legge direttamente dall'operatore.
I pesi sono magnitudini assolute, non preferenze relative. La loro somma fissa anche il tempo di evoluzione, quindi moltiplicare ogni voce per campiona in modo identico ma evolve per anziché per . Passare qualcosa di diverso dalle magnitudini dei coefficienti propri dell'Hamiltoniana cambia quindi quale evoluzione l'ensemble approssima, ed è per questo che questo è uno strumento di prestazioni e non un modo per regolare il campionamento.
Poiché i pesi appartengono al gate il cui operatore descrivono, un circuito può contenere più gate Evolution, ciascuno con i pesi del proprio operatore. Il pass QDriftTrotterization del passo 5 li rispetta allo stesso modo: estrae un gate con il FermionicQDrift che il gate porta, e ricade su quello con cui è stato costruito per i gate che non ne portano alcuno. Un altro tipo di metodo di sintesi su un gate non è un'affermazione su qDRIFT, quindi il pass lo ignora. Passare il metodo a un pass per nome del gate, come in F2FSynthesis({"Evolution": ...}), applica un solo metodo a ogni gate Evolution, quindi i pesi passati lì hanno senso solo per un circuito con un unico gate. La lunghezza dei pesi viene verificata rispetto a ciascun gate a cui vengono applicati.
(Facoltativo) Ottimizza l'indicizzazione dei modi fermionici
Puoi aggiungere alla pipeline di transpilazione un ulteriore passo di ottimizzazione che minimizza la distanza degli intervalli di eccitazione fermionica rietichettando gli indici dei modi fermionici. Questa ottimizzazione è stata introdotta nell'articolo SqDRIFT ed è implementata da build_excitation_span_minimization_model(). Può essere inserita facilmente nella pipeline del transpiler usando il pass RelabelModes:
- PYTHON
- C
>>> from pyomo.environ import SolverFactory
>>> from qiskit_fermions.transpiler.passes import RelabelModes
>>>
>>> solver = SolverFactory("appsi_highs")
>>> solver.options["time_limit"] = 10
>>>
>>> qdrift = F2FSynthesis({"Evolution": FermionicQDrift(5, rng=19)})
>>> relabel = RelabelModes(solver=solver)
>>>
>>> pm.optimization = FermionicPassManager([qdrift, relabel])
>>>
>>> relabeled_circ = pm.run(circ)
>>> # if the automatic mode relabeling was successful, the circuit's
>>> # metadata will contain the mode `permutation` information
// WARNING: This feature is not available via the C API.
Usare l'ottimizzazione automatica all'interno di RelabelModes (che sfrutta build_excitation_span_minimization_model()) richiede la dipendenza opzionale gestita da HAS_PYOMO.
Il rietichettamento cambia quale modo porta un dato qubit, quindi la diagonalizzazione del sottospazio ottiene le bitstring corrette solo se se ne tiene conto. Dove avviene questa contabilizzazione dipende da dove misuri.
Aggiungi le misure alla FermionicCircuit con FermionicCircuit.measure_all() prima di transpilare, e non c'è nulla da fare: ogni misura nomina il modo che legge, quindi il rietichettamento la porta con sé e le bitstring campionate sono indicizzate per il modo originale.
Se invece misuri la QuantumCircuit transpilata, le bitstring devono essere post-elaborate usando la permutation registrata nei metadati del circuito. Consulta la documentazione di RelabelModes per la ricetta.
Prossimi passi
Ora che hai generato con successo un ensemble di circuiti, puoi campionare bitstring da essi. Per farlo, i circuiti devono essere eseguiti su hardware. Consulta la documentazione di Qiskit per istruzioni dettagliate.
Una volta ottenuti i campioni di bitstring, questi possono essere usati insieme ai coefficienti dell'Hamiltoniana per eseguire il post-processing SQD, come spiegato nei tutorial dell'addon SQD.