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

עקרונות תכנון של ייצוגי אופרטורים

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

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

[bi,bj†]=δij,[bi,bj]=[bi†,bj†]=0 .[b_i, b_j^\dagger] = \delta_{ij}, \qquad [b_i, b_j] = [b_i^\dagger, b_j^\dagger] = 0 \, .

שתי תוצאות מעצבות את הממשק. אינדקס מוד יכול לחזור בתוך איבר אחד, כי bi†bi†b_i^\dagger b_i^\dagger הוא אופרטור חוקי שיוצר שני בוזונים. כמו כן, שינוי סדר של שני גורמים לא מכניס סימן לעולם, אבל העברת אופרטור הכחדה מעבר לאופרטור יצירה על אותו מוד מוסיפה איבר, bibi†=bi†bi+1b_i b_i^\dagger = b_i^\dagger b_i + 1.

אחסון​

אופרטור שומר את האיברים שלו בארבעה מערכים שטוחים, בדומה לפורמט compressed sparse row של מטריצה דלילה:

coeffsמקדם מרוכב אחד לכל איבר.
actionsערך בוליאני אחד לכל גורם: True עבור אופרטור יצירה ו-False עבור אופרטור הכחדה.
modesאינדקס מוד אחד לכל גורם.
boundariesהמיקומים ב-actions וב-modes שבהם כל איבר מתחיל ונגמר.

הבנאי מקבל ישירות את ארבעת המערכים האלה. לאופרטור שלהלן שלושה איברים, והם 2b0†b12 b_0^\dagger b_1, −ib1†b1†-i b_1^\dagger b_1^\dagger והקבוע 0.50.5, ואפשר לקרוא את המערכים בחזרה בעזרת המתודות get_*:

>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> op = BosonOperator(
... coeffs=[2.0, -1.0j, 0.5],
... actions=[True, False, True, True],
... modes=[0, 1, 1, 1],
... boundaries=[0, 2, 4, 4],
... )
>>> op.get_coeffs()
[(2+0j), (-0-1j), (0.5+0j)]
>>> op.get_actions()
[True, False, True, True]
>>> op.get_modes()
[0, 1, 1, 1]
>>> op.get_boundaries()
[0, 2, 4, 4]
>>> len(op)
3

האיבר הראשון פורש על מיקומים 0 עד 2 של actions ו-modes, השני על 2 עד 4, והשלישי על 4 עד 4, וכך נשמר האיבר הקבוע, איבר בלי גורמים. המוד החוזר 1 באיבר השני נשמר כפי שנכתב; איבר אף פעם לא נמחק או ממוזג רק בגלל שאינדקס מוד מופיע פעמיים.

הערה

אופרטור שנבנה עם from_dict() לא שומר את האיברים שלו בסדר של המילון, ולכן המערכים שלו יכולים לרשום את האיברים בסדר שונה מריצה לריצה. האופרטור עצמו זהה, ושום דבר מהבא בהמשך שמשווה או מדפיס אופרטורים לא תלוי בסדר השמור.

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

בניית אופרטורים​

הדרך הישירה ביותר לבנות אופרטור היא from_dict(), שמקבלת מילון מ-tuple של גורמים למקדם. הפונקציות cre() ו-ann() יוצרות את הגורמים, וה-tuple הריק הוא האיבר הקבוע:

>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dag = BosonOperator.from_dict({(cre(0),): 1.0})
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> print(format(number))
1.000000e0 +0.000000e0j * (+0 -0)

בצורה המודפסת, +0 מייצג את b0†b_0^\dagger ו--0 את b0b_0. הבנאים zero() ו-one() מחזירים את איבר היחידה החיבורי והכפלי, ו-from_terms() בונה מחדש אופרטור מה-iterator של iter_terms().

ספריית האופרטורים מספקת את הקוודרטורות חסרות הממד של מקום ותנע,

q^=b†+b2,p^=i(b†−b)2 ,\hat{q} = \frac{b^\dagger + b}{\sqrt{2}}, \qquad \hat{p} = \frac{i \left(b^\dagger - b\right)}{\sqrt{2}} \, ,

בתור position() ו-momentum(). יחד עם פונקציות הקומוטטור של אותו מודול, אפשר לבדוק את האלגברה ישירות:

>>> from qiskit_bosons.operators.library import commutator, momentum, position
>>> q, p = position(0), momentum(0)
>>> q.is_hermitian() and p.is_hermitian()
True
>>> commutator(q, p).equiv(1j * BosonOperator.one())
True
>>> commutator(b, b_dag).equiv(BosonOperator.one())
True

