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

Transpilation של ansatz מסוג LUCJ

חשוב

המושגים במדריך הזה זמינים רק ב-API של Python.

ה-ansatz מסוג local unitary cluster Jastrow (LUCJ) הוא פרמטריזציה קומפקטית ויעילה לחומרה של פונקציית גל אלקטרונית מתואמת. הוא חבר במשפחה הכללית יותר של unitary cluster Jastrow (UCJ) ולובש את הצורה

∣Ψ⟩=(∏k=1LUk eiJk Uk†)∣Φ0⟩,\lvert \Psi \rangle = \left(\prod_{k=1}^{L} \mathcal{U}_k\, e^{i \mathcal{J}_k}\, \mathcal{U}_k^\dagger\right) \lvert \Phi_0 \rangle,

כאשר ∣Φ0⟩\lvert \Phi_0 \rangle הוא מצב ייחוס (בדרך כלל Hartree-Fock), כל Uk\mathcal{U}_k הוא סיבוב אורביטלי, וכל Jk\mathcal{J}_k הוא אופרטור קולון אלכסוני

J=12∑ij,στJijστ niσ njτ,\mathcal{J} = \frac12 \sum_{ij,\sigma\tau} \mathbf{J}^{\sigma\tau}_{ij}\, n_{i\sigma}\, n_{j\tau},

כאשר niσn_{i\sigma} הוא אופרטור המספר על האורביטל המרחבי ii עם ספין σ\sigma.

ffsim בונה את אופרטור ה-ansatz; החבילה הזו הופכת אותו למעגל ומורידה את המעגל הזה אל קיוביטים. המדריך הזה עוסק בחצי השני של המשפט. הסיבה לעשות את הדרך הזו היא ההורדה עצמה: השערים של ffsim ל-Qiskit ספציפיים לטרנספורמציית Jordan-Wigner, ואילו FermionicCircuit אינו נושא שום הנחה לגבי קידוד פרמיון-לקיוביט שלו, ולכן אפשר לסנתז את אותו ansatz דרך כל קידוד שמתאים להתקן היעד.

לכן אופרטור ה-ansatz מגיע לכאן מוכן, וכל מה שמופיע להלן הוא מה שקורה לו אחר כך: לְמה השער מתפרק, איך הוא מורד דרך Jordan-Wigner, איך הוא מותאם למפת צימוד של התקן, ומה עדיין נדרש עבור הורדה שאינה Jordan-Wigner. לבנייה, פרמטריזציה או סימולציה של ansatz מסוג UCJ מלכתחילה, ראה את המדריכים של ffsim עצמה.

ראה גם

המדריך ליחסים עם ffsim לגבי האופן שבו שתי החבילות מחלקות ביניהן את העבודה, והמדריך ל-transpilation לגבי שלבי הצינור שבהם נעשה שימוש להלן.

1. קבלת אופרטור LUCJ מ-ffsim​

ה-ansatz מאותחל מהאמפליטודות של חישוב coupled-cluster singles and doubles (CCSD). הרץ Hartree-Fock מוגבל ולאחריו CCSD עבור מולקולת מימן בבסיס 6-31g, תוך שימוש ב-PySCF לכימיה הקוונטית, ואז העבר את האמפליטודות ל-from_t_amplitudes() של ffsim. הוא מבצע פירוק לגורמים כפול של אמפליטודות t2t_2 כדי לקבל את מטריצות קולון האלכסוניות ואת הסיבובים האורביטליים לכל שכבה, ומפיק סיבוב אורביטלי סופי אופציונלי מאמפליטודות t1t_1. מספר החזרות LL הוא מה שהפירוק לגורמים מניב; n_reps קוטם אותו, ומחליף דיוק במעגל רדוד יותר.

>>> import ffsim
>>> import pyscf
>>> import pyscf.cc
>>>
>>> # build the molecule and run Hartree-Fock
>>> mol = pyscf.gto.Mole()
>>> mol.build(
... atom=[["H", (0, 0, 0)], ["H", (0, 0, 0.74)]],
... basis="6-31g",
... symmetry="Dooh",
... verbose=0,
... )
<pyscf.gto.mole.Mole object at ...>
>>> scf = pyscf.scf.RHF(mol).run()
>>>
>>> norb = scf.mo_coeff.shape[1]
>>> nelec = (mol.nelec[0], mol.nelec[1])
>>>
>>> # run CCSD for the t-amplitudes, then factorize them into an ansatz operator
>>> ccsd = pyscf.cc.CCSD(scf).run()
>>> t1, t2 = ccsd.t1, ccsd.t2
>>>
>>> ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(t2, t1=t1, n_reps=2)

