تحويل تمثيلات المؤثرات
المحوِّلات (Mappers) هي روتينات تحوّل المؤثرات من تمثيل إلى آخر مع الحفاظ على التكافؤ الرياضي. استخدمها للعمل بمرونة عبر جبريات المؤثرات المختلفة ولتحضير المؤثرات لتنفيذ الدارات الكمومية.
تنفيذ محوِّلات مخصصة
تتيح وحدة المحوِّلات تنفيذ تحويلات مخصصة بسهولة. تعمل المحوِّلات بتحويل الأفعال الأساسية (التوليد والإفناء وماجورانا وغيرها) التي يتكوّن منها المؤثر، ثم تجميع هذه الأفعال المحوَّلة وفق قواعد التمثيل الهدف.
كمثال ملموس، فكّر في تحويل جوردان-فيغنر (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.
الخطوات الأساسية لتنفيذ أي مُخطِّط مخصّص هي:
-
عرّف تحويل الفعل: حدّد كيف يُحوَّل كل فعل (مُفهرَس حسب النمط) إلى التمثيل الهدف. في المثال أعلاه، تُحوَّل أفعال ماجورانا إلى سلاسل باولي وفق قواعد جوردان-فيغنر.
-
استخدم إحدى دوال التخطيط الجاهزة: توفّر الوحدة
mappersدوال مساعدة للمرور على حدود المُشغّل ثم تطبيق خريطة الفعل المخصّصة على تمثيلات المُشغّلات التي توفّرها الوحدةoperators. مثلًا، تعملmap_majorana_action_generators()مع مُشغّلات ماجورانا. -
احصل على النتيجة: تُرجع دالة التخطيط مُشغّلًا بالتمثيل الهدف مع تطبيق التحويل بشكل متسق على كل الحدود.
هذا التصميم يسهّل تجربة تخطيطات مخصّصة دون معرفة عميقة بالبُنى الداخلية للمُشغّلات. يُطبَّق التحويل تلقائيًا على كل حد في المُشغّل مع الحفاظ على التكافؤ الرياضي.
يمكنك أيضًا المرور على حدود المُشغّل يدويًا بدل استخدام إحدى دوال المرور الجاهزة. راجع قسم المرور على الحدود في دليل المُشغّلات لمزيد من التفاصيل.
تطبيقات المكتبة
توفّر الوحدة qiskit_fermions.mappers.library تطبيقات فعّالة ومُختبَرة جيدًا للتخطيطات الشائعة. تتبع هذه التطبيقات النمط الأساسي نفسه للمُخطِّطات المخصّصة لكنها محسّنة للاستخدام الإنتاجي ومتاحة في واجهتي Python و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() في واجهة C)، وبالمثل لأنواع المُشغّلات الأخرى – edge_vertex_jordan_wigner() وtransfer_vertex_jordan_wigner() (qf_edge_op_jordan_wigner()، qf_transfer_op_jordan_wigner()). كما أن المرور عبر FermionOperator يضخّم النتيجة الوسيطة: فكل مولّد في هذه الجبريات يُحوَّل إلى سلسلة باولي واحدة، بينما يُحوَّل كل فعل فرميوني إلى مجموع من حدّين، فيتوسّع الحد المبني من عدة مولّدات ثم يُدمَج مجددًا. ويزداد هذا التوفير مع طول الحدود؛ أما في الحدود ذات المولّد الواحد لهاميلتونيان القفز فتكلّف الطريقتان تقريبًا الشيء نفسه.
في واجهة Python، يمكن أيضًا الوصول إلى بعض هذه المُخطِّطات عبر مُخطِّطات مريحة لا تعتمد على النوع. تقف fermion_operator() وmajorana_operator() أمام majorana_to_fermion() وfermion_to_majorana() (وغيرها)، وتفوّض داخليًا إلى التطبيق المناسب في المكتبة بحسب نوع الكائن المُعطى، بدل أن تضطر إلى اختيار الدالة الصحيحة بنفسك. وينطبق الأمر نفسه على jordan_wigner() وعلى ternary_tree() (المقدَّمة أدناه)، إذ تُوجّه كل منهما إلى التطبيق المباشر المطابق للمُشغّل المُعطى. راجع qiskit_fermions.protocols لمعرفة كيف يُنفَّذ هذا التوجيه داخليًا.
اختر ترميزًا باستخدام الأشجار الثلاثية
جوردان-فيغنر ليس الترميز الوحيد من فرميون إلى كيوبت، وليس الأرخص. تنمو سلاسل فيه مع فهرس النمط، لذا يُحوَّل مُشغّل ماجورانا على آخر نمطًا إلى سلسلة باولي بوزن . أما الترميزات التي توزّع معلومات التكافؤ على شجرة بدل سلسلة فتؤدي أفضل بشكل مقارب.
تغطي الشجرة الثلاثية ذات عقدة هذه العائلة كلها. كل عقدة هي كيوبت يحمل ثلاثة روابط نازلة موسومة X وY وZ؛ والرابط إما ينزل إلى عقدة ابنة (حافة) أو ينتهي (ورقة). يُهجّي مسار كل ورقة نحو الجذر سلسلة باولي: عبور الرابط الموسوم النازل من العقدة يُسهم بـ على الكيوبت . كل شجرة كهذه تعطي ترميزًا صالحًا، والترميزات المألوفة ليست سوى أشكال خاصة – سلسلة على امتداد Z هي جوردان-فيغنر، أما الشجرة التي لكل عقدة فيها ثلاثة أبناء قدر ما يسمح عدد العقد فهي متوازنة.
- 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 وآخرون أن مُشغّلات ماجورانا لأي تخطيط من فرميون إلى كيوبت لـ نمطًا لها وزن باولي متوسط لا يقل عن ، فلا يمكن لأي تخطيط أن يخفض أكبرها عن . تستخدم الشجرتان كيوبت بالضبط، فالتوفير في وزن باولي لا في عدد الكيوبتات.
لتخطيط مُشغّل، اجمع الشجرة في ترميز ومرّره إلى مُخطِّط مع عدد الكيوبتات للنتيجة. يقرن الجمع أوراق الشجرة في مُشغّلات ماجورانا ويبني سلاسل باولي الخاصة بها مرة واحدة، لذا ينبغي إعادة استخدام الترميز نفسه عبر مُشغّلات كثيرة. يبقى عدد الكيوبتات مع استدعاء التخطيط لا مع الترميز، فيمكن لترميز واحد أن يخدم سجلات بعروض مختلفة؛ وعدد أكبر من عدد عقد الشجرة يُملأ بالمحايد.
- 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() في واجهة 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
لكل تمثيل للمُشغّلات مُخطِّط شجرة ثلاثية، يحاكي مجموعة جوردان-فيغنر؛ راجع qiskit_fermions.mappers.library للقائمة الكاملة، ومكتبة المُخطِّطات في واجهة C. وفي واجهة 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)>
بعض المتطابقات التي تصح في جوردان-فيغنر هي خصائص السلسلة لا الأشجار الثلاثية. مُشغّل العدد هو على السلسلة فقط؛ وعلى شجرة أخرى يصبح سلسلة على مجموعة من الكيوبتات. وبالمثل مُشغّل الرأس هو باولي بوزن 1 على السلسلة فقط، حيث تُلغي سلاسل لسلسلتَي ماجورانا بعضهما.
بما أن كل مُشغّل عدد ما يزال يُحوَّل إلى سلسلة واحدة، تبقى حالة فوك حالة أساس حسابية تحت أي شجرة ثلاثية، لكن البت لم يعد يقرأ الإشغال . بل الإشغال هو تكافؤ البتات المقيسة على دعم صورة . اقرأ هذه الدعائم من المُخطِّط بدل اشتقاقها من الشجرة، ليبقى فك الترميز صحيحًا لأي شكل أو تبديل للأنماط:
>>> 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 في الفضاء الفرميوني). يُحوَّل حد القفز إلى سلسلتَي باولي متبادلتين، لا تحفظ أي منهما عدد الجسيمات بمفردها؛ بل حاصل ضربهما في التطور فقط. تحت جوردان-فيغنر تعمل السلسلتان على الكيوبتات نفسها، فيُبقيهما ترتيب الحدود المبسّط متجاورتين. أما تحت أشجار أخرى فتختلف دعائمهما (كما في Y_3 Z_1 Y_0 وX_3 Z_2 X_0 سابقًا)، ويُدخل الترتيبان الخام والمبسّط صورًا لحدود أخرى بينهما، فتُسرّب صيغة الجداء من جهة الكيوبتات عدد الجسيمات.
ابنِ شجرة اعتباطية
تغطي مُنشئات السلسلة والعرض أولًا العائلات المنتظمة، لكن الواجهة العامة تأخذ زوجًا (parent, label) لكل عقدة، مع None للجذر (وتأخذ واجهة 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);
لاحظ عدد الأوراق: هو دائمًا مهما كان الشكل. العقدة ذات الأبناء الأقل لا تولّد سلاسل باولي أقل – فروابطها غير المستخدمة أوراق، والورقة تُهجّي سلسلة تمامًا كما تفعل أحفاد الحافة. من هذه السلاسل ، تُقرن منها في مُشغّلات ماجورانا للأنماط ، وتبقى واحدة بالضبط.
التكافؤ الكلي
تلك الورقة المتبقية هي التكافؤ الفرميوني الكلي . هو خاصية للترميز لا تخطيط لأي مُشغّل، لذا فهو دالة على الترميز نفسه:
- 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);
كما تُظهر الحالة المتوازنة، هذه ليست في العموم سلسلة الكاملة: بل هي الورقة التي يصل إليها المسار الكامل، وتعمل كمحايد على كل عقدة ليست على ذلك المسار.
التكافؤ الكلي هنا مرصودة تُقاس، لا قيد يُفرض. كلتا قيمتيه الذاتيتين فيزيائيتان، فلا تُسقِط حالة على القطاع : فترميز الشجرة الثلاثية يستهلك كيوبتًا واحدًا لكل نمط وهو شامل للفضاء الكامل ذي البُعد ، ولا يترك فضاءً جزئيًا غير فيزيائي لإسقاطه.
مثال عملي: شجرة برافي-كيتايف
تُحسب المواصفات في الغالب بدل أن تُكتب يدويًا، إذ نادرًا ما تكون شجرة ترميز حقيقي صغيرة بما يكفي لكتابتها باليد. برافي-كيتايف مثال جيد: فهو ينتمي إلى هذه العائلة لكن شجرته ليست منتظمة، فلا يغطيه أي مُنشئ مريح.
تُبنى شجرته بتقسيم تعاودي على العقد، عقدة لكل نمط. ضمن نطاق عقد ، ليكن أكبر قوة للعدد اثنين تناسب النطاق؛ والعقدة الموضوعة لهذا النطاق هي ، آخر عقدة في تلك الكتلة. يهبط رابطها 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. يؤكد جمع الشجرة أن الترميز هو برافي-كيتايف وليس مجرد شجرة من فئة الشكل نفسها – فصورة ماجورانا الأولى له هي :
>>> 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')]
يوضّح تكافؤه الكلي نقطة القسم السابق: الورقة المتبقية هي التي يصل إليها المسار الكامل، وهنا يتوقف ذلك المسار عند الجذر، فتكون المرصودة هي لكيوبت واحد بدل سلسلة على الكيوبتات الأربعة جميعها:
>>> encoding.total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3)>
لا يتطلب التوسّع سوى وسيط أكبر. وبما أن أي عقدة لا تنفق رابط Y على حافة، فإن كلًا منها يتفرّع إلى اتجاهين على الأكثر، فينمو وزن باولي لوغاريتميًا لا خطيًا:
>>> [bravyi_kitaev_tree(n).max_weight for n in (4, 8, 16, 32, 64)]
[3, 4, 5, 6, 7]