המודול מספק גם את anti_commutator() ואת double_commutator(). שלושתן מקבלות אופרטורים מאותו סוג ומחזירות אופרטור חדש.

שימו לב לסדר של מכפלה​

אפשר להכפיל שני אופרטורים בשתי דרכים, וקל לבלבל ביניהן. האופרטור @ הוא המכפלה המתמטית, ולכן a @ b הוא ABAB. האופרטור & הוא הרכבה, שפועלת לפי הקונבנציה של Qiskit של קריאה משמאל לימין: a & b מפעיל קודם את a ואחר כך את b, ולכן הוא BABA. פונקציות הקומוטטור משתמשות ב-@, ולכן commutator(a, b) הוא [A,B][A, B]. אם בונים את הסוגריים בעצמכם מתוך &, הסימן מתהפך בשקט:

>>> (b @ b_dag).equiv(b_dag @ b + BosonOperator.one())
True
>>> (b & b_dag).equiv(b_dag @ b)
True
>>> ((b @ b_dag) - (b_dag @ b)).equiv(BosonOperator.one())
True
>>> ((b & b_dag) - (b_dag & b)).equiv(-BosonOperator.one())
True

בדיקת אופרטור​

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

>>> hopping = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(2), cre(2), ann(2), ann(2)): 0.5,
... }
... )
>>> len(hopping)
3
>>> sorted(hopping.iter_terms())
[([(True, 0), (False, 1)], (-1+0j)), ([(True, 1), (False, 0)], (-1+0j)), ([(True, 2), (True, 2), (False, 2), (False, 2)], (0.5+0j))]

כמה מתודות מסכמות את המבנה של אופרטור:

>>> hopping.get_support()
{0, 1, 2}
>>> hopping.min_num_modes()
3
>>> hopping.max_rank()
4

get_support() היא קבוצת המודים שהאופרטור פועל עליהם, min_num_modes() הוא הרגיסטר הקטן ביותר שיכול להכיל אותו, ו-max_rank() הוא האורך של האיבר הארוך ביותר שלו.

הערה

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

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

>>> hopping.mode_occupations()
[1, 1, 2]
>>> hopping.mode_occupations([2, 0])
[2, 1]
>>> hopping.max_occupation()
2

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

>>> hopping.conserves_particle_number()
True
>>> move = BosonOperator.from_dict({(cre(0), ann(2)): 1.0})
>>> move.conserves_sector([3])
True
>>> move.conserves_sector([2, 1])
False

לבסוף, relabel_modes() משנה שמות של מודים: מוד i הופך ל-permutation[i]. הרשימה חייבת להיות ארוכה מספיק כדי לאנדקס כל מוד של האופרטור, ואסור שתחזור בה רשומה:

>>> print(format(move.relabel_modes([5, 6, 7])))
1.000000e0 +0.000000e0j * (+5 -7)

ביצוע אריתמטיקה​

אופרטורים תומכים בחיבור, חיסור, שלילה, וכפל וחילוק במספר, בצורת האופרטור הרגילה או במקום:

>>> total = 2 * b_dag + b_dag - 3j * b
>>> len(total)
3
>>> len(total.simplify())
2
>>> total.simplify().equiv(3 * b_dag - 3j * b)
True

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

מכפלות משתמשות ב-@ או ב-& כמתואר למעלה, ו-** מעלה אופרטור בחזקה שלמה:

>>> x = b_dag + b
>>> print(format((x ** 2).simplify()))
1.000000e0 +0.000000e0j * (-0 -0)
1.000000e0 +0.000000e0j * (-0 +0)
1.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0)

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

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

>>> hop = BosonOperator.from_dict({(cre(0), ann(1)): 1j})
>>> print(format(hop.adjoint()))
0.000000e0 -1.000000e0j * (+1 -0)
>>> hop.is_hermitian()
False
>>> (hop + hop.adjoint()).is_hermitian()
True

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

סדר נורמלי והשוואת אופרטורים​

שתי מכפלות של אופרטורי סולם יכולות להיות אותו אופרטור ועדיין להיראות שונות, כי b b†b\,b^\dagger ו-b†b+1b^\dagger b + 1 שווים באלגברה. אופרטורים ששווים במובן הזה לא חייבים להישמר בצורה זהה, ולכן השוואה ביניהם דורשת את יחסי החילוף. המתודה normal_ordered() מיישמת אותם: היא מעבירה כל אופרטור יצירה שמאלה מכל אופרטור הכחדה ומוסיפה איבר בכל פעם שזוג על אותו מוד מוחלף.

>>> product = BosonOperator.from_dict({(ann(0), cre(0), ann(0), cre(0)): 1.0})
>>> ordered = product.normal_ordered()
>>> len(ordered)
5
>>> print(format(ordered.simplify()))
1.000000e0 +0.000000e0j * ()
3.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0 -0 -0)