כאן ה-ansatz מפסיק להיות עניין של ffsim. ffsim מציעה כאן אפשרויות שהחבילה הזו לא רואה ואינה צריכה להכיר, כמו הפירוק לגורמים המכווץ ("compressed") שעבר אופטימיזציה וריאציונית שמאחורי optimize=True, או אריזת וקטור הפרמטרים שאופטימייזר וריאציוני מפעיל. כולן מפיקות אותו סוג של אופרטור, וכל מה שלהלן פועל ללא שינוי על כל אחת מהן.

2. הפיכת האופרטור למעגל פרמיוני​

השער UCJ עוטף את האופרטור ומבטא אותו כמעגל מעל מודים פרמיוניים. השער הוא יוניטרי טהור ואינו נושא ייחוס משלו, ולכן הוסף לפניו שער InitializeModes (שנבנה עם from_hartree_fock()) כדי לספק את ייחוס Hartree-Fock שעליו מופעל ה-ansatz.

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, UCJ
>>>
>>> ansatz = UCJ(ucj_op)
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(ansatz, circuit.modes)

פירוק המעגל חושף את האנטומיה שלו, וזה הייצוג שבו שאר המדריך פועל. השער InitializeModes מכין את הדטרמיננטה של הייחוס, וכל שכבה של ה-ansatz תורמת OrbitalRotation Uk†\mathcal{U}_k^\dagger, אחריו eiJke^{i\mathcal{J}_k} (‏Evolution של אופרטור קולון האלכסוני Jk\mathcal{J}_k), ואז Uk\mathcal{U}_k, עם OrbitalRotation סופי בסוף. הסיבובים האורביטליים פועלים לכל מגזר ספין, ולכן כל אחד מהם ממוקם על מודי האלפא 0..norb ועל מודי הבטא norb..2*norb באופן בלתי תלוי.

>>> circuit.decompose().draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The gates that the UCJ ansatz decomposes into.

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

הערה

כל שכבה מסתיימת ב-Uk\mathcal{U}_k והבאה מתחילה ב-Uk+1†\mathcal{U}_{k+1}^\dagger, ולכן אפשר למזג שערי OrbitalRotation סמוכים לסיבוב יחיד. מעבר transpilation שמבצע את המיזוג הזה הוא פיתוח עתידי מתוכנן.

3. בחירת קידוד פרמיון-לקיוביט​

המעגל עדיין פרמיוני, וזו הנקודה שבה החבילה הזו מוכיחה את מקומה בזרימת העבודה. FermionicCircuit אינו נושא הנחה לגבי האופן שבו מודים הופכים לקיוביטים, ולכן הקידוד הוא בחירה של שלב הסינתזה ולא משהו שנאפה לתוך ה-ansatz. זה מה שהחבילה הזו מוסיפה על פני השערים של ffsim ל-Qiskit, שספציפיים לטרנספורמציית Jordan-Wigner.

אם רק Jordan-Wigner מעניין אותך, לא היית צריך את העקיפין הזה: ffsim מגיעה עם UCJOpSpinBalancedJW והאחים שלו, שמעבירים אופרטור UCJ למעגל קיוביטים בעצמם. הסיבה להעביר את ה-ansatz דרך מעגל פרמיוני היא כל מה שאחר שקידוד יכול להעניק לך. קידודים מקומיים, למשל, משקיעים קיוביטים נוספים כדי לחסום את משקל ה-Pauli של כל איבר, ומדריכי ה-flow set ב-1D וב-2D משתמשים בכך כדי להפוך את עומק שני-הקיוביטים של צעד Trotter ללא תלוי בגודל המערכת.

