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

קיבוץ איברי אופרטור: שימוש במבנה האופרטור

קיבוץ הוא אחת מהפעולות שב-qiskit_fermions.operators.terms, שאוסף שגרות שמחלקות את האיברים של אופרטור לפי המבנה שלהם.

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

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

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

  • שימור המבנה הפיזיקלי: קבוצות מקודדות מבנה משמעותי מהבעיה המקורית (למשל דפוסי אינטראקציה או סימטריות) שאפשר להשתמש בו לפירוקים בעלי משמעות פיזיקלית רבה יותר.

  • בחירת flow sets: קיבוץ יכול לקודד באופן טבעי flow sets ומבנים גרפיים אחרים שרלוונטיים לסימולציה קוונטית, כפי שמוצג ב-[1].

  • יציבות אלגוריתמית משופרת: כפי שמודגם בשגרת SqDRIFT, אלגוריתמים מסוימים יכולים ליהנות מיציבות או ביצועים משופרים בזכות שימור תכונות פיזיקליות.

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

שימוש​

הדרך הפשוטה ביותר להגדיר קיבוץ של אופרטור היא לקבוע את התכונה groups של אופרטור היעד. להלן דוגמה לקיבוץ איברים לפי line flow sets כפי שמוצג באיור 1c של [1].

רמז

המודול qiskit_fermions.operators.terms.grouping מספק שיטות נוחות לזיהוי אוטומטי של מבנה באופרטור ולהקצאת אינדקסי קבוצה בהתאם.

>>> import rustworkx as rx
>>> graph = rx.PyDiGraph()
>>> _ = graph.add_nodes_from(range(6))
>>> edges = [(0, 1), (1, 2), (3, 0), (1, 4), (5, 2), (5, 4), (4, 3)]
>>> groups = [0, 0, 1, 2, 1, 3, 3]
>>> edges_with_group_as_payload = [(i, j, g) for (i, j), g in zip(edges, groups)]
>>> _ = graph.add_edges_from(edges_with_group_as_payload)
>>> from rustworkx.visualization import mpl_draw
>>> mpl_draw(
... graph,
... pos={i: (i % 3, i // 3) for i in range(6)},
... edge_labels=str,
... with_labels=True,
... )
<Figure size ... with 1 Axes>

A simple directed graph on which to define a MajoranaOperator.

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

>>> from qiskit_fermions.operators import MajoranaOperator
>>>
>>> # Construct operator directly using sparse arrays
>>> # 7 terms, each with coefficient 1.0, modes from edge endpoints
>>> op = MajoranaOperator(
... coeffs=[1.0] * 7,
... modes=[node for edge in edges for node in edge],
... boundaries=[0, 2, 4, 6, 8, 10, 12, 14],
... )
>>>
>>> # Assign group indices to terms (groups is part of the sparse data structure)
>>> op.groups = groups
>>>
>>> # Partition operator by groups
>>> grouped_ops = op.split_out_groups()
>>>
>>> # Inspect each group
>>> for i, g in enumerate(grouped_ops):
... print(f"Group {i}: {list(sorted(g.iter_terms()))}")
Group 0: [([0, 1], (1+0j)), ([1, 2], (1+0j))]
Group 1: [([3, 0], (1+0j)), ([5, 2], (1+0j))]
Group 2: [([1, 4], (1+0j))]
Group 3: [([4, 3], (1+0j)), ([5, 4], (1+0j))]

ניתוח קיבוץ​

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

המוסכמה הנפוצה ביותר היא שכל קבוצה היא הרמיטית בפני עצמה. זה מה שמאפשר נוסחאות מכפלה פרמיוניות אקראיות מלכתחילה: האיבר הבודד ap†aqa^\dagger_p a_q עם p≠qp \neq q אינו הרמיטי, ולכן ההתפתחות בזמן שלו אינה יוניטרית ואי אפשר לדגום אותו לבדו. צירוף שלו עם aq†apa^\dagger_q a_p מניב קבוצה שאפשר לדגום.

>>> from qiskit_fermions.operators import FermionOperator
>>> from qiskit_fermions.operators.terms.grouping import (
... group_coeff_means,
... groups_are_hermitian,
... groups_have_uniform_coeffs,
... )
>>>
>>> # a conjugate pair, plus one unpaired term. The operator is built from the sparse
>>> # arrays rather than from a dictionary, because only the former fixes the term order
>>> # that the group indices below are paired with.
>>> pair_and_single = FermionOperator(
... [1.0, 1.0, 1.0],
... [True, False, True, False, True, False],
... [0, 1, 1, 0, 2, 3],
... [0, 2, 4, 6],
... )
>>> pair_and_single.groups = [0, 0, 1]
>>>
>>> groups_are_hermitian(pair_and_single) # group 1 is unpaired
[True, False]
>>> group_coeff_means(pair_and_single)
[1.0, 1.0]

אינדקס קבוצה אחד לכל איבר נאכף​

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

>>> pair_and_single.groups = [0, 0] # one index short, on a three-term operator
Traceback (most recent call last):
ValueError: expected one group index per term, but got 2 for 3 terms

ניקוי אינדקסי הקבוצות על ידי השמת None מותר תמיד.

שתי הבדיקות בלתי תלויות זו בזו​

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

>>> from qiskit_fermions.operators.terms.grouping import (
... groups_are_hermitian,
... groups_have_uniform_coeffs,
... )
>>> # uniform coefficients, but not Hermitian: the terms are not each other's adjoint
>>> uniform = FermionOperator(
... [1.0, 1.0],
... [True, False, True, False],
... [0, 1, 2, 3],
... [0, 2, 4],
... )
>>> uniform.groups = [0, 0]
>>> groups_have_uniform_coeffs(uniform), groups_are_hermitian(uniform)
([True], [False])
>>> # Hermitian, but with mixed magnitudes: two conjugate pairs at different scales
>>> mixed = FermionOperator(
... [1.0, 1.0, 5.0, 5.0],
... [True, False, True, False, True, False, True, False],
... [0, 1, 1, 0, 2, 3, 3, 2],
... [0, 2, 4, 6, 8],
... )
>>> mixed.groups = [0, 0, 0, 0]
>>> groups_are_hermitian(mixed), groups_have_uniform_coeffs(mixed)
([True], [False])

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

>>> conjugate = FermionOperator(
... [1.0j, -1.0j],
... [True, False, True, False],
... [0, 1, 1, 0],
... [0, 2, 4],
... )
>>> conjugate.groups = [0, 0]
>>> groups_have_uniform_coeffs(conjugate)
[True]
>>> groups_have_uniform_coeffs(conjugate, abs=False)
[False]
הערה

groups_are_hermitian() יורש את ההבטחה החד-צדדית של is_hermitian(): ערך True תמיד אמין, ואילו ערך False אמין רק עבור סוגי אופרטורים שהצורה הנורמלית שלהם היא צורה קנונית אמיתית. עיין בסוג האופרטור הספציפי כדי לברר מה חל.

[1] (1,2)

A. Gandon et al., Stabilizer-based quantum simulation of fermion dynamics with local qubit encodings, arXiv:2512.11418v2.