האופרטור המסודר מוחזר כסכום לא ממוזג, עם חמישה איברים שמורים שמצטמצמים לשלושה אחרי ש-simplify() ממזגת אותם. סידור נורמלי אף פעם לא גורם לאיבר להיעלם: b†b†b^\dagger b^\dagger וכל גורם חוזר אחר נשארים.

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

>>> pair = BosonOperator.from_dict({(cre(0), cre(1), ann(0), ann(1)): 1.0})
>>> list(pair.normal_ordered().iter_terms())
[([(True, 1), (True, 0), (False, 1), (False, 0)], (1+0j))]
>>> list(pair.normal_ordered(sandwich=True).iter_terms())
[([(True, 0), (True, 1), (False, 1), (False, 0)], (1+0j))]

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

>>> reordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0, (): 1.0})
>>> contracted = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> contracted == reordered
False
>>> contracted.simplify() == reordered.simplify()
False
>>> contracted.equiv(reordered)
True

המתודה simplify לבדה משאירה את שתי הצורות נפרדות, כי היא ממזגת רק איברים זהים כפי שנכתבו. כמו כן, is_hermitian() משווה אופרטור לאדג'וינט שלו בסדר נורמלי, ולכן היא מזהה את b b†+b†bb\,b^\dagger + b^\dagger b כהרמיטי בלי עבודה נוספת.

הערה

סידור נורמלי הוא זהות מדויקת באלגברה הלא קטועה, ו-equiv נשענת על כך. הוא אינו זהות אחרי קטיעה. אופרטור ממופה תלוי בצורה שמיפיתם, ולכן התמונות הממופות של contracted ושל reordered למעלה שונות, ברמה הגבוהה ביותר שנשמרה בלבד. ראו קטיעת מרחב Fock לגודל ההפרש ולאופן הבחירה של צורה.

סידור וסינון איברים​

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

canonical_order() ממיינת את האיברים לסדר קבוע שתלוי רק בגורמים כפי שנכתבו, כך שאופרטור נקרא אותו דבר לא משנה איך הורכב. order_terms() ממיינת לפי מפתח שאתם מספקים, שמקבל כל איבר כ-tuple ש-iter_terms() מחזירה:

>>> from qiskit_bosons.operators.terms import canonical_order, order_terms
>>> mixed = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> list(canonical_order(mixed).iter_terms())
[([], (2+0j)), ([(True, 0), (False, 1)], (3+0j)), ([(True, 1), (False, 0)], (1+0j))]
>>> by_weight = order_terms(mixed, key=lambda term: abs(term[1]), reverse=True)
>>> [coeff for _, coeff in by_weight.iter_terms()]
[(3+0j), (2+0j), (1+0j)]

אם לאופרטור יש אינדקסי קבוצות (ראו קיבוץ איברי אופרטור: שימוש במבנה האופרטור), הפונקציה group_order() הופכת כל קבוצה לרצף רציף אחד של איברים:

>>> from qiskit_bosons.operators.terms import group_order
>>> grouped = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> grouped.groups = [1, 0, 1]
>>> group_order(grouped).groups
[0, 1, 1]

העוזר האחרון מסיר איברים במקום לסדר אותם מחדש. איבר שהוא מכפלה של אופרטורי מספר, bi†bib_i^\dagger b_i, רק מכפיל כל מצב Fock בפאזה, ולכן filter_diagonal_terms() יכולה להסיר את הקבוע, את אופרטורי המספר ואת המכפלות שלהם כשהפאזה הזאת לא רלוונטית למה שאתם מחשבים. היא משנה את האופרטור במקום ודורשת אופרטור בסדר נורמלי, מה שהיא לא בודקת:

>>> from qiskit_bosons.operators.terms import filter_diagonal_terms
>>> model = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(0), cre(0), ann(0), ann(0)): 0.5,
... (cre(0), ann(0)): 0.2,
... (): 0.1,
... }
... )
>>> filter_diagonal_terms(model)
>>> len(model)
2
>>> model.is_hermitian()
True

נשארו רק שני איברי הקפיצה. האינטראקציה באתר והאופרטור של מספר החלקיקים אלכסוניים בבסיס האכלוס, ולכן הם הוסרו יחד עם הקבוע. המוד החוזר באינטראקציה מטופל נכון: b0†b0†b0b0b_0^\dagger b_0^\dagger b_0 b_0 הוא אלכסוני, בעוד ש-b0†b0†b0b_0^\dagger b_0^\dagger b_0 לא היה.

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