דלג לתוכן הראשי
פרויקט זה בשלב בטא. מצאת באג? פתח issue ב-GitHub. יש שאלה או רעיון? שאל ב-Discussions — נשמח לשמוע ממך!
מה איכות התרגום בדף זה?

אופטימיזציה של הכנת Slater determinant

חשוב

הרעיונות במדריך הזה זמינים כרגע רק ב-API של Python. פונקציונליות מקבילה תהיה זמינה ב-API של C בגרסה עתידית.

שער InitializeModes מצהיר על אכלוס מודים לייחוס; שער OrbitalRotation מסובב את בסיס החלקיק הבודד. כששני השערים ממוקמים בזה אחר זה, הם מכינים Slater determinant; המודים מתחילים ב-determinant הייחוס ואז מסובבים לאורביטלים של היעד. השער PrepareSlaterDeterminant לוכד את ההרכבה הזו, ומכיוון שהוא נושא את אכלוס הייחוס, הטרנספילציה שלו יכולה להשתמש ב-givens_decomposition_slater() המלבנית, שמממשת רק את האורביטלים התפוסים, במקום הפירוק הריבועי המלא ש-OrbitalRotation לבדו דורש. זו סינתזה זולה יותר בהחלט (ראו את התועלת בסוף המדריך).

מעבר ה-Transpiler‏ MergeSlaterDeterminantPreparation מזהה את הדפוס InitializeModes ואחריו OrbitalRotation ב-FermionicCircuit וכותב אותו מחדש כשערי PrepareSlaterDeterminant, כך ששלב סינתזה מאוחר יותר קולט את הפירוק המצומצם אוטומטית. בסימולציה, הכתיבה מחדש לא משנה את המצב: PrepareSlaterDeterminant הוא validate-then-rotate (הוא מאמת את אכלוס הייחוס ואז מפעיל את הסיבוב, בדיוק כמו שני השערים הנפרדים), ולכן המיזוג פותח סינתזה זולה יותר בלי לשנות את המצב המוכן.

לאורך המדריך, כל מעגל מצויר לפני ואחרי המעבר, זה לצד זה: הקלט משמאל, והתוצאה של הרצת המעבר עליו מימין. שתי פונקציות עזר עושות את זה. merge מריצה את המעבר על עותק של המעגל (דרך FermionicDAGCircuit) וממירה את התוצאה חזרה ל-FermionicCircuit שאפשר לצייר, ו-draw_merge מציירת את שני החצאים באיור אחד של לפני/אחרי:

>>> 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

מה ממוזג​

המעבר מזהה שלושה דפוסים, כולם מבוססים על מוסכמת מודי ה-spin בבלוקים ש-InitializeModes ו-OrbitalRotation משתמשים בה. במערכת עם norb אורביטלים מרחביים, המודים 0..norb הם סקטור spin-alpha, ו-norb..2*norb הם סקטור spin-beta. הכנת Slater determinant נעשית לכל סקטור spin בנפרד, כי לכל סקטור יש צורה משלו (אלקטרונים, אורביטלים).

רגיסטר מלא (או חסר spin)​

הדפוס הפשוט ביותר הוא שימוש ב-InitializeModes, ומיד אחריו OrbitalRotation על אותם מודים. זה המקרה חסר ה-spin (determinant אחד על כל המודים) וגם מעגל עם spin שהסיבוב שלו משתרע על שני הסקטורים בבת אחת. שני השערים מתמזגים לשער 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.

לכל סקטור spin​

זו אותה צורה, אבל מוגבלת לחצי spin אחד. כאן, שני זוגות עצמאיים של אתחול ואז סיבוב, אחד לכל סקטור, מתמזגים כל אחד בנפרד לשער 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.

אתחול גלובלי, סיבובים לכל spin​

זה כולל InitializeModes יחיד על רגיסטר מלא (2 * norb), ואחריו שתי פעולות OrbitalRotation, אחת על כל חצי spin רציף. המעבר מפצל את האכלוס הגלובלי בגבול הסקטורים ופולט שני שערי PrepareSlaterDeterminant. שני הסיבובים יכולים להופיע בכל סדר.

זו הצורה שנוצרת בתהליך local unitary cluster Jastrow‏ (LUCJ) (ראו את מדריך LUCJ); משתמש מציב InitializeModes (בדרך כלל ייחוס Hartree-Fock באמצעות from_hartree_fock()) בתחילת המעגל ומוסיף אנסץ UCJ, ששני סיבובי האורביטלים הראשונים שלו לכל spin באים ישר אחרי האתחול לאחר פירוק האנסץ.

>>> 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.

הדפוס הזה מופעל גם כשרק חצי spin אחד מסובב על ידי שער שבא ישר אחרי האתחול, הצורה שנוצרת כשסיבוב האורביטלים של סקטור spin אחד בלבד נמצא בראש המעגל. החצי המסובב הופך ל-PrepareSlaterDeterminant עם הסיבוב שלו, והחצי השני מוכן עם סיבוב זהות. הכנת הזהות מסונתזת לשערי X של הייחוס ש-InitializeModes היה פולט לחצי הזה בכל מקרה, ולכן ריפוד שלו לא עולה שערים נוספים ועדיין פותח את הסינתזה המצומצמת של Slater בחצי המסובב:

>>> 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.

מה נשאר ללא שינוי​