עבור ansatz מסוג UCJ בפרט, שלב הסינתזה צריך לכסות שני סוגי שערים:

  • התפתחות קולון האלכסונית. Evolution כבר אגנוסטי לקידוד: MapperFnEvolutionSynthesis מקבל פונקציית מאפר, ולכן שכבות eiJke^{i \mathcal{J}_k} יורדות דרך כל קידוד שאפשר לבטא כאחד כזה. מדריכי ה-flow set מראים איך לכתוב כזה.

  • הסיבובים האורביטליים והכנת דטרמיננטת Slater. לא OrbitalRotation וגם לא PrepareSlaterDeterminant ש-InitializeModes הראשון והסיבוב מתמזגים אליו (ראה בהמשך) ממופים לקיוביטים ככאלה. מעבר F2FSynthesis מפרק אותם קודם, במרחב הפרמיוני, לשערי Evolution על מוד אחד או שניים כל אחד (ובהכנה, גם InitializeModes של הייחוס), ולכן כל קידוד שיכול למפות Evolution מטפל בהם. תחת Jordan-Wigner, הגורמים בין מודים סמוכים הופכים לאובייקטים מסוג XXPlusYYGateבשלב הקיוביטים.

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

איך לעשות transpile למעגלי UCJ תחת קידוד שאינו Jordan-Wigner היא שאלת מחקר פתוחה, והחבילה הזו מספקת את המסגרת שבתוכה אפשר לפתח צינור כזה. בהמשך המדריך נבנה כדוגמה את צינור ה-transpiler לקידוד Jordan-Wigner, שזהה לזה ש-generate_preset_jw_pass_manager() מפיק.

4. הורדה דרך Jordan-Wigner​

ההורדה מתרחשת ב-F2QSynthesis, שפועל כמו HighLevelSynthesis של Qiskit: הוא עובר על ההוראות הפרמיוניות ומעביר כל אחת לפלאגין שרשום עבור שם השער הזה. התכונה F2QSynthesis.methods שלו מחזיקה F2QSynthesisPlugin אחד לכל שם שער, ולכן בניית השלב פירושה בחירת פלאגין לכל שער פרמיוני שהמעגל מכיל:

>>> from qiskit_fermions.mappers.library import jordan_wigner
>>> from qiskit_fermions.transpiler.passes import F2QSynthesis
>>> from qiskit_fermions.transpiler.passes.synthesis import (
... MapperFnEvolutionSynthesis,
... TrivialOccupationInitializeModesSynthesis,
... )
>>>
>>> synthesis = F2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(jordan_wigner)
>>> synthesis.methods["InitializeModes"] = TrivialOccupationInitializeModesSynthesis()

השמת מופעים כך היא גם הדרך שבה פלאגין שכתבת בעצמך נכנס לצינור, בלי שתצטרך לרשום אותו קודם בשום מקום.

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

השלב הזה משתלב אחר כך ב-MultiStagePassManager ששלביו רצים בסדר קבוע:

>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.transpiler import generate_preset_pass_manager
>>> from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig
>>>
>>> from qiskit_fermions.transpiler import (
... FermionicCircuitToDAG,
... FermionicPassManager,
... QuantumDAGToCircuit,
... )
>>> from qiskit_fermions.transpiler.passes import (
... F2FSynthesis,
... HopEvolutionSynthesis,
... MergeOrbitalRotations,
... MergeSlaterDeterminantPreparation,
... TrivialF2QLayout,
... )
>>>
>>> pm = MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... optimization=FermionicPassManager(
... [
... MergeOrbitalRotations(),
... MergeSlaterDeterminantPreparation(),
... F2FSynthesis(
... {"OrbitalRotation": "Givens", "PrepareSlaterDeterminant": "Givens"}
... ),
... ]
... ),
... layout=FermionicPassManager(TrivialF2QLayout()),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(
... hls_config=HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
... ),
... output=QuantumDAGToCircuit(),
... )

