מיפוי ייצוגי אופרטורים
Mappers הם שגרות שממירות אופרטורים מייצוג אחד לאחר תוך שמירה על שקילות מתמטית. השתמש בהם כדי לעבוד בגמישות בין אלגברות אופרטורים שונות וכדי להכין אופרטורים להרצה על מעגל קוונטי.
מימוש mappers מותאמים אישית
מודול ה-mappers מקל על מימוש מיפויים מותאמים אישית. ה-mappers פועלים על ידי המרת הפעולות הבסיסיות (יצירה, איון, Majorana וכן הלאה) שמרכיבות אופרטור, ואז שילוב הפעולות שהומרו לפי הכללים של הייצוג היעד.
כדוגמה קונקרטית, נניח טרנספורמציית Jordan-Wigner שממפה MajoranaOperator לאופרטור קיוביטים באמצעות map_majorana_action_generators():
- PYTHON
- C
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_majorana_action_generators
>>> from qiskit_fermions.operators import MajoranaOperator
>>>
>>> # Create a Majorana operator
>>> maj_op = MajoranaOperator.from_dict({(0, 1): 1.0, (2,): 0.5})
>>>
>>> # Define how each Majorana action maps to Pauli strings
>>> num_qubits = 2
>>> def map_action(mode: int) -> SparseObservable:
... idx = mode // 2
... qubits = list(range(idx + 1))
... pauli = "Y" if mode % 2 else "X"
... return SparseObservable.from_sparse_list(
... [("Z" * idx + pauli, qubits, 1.0)],
... num_qubits=num_qubits,
... )
>>>
>>> # Apply the mapping to transform the operator
>>> custom_observable = map_majorana_action_generators(
... maj_op,
... map_action,
... identity=lambda: SparseObservable.identity(num_qubits),
... compose=SparseObservable.compose,
... )
>>> print(custom_observable.simplify())
<SparseObservable with 2 terms on 2 qubits: (0+1j)(Z_0) + (0.5+0j)(X_1 Z_0)>
// The C API provides direct mapping functions for common transformations.
// Custom mapper prototyping as shown in Python is primarily a Python feature.
// For custom mappings in C, you will need to perform the iteration and
// conversion logic entirely by yourself.
השלבים העיקריים למימוש כל ממפה (mapper) מותאם אישית הם:
-
הגדרת טרנספורמציית הפעולה: קבעו איך כל פעולה (שמאונדקסת לפי מוד) ממופה לייצוג היעד. בדוגמה למעלה, פעולות Majorana ממופות למחרוזות Pauli לפי כללי Jordan-Wigner.
-
שימוש באחת מפונקציות המיפוי המובנות: המודול
mappersמספק פונקציות עזר שעוברות על האיברים של האופרטור ומפעילות את מפת הפעולות המותאמת על ייצוגי האופרטורים שמספק המודולoperators. לדוגמה,map_majorana_action_generators()עובדת עם אופרטורי Majorana. -
קבלת התוצאה: פונקציית המיפוי מחזירה אופרטור בייצוג היעד, כשהטרנספורמציה מיושמת באופן עקבי על כל האיברים.
העיצוב הזה מקל על יצירת אבות טיפוס של מיפויים מותאמים בלי להכיר לעומק את המבנים הפנימיים של האופרטורים. הטרנספורמציה מיושמת אוטומטית על כל איבר באופרטור, תוך שמירה על שקילות מתמטית.
אפשר גם לעבור על האיברים של אופרטור ידנית, במקום להשתמש באחת מפונקציות האיטרציה המובנות. פרטים בסעיף על איטרציה על איברים במדריך האופרטורים.
מימושי ספרייה
המודול qiskit_fermions.mappers.library מספק מימושים יעילים ומנוסים היטב של מיפויים נפוצים. הם פועלים לפי אותו דפוס בסיסי כמו ממפים מותאמים, אבל מותאמים לשימוש בפרודקשן וזמינים גם ב-API של Python וגם ב-API של C.
הדוגמה הבאה מפעילה ממפה מהספרייה על אותו MajoranaOperator מסעיף הממפה המותאם, ומראה תוצאות שקולות:
- PYTHON
- C
>>> from qiskit_fermions.mappers.library import majorana_to_fermion, jordan_wigner
>>>
>>> # Convert MajoranaOperator to FermionOperator using library mapper
>>> ferm_op = majorana_to_fermion(maj_op)
>>>
>>> # Apply Jordan-Wigner mapper to get qubit operator
>>> sparse_observable = jordan_wigner(ferm_op, num_qubits=2)
>>> difference = sparse_observable - custom_observable
>>> print(difference.simplify() == SparseObservable.zero(num_qubits))
True
#include <qiskit_fermions.h>
// Create a MajoranaOperator with the same terms as maj_op above
uint64_t num_terms = 2;
uint64_t num_modes = 3;
uint32_t modes[3] = {0, 1, 2};
QkComplex64 coeffs[2] = {{1.0, 0.0}, {0.5, 0.0}};
uint32_t boundaries[3] = {0, 2, 3};
QfMajoranaOperator *maj_op = qf_maj_op_new(num_terms, num_modes, coeffs, modes, boundaries);
// Convert MajoranaOperator to FermionOperator using library mapper
QfFermionOperator *ferm_op = qf_majorana_to_fermion(maj_op);
// Apply Jordan-Wigner mapper to get qubit operator
QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_jordan_wigner(ferm_op, 2, &qubit_op);
assert(exit == QfExitCode_Success);
// Clean up
qf_ferm_op_free(ferm_op);
qf_maj_op_free(maj_op);
qk_obs_free(qubit_op);
שני השלבים למעלה נכתבים במפורש כדי להראות שני ממפים מהספרייה שמורכבים יחד. בפועל כדאי למפות MajoranaOperator בשלב אחד, עם majorana_jordan_wigner() (qf_maj_op_jordan_wigner() ב-API של C), וכך גם לסוגי האופרטורים האחרים – edge_vertex_jordan_wigner() ו-transfer_vertex_jordan_wigner() (qf_edge_op_jordan_wigner(), qf_transfer_op_jordan_wigner()). מעבר דרך FermionOperator גם מנפח את התוצאה הביניים: כל מחולל של האלגברות האלה ממופה למחרוזת Pauli אחת, ואילו כל פעולה פרמיונית ממופה לסכום של שני איברים, כך שאיבר שבנוי מכמה מחוללים מתרחב רק כדי להתמזג חזרה. החיסכון גדל עם אורך האיברים; באיברים של מחולל יחיד, כמו בהמילטוניאן קפיצות (hopping), שתי הדרכים עולות בערך אותו דבר.
ב-API של Python, חלק מהממפים האלה זמינים גם דרך ממפים נוחים שאינם תלויים בסוג. fermion_operator() ו-majorana_operator() עומדות לפני majorana_to_fermion() ו-fermion_to_majorana() (בין היתר), ומאצילות פנימית למימוש המתאים בספרייה לפי סוג האובייקט שקיבלו, כך שלא צריך לבחור בעצמכם את הפונקציה הנכונה. אותו הדבר חל על jordan_wigner() ועל ternary_tree() (שמוצגת בהמשך), וכל אחת מהן שולחת למימוש הישיר שמתאים לאופרטור שקיבלה. ראו qiskit_fermions.protocols כדי להבין איך השליחה הזו ממומשת פנימית.
בחירת קידוד עם עצים טרנריים
Jordan-Wigner הוא לא הקידוד היחיד מפרמיונים לקיוביטים, והוא גם לא הזול ביותר. מחרוזות ה- שלו גדלות עם אינדקס המוד, ולכן אופרטור Majorana על המוד האחרון מבין מודים ממופה למחרוזת Pauli במשקל . קידודים שמפזרים את מידע הזוגיות על פני עץ ולא על פני שרשרת מגיעים לתוצאות טובות יותר מבחינה אסימפטוטית.
עץ טרנרי על צמתים מכיל את כל המשפחה הזו. כל צומת הוא קיוביט עם שלושה קישורים כלפי מטה שמסומנים X, Y ו-Z; קישור או יורד לצומת בן (קשת) או נגמר (עלה). המסלול של כל עלה בחזרה לשורש מייצר מחרוזת Pauli: חציית הקישור שמסומן ויורד מהצומת תורמת על הקיוביט . כל עץ כזה מניב קידוד תקף, והקידודים המוכרים הם פשוט צורות מסוימות – שרשרת לאורך Z היא Jordan-Wigner, ואילו עץ שבו לכל צומת שלושה בנים, ככל שמספר הצמתים מאפשר, הוא עץ מאוזן.
- PYTHON
- C
>>> from qiskit_fermions.mappers import TernaryTree, TernaryTreeEncoding
>>>
>>> # Jordan-Wigner is a chain descending along Z, with X and Y as legs at every node.
>>> TernaryTree.chain(13, "Z").max_weight
13
>>> # The balanced tree spreads the same information over depth log_3(2n+1).
>>> TernaryTree.breadth_first(13, 3).max_weight
3
#include <qiskit_fermions.h>
QfTernaryTree *chain;
qf_ternary_tree_chain(13, QfPauliLabel_Z, &chain);
QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *balanced;
qf_ternary_tree_breadth_first(13, 3, order, &balanced);
assert(qf_ternary_tree_max_weight(chain) == 13);
assert(qf_ternary_tree_max_weight(balanced) == 3);
qf_ternary_tree_free(chain);
qf_ternary_tree_free(balanced);
המשקל המרבי הזה אופטימלי. Jiang et al. מוכיחים שלאופרטורי Majorana של כל מיפוי מפרמיונים לקיוביטים של מודים יש משקל Pauli ממוצע של לפחות , ולכן אף מיפוי לא יכול להוריד את הגדול שבהם מתחת ל-. שני העצים משתמשים בדיוק ב- קיוביטים, כך שהחיסכון הוא במשקל Pauli ולא במספר הקיוביטים.
כדי למפות אופרטור, מהדרים עץ לקידוד ומעבירים אותו לממפה יחד עם מספר הקיוביטים של התוצאה. ההידור מזווג את העלים של העץ לאופרטורי Majorana ובונה את מחרוזות ה-Pauli שלהם פעם אחת, ולכן כדאי להשתמש בקידוד אותו בהרבה אופרטורים. מספר הקיוביטים נשאר בקריאת המיפוי ולא בקידוד, כך שקידוד אחד יכול לשרת רגיסטרים ברוחבים שונים; מספר גדול ממספר הצמתים של העץ מרופד באיבר היחידה.
- PYTHON
- C
>>> from qiskit_fermions.mappers.library import fermion_ternary_tree
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> hamiltonian = FermionOperator.from_dict(
... {((True, 0), (False, 3)): 1.0, ((True, 3), (False, 0)): 1.0}
... )
>>> encoding = TernaryTreeEncoding(TernaryTree.breadth_first(4, 3))
>>> fermion_ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>
// Create the hopping Hamiltonian a^dagger_0 a_3 + a^dagger_3 a_0
QfFermionOperator *hamiltonian = qf_ferm_op_zero();
QkComplex64 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
bool actions[4] = {true, false, true, false};
uint32_t indices[4] = {0, 3, 3, 0};
for (int i = 0; i < 2; i++) {
qf_ferm_op_add_term(hamiltonian, 2, actions + 2 * i, indices + 2 * i, &coeffs[i]);
}
QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *tree;
qf_ternary_tree_breadth_first(4, 3, order, &tree);
// Compile the tree once, then reuse the encoding for every operator mapped through it.
QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);
QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_ternary_tree(hamiltonian, encoding, 4, &qubit_op);
assert(exit == QfExitCode_Success);
// Clean up
qf_ferm_op_free(hamiltonian);
qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(qubit_op);
מיפוי דרך שרשרת לאורך Z משחזר בדיוק את fermion_jordan_wigner() (qf_ferm_op_jordan_wigner() ב-API של C), וזו המשמעות של כך שהעצים הטרנריים מכלילים אותו:
>>> from qiskit_fermions.mappers.library import fermion_jordan_wigner
>>> chain = TernaryTreeEncoding(TernaryTree.chain(4, "Z"))
>>> direct = fermion_jordan_wigner(hamiltonian, 4)
>>> (fermion_ternary_tree(hamiltonian, chain, 4) - direct).simplify().num_terms
0
לכל ייצוג אופרטור יש ממפה של עץ טרנרי, בהתאמה לסט של Jordan-Wigner; ראו qiskit_fermions.mappers.library לרשימה המלאה, ו-Mapper Library ב-API של C. ב-API של Python הם זמינים גם דרך ternary_tree(), העטיפה שאינה תלויה בסוג ובוחרת את הממפה הנכון לאופרטור שקיבלה, בדיוק כמו ש-jordan_wigner() עושה:
>>> from qiskit_fermions.mappers.library import ternary_tree
>>> ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>
כמה זהויות שמתקיימות ב-Jordan-Wigner הן תכונות של השרשרת ולא של עצים טרנריים. אופרטור המספר הוא רק בשרשרת; בעץ אחר ה- הופך למחרוזת על קבוצה של קיוביטים. באותו אופן, אופרטור צומת הוא ה-Pauli ממשקל 1 רק בשרשרת, שבה שרשראות ה- של שתי מחרוזות Majorana מבטלות זו את זו.
מכיוון שכל אופרטור מספר עדיין ממופה למחרוזת אחת, מצב Fock הוא עדיין מצב בסיס חישובי בכל עץ טרנרי, אבל הביט כבר לא קורא את האכלוס . במקום זאת, אכלוס הוא הזוגיות של הביטים שנמדדו על התומך של התמונה של . קראו את התומכים האלה מהממפה ולא גזרו אותם מהעץ, כך שהפענוח נשאר נכון לכל צורה או תמורת מודים:
>>> for mode in range(4):
... number = FermionOperator.from_dict({((True, mode), (False, mode)): 1.0})
... print(mode, ternary_tree(number, encoding, 4).simplify())
0 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2 Z_1 Z_0)>
1 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_1)>
2 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2)>
3 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_3)>
בעץ המאוזן הזה, מוד 0 מאוכלס כשקיוביטים 0, 1 ו-2 מחזיקים מספר אי-זוגי של אחדות, ולכן הכנתו פירושה היפוך של קיוביט 0 בלבד, ואילו פענוח מחרוזת ביטים שנמדדה דורש זוגיות על שלושתם. שגיאת היפוך ביט בודדת על קיוביט 1 פוגעת אז באכלוסים של מוד 0 וגם של מוד 1, וזה חשוב לכל תהליך שקורא אכלוסים מדגימות רועשות.
כש-Evolution מורד דרך ממפה כזה, עטפו את הממפה ב-group_wise() (ראו סינתזה של שער Evolution במרחב פרמיוני). איבר קפיצה (hopping) ממופה לשתי מחרוזות Pauli מתחלפות, ואף אחת מהן לא משמרת מספר חלקיקים בפני עצמה; רק המכפלה שלהן באבולוציה משמרת. ב-Jordan-Wigner שתי המחרוזות פועלות על אותם קיוביטים, ולכן סדר איברים מפושט שומר אותן סמוכות. בעצים אחרים התומכים שלהן שונים (כמו Y_3 Z_1 Y_0 ו-X_3 Z_2 X_0 קודם), גם הסדר הגולמי וגם המפושט משלבים ביניהן את התמונות של איברים אחרים, ואז נוסחת מכפלה בצד הקיוביטים מדליפה מספר חלקיקים.
בניית עץ שרירותי
הבנאים של שרשרת ושל סריקה לרוחב מכסים את המשפחות האחידות, אבל הממשק הכללי מקבל זוג (parent, label) אחד לכל צומת, עם None לשורש (ב-API של C מקבלים במקום זאת את האינדקס של השורש). זה חשוב כי העצים שכדאי לחפש בדרך כלל אינם שרשרת ואינם אחידים – אלגוריתם Bonsai, למשל, מגדל עץ פורש של גרף הצימוד של המכשיר, כך שההסתעפות שלו עוקבת אחרי הקישוריות הזו.
- PYTHON
- C
>>> # Node 0 branches three ways, node 1 twice, and node 4 carries a single child.
>>> tree = TernaryTree([None, (0, "X"), (0, "Y"), (0, "Z"), (1, "X"), (1, "Z"), (4, "Y")])
>>> tree.num_nodes, tree.num_legs, tree.max_weight
(7, 15, 4)
>>> tree.children(0)
{'X': 1, 'Y': 2, 'Z': 3}
// Node 0, the root, branches three ways, node 1 twice, and node 4 carries a single child.
// The root has no parent and no incoming link, so its entries are never read: any value works.
#define IGNORED 0
uint32_t parents[7] = {IGNORED, 0, 0, 0, 1, 1, 4};
QfPauliLabel labels[7] = {
IGNORED, QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z,
QfPauliLabel_X, QfPauliLabel_Z, QfPauliLabel_Y,
};
QfTernaryTree *tree;
QfExitCode exit = qf_ternary_tree_new(7, 0, parents, labels, &tree);
assert(exit == QfExitCode_Success);
assert(qf_ternary_tree_num_nodes(tree) == 7);
assert(qf_ternary_tree_num_legs(tree) == 15);
assert(qf_ternary_tree_max_weight(tree) == 4);
qf_ternary_tree_free(tree);
שימו לב למספר העלים: הוא תמיד , לא משנה מהי הצורה. צומת עם פחות בנים לא מייצר פחות מחרוזות Pauli – הקישורים שלא בשימוש שלו הם עלים, ועלה מייצר מחרוזת בדיוק כמו צאצאים של קשת. מתוך המחרוזות האלה, מזדווגות לאופרטורי Majorana של המודים, ובדיוק אחת נשארת.
זוגיות כוללת
העלה שנשאר הוא הזוגיות הפרמיונית הכוללת . זו תכונה של הקידוד ולא מיפוי של אופרטור כלשהו, ולכן היא מתודה של הקידוד עצמו:
- PYTHON
- C
>>> TernaryTreeEncoding(TernaryTree.chain(4, "Z")).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_2 Z_1 Z_0)>
>>> TernaryTreeEncoding(TernaryTree.breadth_first(4, 3)).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_0)>
QfTernaryTree *tree;
qf_ternary_tree_chain(4, QfPauliLabel_Z, &tree);
QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);
// On the chain this is the full Z string; on the balanced tree it is not.
QkObs *parity;
qf_ternary_tree_encoding_total_parity(encoding, 4, &parity);
qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(parity);
כפי שהמקרה המאוזן מראה, זו לא בדרך כלל מחרוזת ה- המלאה: זה העלה שמגיעים אליו במסלול של בלבד, והוא פועל כאיבר היחידה על כל צומת שלא נמצא במסלול הזה.
הזוגיות הכוללת כאן היא observable למדידה, לא אילוץ להטלה. שני הערכים העצמיים שלה פיזיקליים, ולכן אל תטילו מצב על הסקטור : קידוד של עץ טרנרי משתמש בקיוביט אחד לכל מוד ומכסה את כל המרחב ממימד , כך שאין תת-מרחב לא פיזיקלי להטיל החוצה.
דוגמה מעובדת: עץ Bravyi-Kitaev
לרוב מחשבים מפרטים במקום להקליד אותם, כי העץ של קידוד אמיתי כמעט אף פעם לא קטן מספיק לכתיבה ידנית. Bravyi-Kitaev הוא דוגמה טובה: הוא שייך למשפחה הזו, אבל העץ שלו לא אחיד, ולכן אף בנאי נוח לא מכסה אותו.
העץ שלו נבנה בפיצול רקורסיבי של הצמתים, אחד לכל מוד. בטווח צמתים , יהי החזקה הגדולה ביותר של שתיים שנכנסת; הצומת שממוקם לטווח הוא , האחרון באותו בלוק. הקישור X שלו יורד לעץ שנבנה מהבלוק שלפניו, והקישור Z שלו לעץ שנבנה מהשארית שאחריו, וקישור Y שלו נשאר עלה:
>>> def bravyi_kitaev_tree(num_modes: int) -> TernaryTree:
... """Builds the ternary tree of the Bravyi-Kitaev encoding on ``num_modes`` modes."""
... spec: list[tuple[int, str] | None] = [None] * num_modes
... def build(left: int, right: int) -> int | None:
... if left > right:
... return None # an empty range: the parent's link stays a leg
... block = 1
... while 2 * block <= right - left + 1:
... block *= 2
... node = left + block - 1
... for child, label in (
... (build(left, node - 1), "X"), # the block before this node
... (build(node + 1, right), "Z"), # the remainder after it
... ):
... if child is not None:
... spec[child] = (node, label)
... return node
... build(0, num_modes - 1)
... return TernaryTree(spec)
בארבעה מודים כל הטווח הוא בלוק יחיד, ולכן הצומת שממוקם ראשון הוא 3, עם לפניו ושום דבר אחריו; תת-הטווח הזה נותן את צומת 1 מעל הצמתים הבודדים 0 ו-2. הידור העץ מאשר שהקידוד הוא Bravyi-Kitaev ולא רק עץ באותה משפחת צורות – התמונה הראשונה של Majorana שלו היא :
>>> tree = bravyi_kitaev_tree(4)
>>> tree.children(1)
{'X': 0, 'Z': 2}
>>> encoding = TernaryTreeEncoding(tree)
>>> encoding.majorana_image(0)
[(0, 'X'), (1, 'X'), (3, 'X')]
הזוגיות הכוללת שלו ממחישה את הנקודה מהסעיף הקודם: העלה שנשאר הוא זה שמגיעים אליו במסלול של בלבד, וכאן המסלול הזה נעצר בשורש, ולכן ה-observable הוא על קיוביט יחיד ולא מחרוזת על כל ארבעת הקיוביטים:
>>> encoding.total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3)>
כדי להגדיל לא צריך כלום חוץ מארגומנט גדול יותר. מכיוון שאף צומת לא מבזבז את קישור ה-Y שלו על קשת, כל אחד מסתעף לכל היותר בשתי דרכים, ולכן משקל Pauli גדל באופן לוגריתמי ולא ליניארי:
>>> [bravyi_kitaev_tree(n).max_weight for n in (4, 8, 16, 32, 64)]
[3, 4, 5, 6, 7]