SqDRIFT-Circuits erzeugen
Bei der Sample-based Quantum Diagonalization (SQD) musst du einen Ansatz wählen, aus dem Bitstrings gesampelt werden. Die Variante SqDRIFT verwendet stattdessen ein Ensemble von Zeitentwicklungs-Circuits, die direkt aus dem Ziel-Hamiltonoperator konstruiert werden. Dies geschieht, indem kleinere Zeitentwicklungsoperatoren anhand der Koeffizienten aus dem Hamiltonoperator unterabgetastet werden, was als qDRIFT-Trotterisierungsmethode bekannt ist.
Dieser Einstiegsleitfaden zeigt, wie du ein Ensemble solcher randomisierten Circuits erzeugst.
1. Einrichtung des Hamiltonoperators
Lade für die Zwecke dieses Leitfadens den Elektronenstruktur-Hamiltonoperator von N2 aus einer FCIDUMP-Datei. Es gibt weitere Möglichkeiten, den FermionOperator zu konstruieren. Sieh dir unbedingt seine Dokumentation sowie qiskit_fermions.operators.library an.
- 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. Terme des Hamiltonoperators gruppieren
Nutze die vielen Symmetrien im Elektronenstruktur-Hamiltonoperator, indem du verwandte Terme mit identischen Koeffizienten gruppierst. Diese Maßnahme verändert die Verteilung der Operatorkoeffizienten, aus der das qDRIFT-Protokoll sampelt, beeinträchtigt aber seine Konvergenzgarantien nicht. Entscheidend ist: Das Gruppieren von Termen, die durch Symmetrie zusammenhängen, führt zu einer günstigen Auslöschung von Pauli-Termen, was insgesamt eine geringere Circuit-Tiefe ergibt, wenn ein Zustand unter ihrer Wirkung zeitentwickelt wird.
Das Modul qiskit_fermions.operators.terms.grouping stellt Komfortfunktionen zum Gruppieren der Terme eines Operators bereit. Das wird in diesem Leitfaden ausführlicher erklärt.
Die Implementierung von group_terms_by_electronic_structure() setzt voraus, dass die Terme des Hamiltonoperators normalgeordnet sind!
- 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);
Der vollständige Elektronenstruktur-Hamiltonoperator enthält bestimmte Terme, deren Aufnahme in einen Zeitentwicklungs-Circuit keinen Einfluss auf die wahrgenommenen Bitstrings hat und daher nur den Sampling-Overhead erhöht. Es wird deshalb empfohlen, solche Terme an dieser Stelle aus dem Hamiltonoperator herauszufiltern, bevor im nächsten Schritt das Evolution-Gate konstruiert wird.
Die Terme, auf die diese Beschreibung zutrifft, sind diejenigen, die in der Besetzungszahlbasis diagonal sind, also die Produkte von Zahloperatoren (). Dazu gehören der konstante Energie-Offset, dessen Zeitentwicklung nur eine globale Phase in den Circuit einbringt, die einzelnen Zahloperatoren, deren Zeitentwicklung auf Ein-Qubit-Z-Rotationen hinausläuft, sowie Produkte höherer Ordnung wie . Keiner von ihnen kann die Besetzung eines Modus ändern, sodass das Weglassen keinen Anregungsgehalt kostet. Anders als die Zurückweisung pro Ziehung in Schritt 5 geschieht diese Filterung einmalig am Hamiltonoperator selbst, sodass die Koeffizientenverteilung, aus der das Protokoll sampelt, konsistent aus dem gefilterten Operator neu aufgebaut wird, statt mitten in der Ziehung renormalisiert zu werden.
Die Funktion filter_diagonal_terms() entfernt solche Terme direkt (in place) aus einem Operator:
- PYTHON
- C
>>> from qiskit_fermions.operators.terms.filtering import filter_diagonal_terms
>>>
>>> filter_diagonal_terms(canon)
qf_ferm_op_filter_diagonal_terms(canon);
Hier einmalig zu filtern ist deutlich günstiger, als wiederholt zu filtern. FermionicQDrift läuft einmal pro transpiliertem Circuit. Wenn du den Hamiltonoperator also zu Beginn filterst, statt bei jedem Aufruf, vermeidest du, die Arbeit für jeden daraus erzeugten Circuit zu wiederholen.
Aus demselben Grund lohnt es sich, die Terme des Hamiltonoperators hier nach Gruppenindex zu ordnen. Gruppenindizes sind ein Tag pro Term, der nichts darüber aussagt, wo diese Terme stehen, sodass die Terme einer Gruppe im Allgemeinen über den ganzen Operator verstreut sind: Die oben angewandte Gruppierung vergibt Indizes in der Reihenfolge des ersten Auftretens, wodurch der zweite Term einer Gruppe weit von ihrem ersten entfernt liegt.
FermionicQDrift sampelt ganze Gruppen und schlägt jede gezogene Gruppe mit split_out_groups() nach. Eine Gruppe unter verstreuten Termen zu finden bedeutet, alle zu durchsuchen, sodass dieses Nachschlagen gleich viel kostet, egal ob eine Gruppe gezogen wird oder vierzig. group_order() fasst jede Gruppe zu einem zusammenhängenden Abschnitt zusammen, danach ist ein Nachschlagen eine binäre Suche über die Gruppengrenzen und kostet so viel wie die gezogenen Gruppen statt wie die gehaltenen Terme. Das Nachschlagen selbst wird dramatisch günstiger; wie viel das bei einer konkreten Transpilation einspart, hängt davon ab, was die Pipeline sonst noch tut, da Mapping und Synthese dominieren, sobald das Durchsuchen entfällt:
- 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);
Das ordnet die Terme um, ohne den Wert des Operators zu ändern, und muss nur einmal erfolgen, unabhängig davon, wie viele Circuit-Randomisierungen daraus gezogen werden.
3. Den Zeitentwicklungs-Circuit vorbereiten
Bereite den Zeitentwicklungs-Circuit des Hamiltonoperators und den Basis-Circuit vor, aus dem Samples gezogen werden. Die qiskit_fermions.circuit.library enthält alle dafür nötigen Komponenten, im Einklang mit den Qiskit-Konventionen.
Das qDRIFT-Protokoll ist die Synthesemethode FermionicQDrift, und das Evolution-Gate trägt sie als seine synthesis. Die Methode führt die wenigen Parameter ein, mit denen du die zu erzeugenden Circuits anpassen kannst:
-
Die Länge jedes Circuits in Anregungsgruppen:
num_groups -
Optional ein Seed für den Zufallszahlengenerator:
rng -
Optional vorberechnete Sampling-Gewichte, falls die Transpilation selbst zum Engpass wird:
weights(siehe (Optional) Die Sampling-Gewichte vorberechnen weiter unten)
Weil die Methode vom Gate gehalten wird, steht alles, was das Gate beschreibt, etwa Operatoren wie diese Gewichte, direkt neben diesem Operator.
- 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.
Dieses Beispiel initialisiert die fermionischen Modi weder mit Teilchen noch misst es ihren Endzustand.
4. Den Circuit transpilieren
Das Modul qiskit_fermions.transpiler integriert sich direkt in die Transpilations-Pipeline von Qiskit, sodass der oben konstruierte FermionicCircuit direkt in einen QuantumCircuit transpiliert werden kann.
Verwende die Fermion-zu-Qubit-Abbildung jordan_wigner(), um den in Fermionen ausgedrückten Hamiltonoperator stattdessen in Pauli-Strings auszudrücken. Das lässt sich direkt als Teil des Transpilationsprozesses mit dem Transpilations-Pass F2QSynthesis erledigen. Verwende generate_preset_jw_pass_manager(), um FermionicStagedPassManager zu bauen, der sicherstellt, dass die Jordan-Wigner-Kodierung für alle Circuit-Anweisungen konsistent verwendet wird.
Füge unbedingt den Pass Decompose von Qiskit zur Stufe optimization der Transpilations-Pipeline hinzu. Er zerlegt das Evolution-Gate mit der Synthesemethode, die das Gate trägt, nämlich der oben vorbereiteten qDRIFT-Methode. Dadurch wird sichergestellt, dass der Circuit nicht die Zeitentwicklung des gesamten Hamiltonoperators verwendet, deren Tiefe die Möglichkeiten der derzeit verfügbaren Quantencomputer-Hardware übersteigen würde.
Stattdessen werden für jeden Circuit bei jeder Transpilation eine feste Anzahl von groups von Hamiltonoperator-Termen unterabgetastet. Dadurch kannst du mehrere Circuit-Randomisierungen, wie vom Algorithmus SqDRIFT verlangt, erzeugen, indem du die Transpilations-Pipeline wiederholt ausführst. Die Anzahl der zu erzeugenden Circuits ist 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.
Die vorangegangenen Beispiele legen den rng-Seed der Synthesemethode FermionicQDrift fest, damit die Abfolge der Randomisierungen reproduziert werden kann.
Dieselbe Pipeline akzeptiert die qDRIFT-Methode an zwei weiteren Stellen. Der Pass F2FSynthesis nimmt eine Methode für jedes Evolution-Gate über den Namen entgegen, F2FSynthesis({"Evolution": FermionicQDrift(num_groups)}), was alles ist, was nötig ist, wenn kein Gate etwas Spezifisches zu seinem Operator enthält. Der nächste Schritt verwendet die dritte, den Pass QDriftTrotterization.
5. Gezogene Anregungen nach Besetzung filtern
Über die im vorherigen Schritt herausgefilterten diagonalen Terme hinaus kann eine gezogene Anregung vollständig innerhalb einer Menge von Modi wirken, deren Besetzung bereits feststeht (alle besetzt oder alle unbesetzt), wo sie kein Teilchen von der einen Seite auf die andere bewegen kann. Eine solche Anregung sagt nichts über die gesampelten Bitstrings aus, sodass ein dafür aufgewendeter Slot ein verschwendeter Slot ist. Der Pass QDriftTrotterization, der dieselbe Methode FermionicQDrift umschließt, weist solche Anregungen beim Ziehen zurück und zieht einen Ersatz, sodass jeder der num_groups Slots des resultierenden Circuits eine Anregung trägt, die beide Seiten koppelt.
Diese Filterung ist nicht kostenlos, und deshalb ist sie ein eigener Pass. Was sie erhält, ist das Budget von num_groups gesampelten Slots, nicht die Circuit-Tiefe: Da dieses Budget fest ist, wird eine zurückgewiesene Anregung ersetzt statt verworfen, und eine günstige weicht einer koppelnden Anregung, deren Synthese mehr kostet. Erwarte, dass ein gefilterter Circuit tiefer ist als ein ungefilterter, der mit demselben Seed gezogen wurde.
Das Renormalisieren der Ziehung über die akzeptierten Anregungen verändert die Verteilung, aus der das qDRIFT-Protokoll sampelt, und zerstört anders als die Gruppierung in Schritt 2 auch die Konvergenzgarantie des Protokolls: Das gesampelte Produkt mittelt sich nicht mehr zur Evolution unter dem von dir übergebenen Hamiltonoperator. Jede beibehaltene Anregung wird am Ende mit dem Kehrwert der Akzeptanzwahrscheinlichkeit gewichtet, die zurückgewiesenen tragen nichts bei, und weder die gesamte Koeffizientenbetragssumme noch die Evolutionszeit pro Gate wird zum Ausgleich angepasst.
Die Akzeptanzregel ist zudem eine Überapproximation. Sie vergleicht nur Modus-Träger und enthält daher keine Amplitudeninformation, und die verfolgten Mengen wachsen nur: Eine auf die besetzte Menge beschränkte Anregung ist auf einem Produktzustand tatsächlich inert, aber dieselbe Überlegung wird für spätere Ziehungen schwächer, sobald früher akzeptierte Anregungen den Zustand verschränkt und die meisten Modi als „ungewiss“ markiert haben.
Verwende die Filterung für den in diesem Leitfaden beschriebenen Workflow zum Sampeln von Bitstrings, bei dem die Circuits in die SQD-Nachverarbeitung einfließen und nur die gesampelten Bitstrings zählen. Verwende sie nicht, wenn du aus diesen Circuits einen Erwartungswert schätzen oder dich in anderer Weise auf die qDRIFT-Fehlerschranke verlassen willst: Diese Ergebnisse tragen einen Bias, den der Pass nicht korrigiert.
Diese Filterung muss wissen, welche Modi anfangs besetzt sind. Sie erfordert daher ein InitializeModes-Gate vor den Evolution-Gates im Circuit; InitializeModes.from_hartree_fock() ist eine bequeme Möglichkeit, eines zu konstruieren. Füge hier eines für die Hartree-Fock-Referenz von N2 hinzu (sieben Alpha- und sieben Beta-Elektronen in 14 räumlichen Orbitalen) und vergleiche die gesampelten Anregungen mit und ohne Filterung:
- 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.
Keine der ohne Filterung gezogenen Anregungen berührt die besetzte Menge (0-6 und 28-34) überhaupt, sodass keine von ihnen ein Teilchen zwischen einem besetzten und einem unbesetzten Modus bewegen kann. Mit der Filterung werden alle fünf zurückgewiesen und durch Anregungen ersetzt, die einen besetzten mit einem unbesetzten Modus koppeln. Zum Beispiel bewegt die erste akzeptierte Anregung [0, 1, 6, 7] ein Teilchen zwischen den besetzten Modi 0, 1 und 6 und dem unbesetzten Modus 7.
Sobald eine Anregung akzeptiert ist, wird jeder Modus in ihrem Träger „ungewiss“ und damit für spätere Samples in beiden Rollen zulässig, sodass die Mengen besetzter und unbesetzter Modi weiter wachsen, je mehr Anregungen akzeptiert werden. Dadurch ist die zweite Anregung, [0, 1, 28, 29], zulässig. Alle ihre vier Modi gehören zu den ursprünglich besetzten, sie koppelt also an keinen ursprünglich unbesetzten Modus. Sie wird nur akzeptiert, weil die Modi 0 und 1 ungewiss wurden (und damit als „unbesetzte“ Seite der Kopplung zulässig), nachdem die erste Anregung sie berührt hatte.
Dieses Wachstum macht die Regel zu einer Überapproximation statt zu einem Test: Je mehr Modi ungewiss werden, desto weniger Ziehungen werden zurückgewiesen, und die Filterung wirkt sich bei den frühesten Ziehungen am stärksten auf die Sampling-Verteilung aus.
Um zu beurteilen, wie weit die Filterung die Verteilung verschoben hat, lies die Zurückweisungszähler, die der Pass in den Circuit-Metadaten festhält. Pro gefiltertem Evolution-Gate wird ein Eintrag gespeichert, und das Verhältnis von akzeptierten zu gesamten Ziehungen schätzt die Akzeptanzwahrscheinlichkeit, also den Faktor, um den die beibehaltenen Anregungen übergewichtet wurden:
- 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.
Das Filtern braucht Besetzungsinformationen, gegen die gefiltert wird. Ein PrepareSlaterDeterminant-Gate liefert sie genauso wie InitializeModes, und jede OrbitalRotation markiert jeden Modus, auf den sie wirkt, als „ungewiss“. Ohne eines davon, oder wenn alle initialisierten Modi auf einer Seite liegen (jeder Modus besetzt oder jeder Modus unbesetzt), gibt der Pass eine UserWarning aus und lässt das Sampling für dieses Evolution-Gate ungefiltert. Bei einem Hamiltonoperator, dessen verbleibende Terme die beiden verfolgten Mengen nicht mehr koppeln können, löst der Pass stattdessen nach QDriftTrotterization.MAX_SAMPLE_RETRIES aufeinanderfolgenden Zurückweisungen einen RuntimeError aus.
(Optional) Die Sampling-Gewichte vorberechnen
Greife nur dann darauf zurück, wenn die Transpilation selbst ein Engpass ist. An den Circuits, die du erhältst, ändert es nichts.
FermionicQDrift zieht jede Gruppe mit einer Wahrscheinlichkeit proportional zum Betrag ihres Koeffizienten und leitet diese Beträge bei jedem Aufruf aus dem Hamiltonoperator ab: ein Wert pro Hamiltonoperator-Term, reduziert auf einen pro Gruppe. Weil die Methode zustandslos ist, wiederholt sie diese Reduktion für jede Randomisierung, obwohl das Ergebnis jedes Mal identisch ist. Mit dem Argument weights kannst du sie einmal mit group_coeff_means() berechnen und das Ergebnis übergeben, wodurch der Aufwand aus der Schleife herausgenommen wird, während die Methode frei von zwischengespeichertem, vom Hamiltonoperator abhängigem Zustand bleibt.
Dies ist das Gegenstück zur Termordnung in Schritt 2. Jene zieht das Nachschlagen der Gruppen aus der Schleife, diese die Reduktion der Gewichte, und jede fällt nur so stark auf, wie die andere fehlt, daher lohnt es sich, beide zusammen anzuwenden. Da canon oben bereits umsortiert wurde, lassen sich die beiden direkt kombinieren.
Die Gewichte beschreiben die Gruppen eines bestimmten Operators, daher gehören sie zum Evolution-Gate dieses Operators: Baue die Methode für dieses Gate und übergib sie als synthesis des Gates, genau wie in Schritt 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.
Da dies genau die Gewichte sind, die der Pass berechnet hätte, sind die Circuits dieselben wie zuvor; nur die wiederholte Reduktion entfällt.
Es wird ein Eintrag pro Gruppe erwartet, nicht pro Term, sobald der Operator Gruppen trägt, da ein gruppierter Operator gruppenweise gesampelt wird. Einträge müssen nichtnegativ sein: Ein Gewicht ist ein Betrag, weil das qDRIFT-Protokoll mit positiven zerlegt und das Vorzeichen eines Koeffizienten zu gehört, wo der Pass es direkt aus dem Operator abliest.
Die Gewichte sind absolute Beträge, keine relativen Präferenzen. Ihre Summe legt auch die Evolutionszeit fest, sodass das Multiplizieren jedes Eintrags mit identisch sampelt, aber für statt evolviert. Alles andere als die eigenen Koeffizientenbeträge des Hamiltonoperators zu übergeben, ändert daher, welche Evolution das Ensemble approximiert, weshalb dies ein Performance-Werkzeug ist und kein Weg, das Sampling abzustimmen.
Weil die Gewichte zu dem Gate gehören, dessen Operator sie beschreiben, kann ein Circuit mehrere Evolution-Gates enthalten, jedes mit den Gewichten seines eigenen Operators. Der Pass QDriftTrotterization aus Schritt 5 berücksichtigt sie auf dieselbe Weise: Er zieht ein Gate mit dem FermionicQDrift, das das Gate trägt, und greift für Gates, die keines tragen, auf das zurück, mit dem er konstruiert wurde. Eine andere Art von Synthesemethode an einem Gate ist keine Aussage über qDRIFT, daher ignoriert der Pass sie. Wird die Methode stattdessen einem Pass über den Gate-Namen übergeben, wie in F2FSynthesis({"Evolution": ...}), gilt eine Methode für jedes Evolution-Gate, sodass dort übergebene Gewichte nur für einen Circuit mit einem einzigen sinnvoll sind. Die Länge der Gewichte wird gegen jedes Gate geprüft, auf das sie angewendet werden.
(Optional) Die Indizierung der fermionischen Modi optimieren
Du kannst der Transpilations-Pipeline einen zusätzlichen Optimierungsschritt hinzufügen, der den Abstand der fermionischen Anregungsträger minimiert, indem die fermionischen Modusindizes umbenannt werden. Diese Optimierung wurde im SqDRIFT-Paper eingeführt und wird von build_excitation_span_minimization_model() implementiert. Sie lässt sich mit dem Pass RelabelModes einfach in die Transpiler-Pipeline einfügen:
- 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.
Die Verwendung der automatischen Optimierung innerhalb von RelabelModes (die build_excitation_span_minimization_model() nutzt) erfordert die optionale Abhängigkeit, die von HAS_PYOMO verwaltet wird.
Das Umbenennen ändert, welchen Modus ein bestimmtes Qubit trägt, sodass die Unterraum-Diagonalisierung nur dann die richtigen Bitstrings erhält, wenn dies berücksichtigt wird. Wo das geschieht, hängt davon ab, wo du misst.
Füge die Messungen vor dem Transpilieren mit FermionicCircuit.measure_all() zum FermionicCircuit hinzu, dann ist nichts weiter zu tun: Jede Messung nennt den Modus, den sie liest, sodass die Umbenennung sie mitführt und die gesampelten Bitstrings nach dem ursprünglichen Modus indiziert sind.
Misst du stattdessen den transpilierten QuantumCircuit, müssen die Bitstrings mit der in den Metadaten des Circuits festgehaltenen permutation nachverarbeitet werden. Das Rezept findest du in der Dokumentation zu RelabelModes.
Nächste Schritte
Nachdem du erfolgreich ein Ensemble von Circuits erzeugt hast, kannst du daraus Bitstrings sampeln. Dazu müssen die Circuits auf Hardware ausgeführt werden. Detaillierte Anweisungen findest du in der Qiskit-Dokumentation.
Sobald die Bitstring-Samples vorliegen, können sie zusammen mit den Koeffizienten des Hamiltonoperators für die SQD-Nachverarbeitung verwendet werden, wie in den SQD-Addon-Tutorials erklärt.