שלב ה-optimization רץ לפני הסינתזה, על המעגל הפרמיוני, וזה מה שהופך אותו לכדאי: הוא משכתב שערים בזמן שהם עדיין פרמיוניים ואגנוסטיים לקידוד, כך שהחיסכון עובר לכל קידוד שיבוא אחריו. MergeOrbitalRotations מכווץ כל רצף של שערי OrbitalRotation עוקבים לשער אחד, וזה חשוב עבור ansatz מסוג UCJ כי כל שכבה מסתיימת ב-Uk\mathcal{U}_k והבאה מתחילה ב-Uk+1†\mathcal{U}_{k+1}^\dagger. אחר כך MergeSlaterDeterminantPreparation ממזג את ה-InitializeModes הראשון ואת הסיבוב שבא אחריו ל-PrepareSlaterDeterminant יחיד, שהסינתזה מורידה דרך פירוק Slater מצומצם וזול יותר במקום סיבוב ריבועי מלא. הסדר חשוב: מיזוג הסיבובים קודם חושף את הסיבוב היחיד מיד אחרי האתחול, ואז מיזוג Slater מכווץ אותו לתוכו. F2FSynthesis בא אחרון, כי הוא מפרק כל סיבוב שנשאר, ואם היה רץ מוקדם יותר, לשני מעברי המיזוג לא היה מה למזג.

שאר השלבים הם אינסטלציה. input ו-output ממירים בין מעגלים ל-DAG, layout מקצה מודים פרמיוניים לקיוביטים (TrivialF2QLayout ממפה מוד i לקיוביט i), ו-qubit הוא pass manager רגיל של Qiskit שמשתלט כשלא נשאר שום דבר פרמיוני.

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

>>> transpiled = pm.run(circuit.decompose())
>>> print(dict(sorted(transpiled.count_ops().items())))
{'rz': 14, 'rzz': 12, 'x': 2, 'xx_plus_yy': 28}

בלי התקן יעד, זה ממופה על 2 * norb קיוביטים בהנחת קישוריות של כל-לכל; הסיבובים האורביטליים מסונתזים לאובייקטים מסוג XXPlusYYGateוההתפתחויות האלכסוניות של קולון לאובייקטים מסוג RZZGate:

>>> transpiled.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The Jordan-Wigner transpiled LUCJ circuit.

הצינור הזה הוא בדיוק מה ש-generate_preset_jw_pass_manager() מרכיב, ולכן בפועל השתמש ב-preset ובנה את השלבים ידנית כשאתה צריך לשנות אחד מהם:

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> preset = generate_preset_jw_pass_manager()
>>> preset.run(circuit.decompose()).count_ops() == transpiled.count_ops()
True

5. התאמה לטופולוגיית ההתקן​

להתקן אמיתי יש מפת צימוד מוגבלת של קיוביטים, וה-ansatz מסוג LUCJ מתוכנן להתאים לה. האינטראקציות של אותו ספין (pairs_aa) יוצרות שתי שרשראות לינאריות והאינטראקציות אלפא-בטא (pairs_ab) מגשרות ביניהן. generate_lucj_pass_manager() של ffsim בונה צינור קיוביטים מודע-התקן למבנה הזה, ומחזיר את תת-הקבוצה של pairs_ab שהחומרה באמת יכולה לקלוט. שבץ את הצינור הזה בשלב qubit של ה-preset תוך שמירה על סינתזת פרמיון-לקיוביט של החבילה עצמה:

>>> from ffsim.qiskit import generate_lucj_pass_manager
>>> from qiskit.providers.fake_provider import GenericBackendV2
>>> from qiskit.transpiler import CouplingMap
>>>
>>> # a heavy-hex device coupling map (any BackendV2 works, e.g. a real fake_provider backend)
>>> coupling_map = CouplingMap.from_heavy_hex(5)
>>> backend = GenericBackendV2(
... num_qubits=coupling_map.size(),
... basis_gates=["cp", "xx_plus_yy", "p", "x", "swap"],
... coupling_map=coupling_map,
... )
>>>
>>> # nearest-neighbor same-spin chain; let the pass manager choose the alpha-beta pairs
>>> pairs_aa = [(p, p + 1) for p in range(norb - 1)]
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> hls_config = HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
>>> pm.qubit, allowed_pairs_ab = generate_lucj_pass_manager(
... backend,
... norb,
... "heavy-hex",
... (pairs_aa, None),
... optimization_level=3,
... seed_transpiler=0,
... hls_config=hls_config,
... )
>>>
>>> # the alpha-beta interactions the heavy-hex connectivity can implement
>>> print(allowed_pairs_ab)
[(0, 0)]