המעבר שמרני בכוונה. הוא מתמזג רק כששני השערים באמת מרכיבים הכנה של Slater determinant, ומעתיק כל דבר אחר כמו שהוא. “ישר אחרי” מובן לפי גרף זרימת הנתונים של המעגל. ה-OrbitalRotation מתמזג רק כש-InitializeModes הוא הקודם היחיד שלו על פני כל המודים שלו. באיורים שלהלן, ה-לפני וה-אחרי זהים: שום דבר לא התמזג.

פעולה באמצע​

אם שער כלשהו נוגע במודים בין האתחול לסיבוב, הם כבר לא מרכיבים הכנה חשופה, ולכן שום דבר לא ממוזג:

>>> 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.

סיבוב חלקי שאינו חצי spin​

סיבוב שמכסה בדיוק חצי spin רציף אחד של אתחול ברגיסטר מלא כן מתמזג (ראו הסעיף הקודם). אבל סיבוב על כל קבוצת מודים חלקית אחרת (פחות מודים מהאתחול ולא אחד משני חצאי ה-spin שלו, כמו תת-טווח שחוצה את גבול הסקטורים) אינו הכנה לכל סקטור. מיזוג שלו היה מוחק בשקט את תפקיד האתחול על המודים שהוא משאיר בחוץ, ולכן המעבר משאיר את שני השערים במקומם:

>>> 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.

למה זה חשוב: התועלת בסינתזה​

המיזוג משתלם כי PrepareSlaterDeterminant מסונתז להרבה פחות שערים מה-OrbitalRotation שהוא סופג. determinant של mm אלקטרונים על nn אורביטלים צריך לכל היותר m(n−m)m(n-m) סיבובים של שני קיוביטים וללא שערי פאזה אלכסוניים, לעומת n(n−1)/2n(n-1)/2 סיבובים ועוד nn פאזות בסיבוב האורביטלים הריבועי המלא.

מנהל המעברים הקבוע מראש של Jordan-Wigner מריץ את MergeSlaterDeterminantPreparation בשלב האופטימיזציה שלו, ולכן ההפחתה הזו קורית אוטומטית. בצעו טרנספילציה להכנה של שני אלקטרונים ושישה אורביטלים עד QuantumCircuit וספרו את השערים שלה:

הערה

ה-preset מריץ את MergeOrbitalRotations ממש לפני MergeSlaterDeterminantPreparation. זה חשוב כשרצף של שערי OrbitalRotation עוקבים בא אחרי האתחול. המעבר המוקדם מקפל קודם את הרצף לסיבוב יחיד, ש-MergeSlaterDeterminantPreparation מכווץ אחר כך עם האתחול ל-PrepareSlaterDeterminant אחד. בלי הסדר הזה, רק הסיבוב הראשון ברצף היה נספג ושאר הסיבובים היו מסונתזים עם הפירוק הריבועי המלא.

>>> 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.

השוו את זה לסינתזה של אותו סיבוב אורביטלים לבדו. בלי אתחול קודם אין מה למזג, ולכן ה-preset חוזר לפירוק הריבועי המלא, שהוא עמוק יותר ונושא שערי פאזה אלכסוניים:

>>> 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.

הפירוק המצומצם מוריד את כל ששת שערי הפאזה וכמעט חוצה את מספר הסיבובים של שני קיוביטים. ה-determinant המוכן נכון עד כדי פאזה גלובלית (לא רלוונטית פיזיקלית להכנת מצב), וזה מה ששערי הפאזה היו מתקנים.

שתי דרכים לפרק את השער​

את PrepareSlaterDeterminant אפשר לפרק בשתי דרכים שונות, והן לא ניתנות להחלפה:

decompose()​

מרחיב את השער להגדרה שלו: InitializeModes של הייחוס ואחריו OrbitalRotation המלא. זה שקול לשער כאופרטור, לכל מצב קלט.

PrepareSlaterDeterminant.synthesis, מיושם על ידי F2FSynthesis​

מחליף את השער בפירוק המצומצם של המדריך הזה. זה שקול לשער רק כהכנת מצב: הוא מפיק את אותו determinant, עד כדי פאזה גלובלית, כשהמודים מתחילים בוואקום. הוא זול יותר בדיוק בגלל שהוא מניח את זה.

השתמשו בסינתזה כשהשער מכין מצב על הוואקום, וזה מה שהשער נועד לו ומה שה-Transpiler עושה בשבילכם. השתמשו ב-decompose() כשהשער צריך להמשיך לפעול כאופרטור, למשל כי שערים אחרים קודמים לו על אותם מודים, או כשרוצים לבדוק או לסמלץ את השערים הפשוטים. המעבר F2FSynthesis לא יכול לדעת באיזה מקרה הוא נמצא, ולכן באחריות המעגל שמוסרים לו להכין את ה-determinant מהוואקום.

סימולציה של מעגל עם ffsim לא צריכה אף אחד מהשניים: כל שער מפעיל את כל היוניטרי שלו, ולכן מעגל שמכיל PrepareSlaterDeterminant מסומלץ כמות שהוא, ונשאר במסלול המהיר לשערי OrbitalRotation שסביבו. גם פירוק של שער Slater בלבד עם circuit.decompose("PrepareSlaterDeterminant") משאיר את הסיבובים האלה שלמים.

ראו גם