ההרכבה הזו היא העיקר: שלב qubit מודע להתקן, ואילו שלב הסינתזה של פרמיון-לקיוביט שלפניו נשאר של החבילה הזו, ולכן נשאר ניתן להחלפה. החלפת שלב qubit אכן מסירה את מה שה-preset רושם שם, שהוא HopEvolutionSynthesis שהופך את גורמי Givens של סיבוב אורביטלי לאובייקטים מסוג XXPlusYYGate‏. העבר אותו הלאה דרך hls_config כמו למעלה, או שיהיה צורך להגיע לבסיס של ההתקן בלעדיו.

כאשר pm.qubit מוגדר כעת לצינור המודע להתקן, הרצת ה-pass manager מפרסת את המעגל על הקיוביטים של ה-Backend ומנתבת אותו למפת הצימוד. עבור ה-ansatz הבלתי מוגבל משלב 1, שאופרטור קולון האלכסוני שלו עדיין מכיל איברי אלפא-בטא שהחומרה אינה יכולה להגיע אליהם ישירות, הנתב חייב להוסיף הרבה שערי SWAP כדי לגשר עליהם:

>>> naive = pm.run(circuit.decompose())
>>> naive.num_qubits # laid out on the full heavy-hex device register
57
>>> naive_swaps = naive.count_ops()["swap"]
>>> naive_swaps # many SWAPs to bridge the unreachable alpha-beta interactions
33

הגבלת ה-ansatz לאינטראקציות שאפשר לממש בחומרה

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

>>> restricted = UCJ(
... ffsim.UCJOpSpinBalanced.from_t_amplitudes(
... t2, t1=t1, n_reps=2, interaction_pairs=(pairs_aa, allowed_pairs_ab)
... )
... )
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(restricted, circuit.modes)
>>>
>>> transpiled = pm.run(circuit.decompose())
>>> restricted_swaps = transpiled.count_ops()["swap"]
>>> restricted_swaps # far fewer routing SWAPs than the unrestricted ansatz
4

ציור הקיוביטים הפעילים בלבד (idle_wires=False) מראה את המעגל מוגבל לשתי שרשראות הספין ולגשר אלפא-בטא, מבוטא בשערי הבסיס של ההתקן. Layout וניתוב מפזרים את המודים הלוגיים על פני הקיוביטים הפיזיים של ההתקן, ולכן העבר wire_order שנלקח מה-layout הסופי של המעגל (final_index_layout() מפרט את הקיוביט הפיזי שכל קיוביט קלט הסתיים בו, לפי סדר קיוביטי הקלט) כדי לצייר את החוטים בחזרה בסדר המודים המקורי:

>>> wire_order = transpiled.layout.final_index_layout(filter_ancillas=False)
>>> transpiled.draw("mpl", idle_wires=False, fold=-1, wire_order=wire_order)
<Figure size ... with 1 Axes>

The hardware-restricted LUCJ circuit routed onto the heavy-hex device coupling map.

הערה

מספרי השערים והעומק המדויקים לאחר ה-layout תלויים במעברי הניתוב/האופטימיזציה ובהתקן שנבחר, ולכן הם לא מובאים כאן. הנקודה המרכזית היא התכנון המשותף; ביטוי ה-ansatz עם שרשרת pairs_aa של שכנים קרובים וגשרי pairs_ab מסוננים-לפי-חומרה שומר על המעגל המסונתז קרוב לטופולוגיית ההתקן, ומצמצם את תקורת הניתוב (שערי SWAP שנוספו). ראה FermionicSlaterGivensDecomposition לבחירת סינתזה קשורה (minimize_2q_gate_count), שמחליפה מספר שערי שני-קיוביטים בעומק מנותב.

השלבים הבאים​

  • למד איך השערים הבודדים עובדים בתיעוד של qiskit_fermions.circuit.library ובמדריך למעגל פרמיוני.

  • עיין במדריך ההסבר על אופרטורים כדי להבין איך לבנות אופרטורים פרמיוניים והמילטוניאנים ישירות.

  • ראה איך מעגל פרמיוני ממופה לקיוביטים במדריך ל-transpilation.

  • קרא את התיעוד של F2QSynthesis כדי להכיר את הדרך האחרת להגדיר את שלב הסינתזה: מילון config שבוחר פלאגינים לפי השמות הקצרים שהם רושמים תחת נקודות הכניסה qiskit_fermions.transpiler.synthesis שלהם, במקום לפי מופע.

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