انتقل إلى المحتوى الرئيسي
هذا المشروع في مرحلة تجريبية. وجدت خطأ؟ افتح مشكلة على GitHub. لديك سؤال أو فكرة؟ اسأل في Discussions — نحب سماع ملاحظاتك!
كيف جودة الترجمة في هذه الصفحة؟

ركّب بوابة Evolution في الفضاء الفرميوني

مهم

المفاهيم في هذا الدليل متاحة حاليًا في واجهة Python البرمجية فقط. هتتوفر وظائف مكافئة في واجهة C البرمجية في إصدار قادم.

بوابة Evolution بتحمل التطور الزمني e−itHe^{-itH} لمُشغّل كامل. التركيب من فرميون لفرميون بيعيد كتابة البوابة دي في FermionicCircuit من بوابات فرميونية أصغر. جانبا إعادة الكتابة بيفضلوا في الفضاء الفرميوني، وده اللي بيميّزه عن تركيب الفرميون للكيوبت اللي بييجي بعده: groups بتاعة المُشغّل (شوف تجميع حدود المُشغّل: استخدم بنية المُشغّل) بتفضل موجودة بعده، بينما تحويل الفرميون للكيوبت بيرجّع SparseObservable، اللي ملهاش مفهوم للتجميع.

فيه مفتاحين بيحددوا الطريقة. Evolution.synthesis بيحددها على بوابة واحدة، وتمريرة الـ transpiler اسمها F2FSynthesis بتحددها على دائرة كاملة. الاتنين بياخدوا F2FSynthesisPlugin، الواجهة اللي أي إعادة كتابة من النوع ده بتنفّذها. الطرق اللي الحزمة دي بتوفرها حاليًا للتطور هي صيغ الضرب (product formulas)، وهي اللي باقي الدليل بيستخدمها: FermionicLieTrotter لصيغة من الرتبة الأولى (الافتراضية) وFermionicSuzukiTrotter لرتبة زوجية أعلى. أي حاجة تانية بتنفّذ الواجهة بتتحدد بنفس الطريقة، والأقسام الخاصة بالمفتاحين بتنطبق عليها من غير تغيير.

ملاحظة

خطوة الفرميون-لفرميون دي اختيارية. بوابة Evolution تقدر تروح مباشرةً لمرحلة الفرميون للكيوبت، اللي بتحوّلها وتركّبها كاملة مهما كان عدد الحدود اللي فيها. تقسيمها الأول اختيار، بيتعمل لأن البوابات الناتجة أرخص كل واحدة لوحدها أو لأن التقسيم بيكشف بنية تقدر مرحلة لاحقة تستفيد منها.

الخطوتين بيتركبوا مع بعض، فالتطور ممكن يتقرّب على أي من المستويين، أو الاتنين، أو ولا واحد. صيغة الضرب بتقرّب إلا لو عواملها بتتبادل؛ طريقة التركيب مش لازم تقرّب خالص.

ملاحظة

تخطي الخطوة دي مش لازم يعني فقدان التجميع بالكامل. تغليف الـ mapper بـ group_wise() بيحوّل المُشغّل مجموعة مجموعة وبيجمع النتائج، فحدود Pauli المحوّلة بتوصل مجمّعة مش متداخلة. صيغة ضرب على جانب الكيوبتات بتركّبها بالترتيب اللي بتستلمه، فالحدود المتبادلة اللي بتنتهي متجاورة ممكن تتجدول مع بعض.

على الهاميلتوني اللي تحت ده بيسترجع كل فايدة عمق البوابات ثنائية الكيوبت من التقسيم: 18 بدل 30، بنفس عدد البوابات. الفرق هو مكان التعبير عن البنية. التقسيم في الفضاء الفرميوني بيخلّي كل مجموعة بوابة منفصلة، تقدر كل مرحلة لاحقة تشوفها وتتصرف عليها؛ بينما group_wise بيسيب بوابة واحدة وبيعيد ترتيب الحدود جواها بس، وده كفاية للجدولة لكن مش بيسيب حاجة لتمريرة فرميونية تشتغل عليها. استخدمه لما يكون لازم التطور يفضل بوابة واحدة، واستخدم صيغة ضرب فرميونية في غير كده.

ابنِ هاميلتوني مجمّع​

صيغتا الضرب بتقسّما المُشغّل لعوامل، وفي المُشغّل المجمّع العوامل دي هي مجموعاته. فالتجميع هو اللي بيحدد تكلفة النتيجة ودقتها. خُد سلسلة Fermi-Hubbard من ست مواقع، مجمّعة بالطريقة اللي بتتبادل بيها حدودها: حدود القفز على الروابط الزوجية، وحدود القفز على الروابط الفردية، والتفاعل القطري.

>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> def fermi_hubbard_chain(num_sites, tunneling, interaction):
... """A 1D Fermi-Hubbard chain grouped into even bonds, odd bonds, and the interaction."""
... terms, groups = [], []
... for offset in (0, 1):
... for j in range(offset, num_sites - 1, 2):
... terms += [
... ([cre(j), ann(j + 1)], -tunneling),
... ([cre(j + 1), ann(j)], -tunneling),
... ]
... groups += [offset, offset]
... for j in range(num_sites - 1):
... terms.append(([cre(j), ann(j), cre(j + 1), ann(j + 1)], interaction))
... groups.append(2)
... operator = FermionOperator.from_terms(terms)
... operator.groups = groups
... return operator
>>>
>>> num_sites = 6
>>> hamiltonian = fermi_hubbard_chain(num_sites, tunneling=1.0, interaction=2.0)
>>> hamiltonian.num_groups()
3

خمستاشر حد في تلات مجموعات. كل مجموعة قفز بتقرن كل حد بشريكه المرافق، وده مهم: العامل لازم يكون هيرميتيًا عشان أسّه يبقى وحدوي، وده مش بيتحقق تلقائيًا من كون المجموع هيرميتي. groups_are_hermitian() بتتحقق من الاصطلاح ده بدل ما تسيبه للفحص اليدوي:

>>> from qiskit_fermions.operators.terms.grouping import groups_are_hermitian
>>>
>>> groups_are_hermitian(hamiltonian)
[True, True, True]

فصل a0†a1a^\dagger_0 a_1 عن شريكه a1†a0a^\dagger_1 a_0 هيكسر ده، وترك المُشغّل من غير تجميع ليه نفس التأثير، لأن الصيغتين وقتها بتقسماه حد حد وai†aja^\dagger_i a_j لوحده مش هيرميتي:

>>> ungrouped = fermi_hubbard_chain(num_sites, tunneling=1.0, interaction=2.0)
>>> ungrouped.groups = range(len(list(ungrouped.iter_terms()))) # one group per term
>>> all(groups_are_hermitian(ungrouped))
False

عامل زي ده هو مُشغّل فرميوني صالح، والحزمة دي هتبني له البوابة، فمفيش حاجة بتشتكي لحد ما المُشغّل المحوّل يوصل PauliEvolutionGate، اللي بتطلب معاملات حقيقية وبترفع ValueError: Operator contains complex coefficients, which are not supported. التحقق من التجميع مقدمًا بيحوّل الفشل المتأخر ده لإجابة متاحة قبل بناء أي دائرة.

حدد الطريقة على بوابة واحدة​

مرّر طريقة تركيب للبوابة، وبعدين حلّلها. كل بوابة بتطلّعها بترجع كبوابة Evolution منفصلة:

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import (
... FermionicLieTrotter,
... FermionicSuzukiTrotter,
... )
>>>
>>> total_time = 1.0
>>>
>>> def factors(synthesis):
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(num_sites, hamiltonian, time=total_time, synthesis=synthesis),
... circuit.modes,
... )
... return circuit.decompose().count_ops()["Evolution"]
>>>
>>> factors(FermionicLieTrotter())
3
>>> factors(FermionicSuzukiTrotter(order=2))
5
>>> factors(FermionicSuzukiTrotter(order=2, reps=4))
17
>>> factors(FermionicSuzukiTrotter(order=2, reps=4, merge_adjacent=False))
20

الرتبة الأولى بتطلّع عامل واحد لكل مجموعة. الرتبة التانية بتطلّع palindrome متماثل، فالمجموعتين الخارجيتين بيظهروا مرتين بنص الزمن كل مرة، فيطلع خمسة. أربع تكرارات منه هتدّي عشرين، لكن كل تكرار بينتهي على نفس المجموعة اللي التكرار اللي بعده بيبدأ بيها. البوابات المتجاورة دي بتتطور تحت نفس المُشغّل، فبتندمج في بوابة واحدة بالزمن المجموع، وده بيوفّر بوابة لكل وصلة. مرّر merge_adjacent=False عشان تحافظ على التسلسل الكتبي. كل رتبة بتقسم المُشغّل بنفس الطريقة؛ اللي بيتغير هو ترتيب العوامل وأزمنة تطورها، مش التقسيم.

ملاحظة

العوامل متعلّمة بـ atomic، فالتحليل مرة تانية بيسيبها مكانها بدل ما يقسمها أكتر. التحليل ليه نقطة ثابتة، وده اللي بيخلّي استدعاء decompose() مرارًا آمن.

حدد الطريقة لخط معالجة كامل​

تحديد synthesis لكل بوابة معناه تمرير الاختيار عبر كل حاجة بتبني واحدة، بما فيها UCC وUCJ، اللي بيبنوا بواباتهم داخليًا. تمريرة F2FSynthesis بتطبّق طريقة واحدة على كل Evolution في الدائرة بدل كده، فالاختيار بيتعمل مرة واحدة للخط كله. بتتضبط باسم البوابة، بنفس طريقة F2QSynthesis، والبوابة اللي مش مسمّاة بتتساب زي ما هي:

>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import F2FSynthesis
>>>
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
>>>
>>> expanded = FermionicPassManager(
... F2FSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2, "reps": 4})})
... ).run(circuit)
>>> expanded.count_ops()["Evolution"]
17

ده نفس العدد اللي decompose() بيدّيه للطريقة دي فوق. تمريرة MergeEvolutions بتطبّق نفس الدمج على البوابات المتجاورة اللي مجتش من تركيب واحد، زي الوصلات بين الدوائر المتتالية. التمريرة بتنفّذ الصيغة بنفسها، فمفيش حاجة لخطوة توسيع منفصلة، وتشغيلها مرتين مش بيغيّر حاجة، لأن العوامل اللي بتطلّعها atomic فمالهاش تعريف خاص بيها.

ملاحظة

مرّر apply=False عشان تختار الطريقة بس وتسيب التوسيع لحاجة تانية، زي Decompose بتاعة Qiskit (تسمية البوابة كـ Decompose("Evolution") بتقصرها على التطورات، وبتسيب البوابات الفرميونية التانية لـ plugins التركيب الخاصة بيها).

ده مفيد لاختيار الطريقة لدائرة كاملة مقدمًا مع تأجيل التوسيع للحظة اللي بتحتاجه فيها، مثلًا لفحص الدائرة الأول:

>>> selected = FermionicPassManager(
... F2FSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}, apply=False)
... ).run(circuit)
>>> selected.count_ops()["Evolution"] # selected, not yet expanded
1
>>> selected.decompose().count_ops()["Evolution"] # expanded on demand
5

خلّي بالك من اللي بتعمله البوابة غير الموسّعة، مع ذلك: بتوصل لمرحلة الفرميون للكيوبت كاملة، وبتتحوّل من غير ما synthesis يتراجع. وده بيعمل غلطة سهلة عند مقارنة الطرق، لأن كل واحدة وقتها بتطلّع مخرجات متطابقة والمقارنة بتقيس لا شيء في صمت. على الهاميلتوني اللي فوق، ترك بوابة الرتبة التانية غير موسّعة بيطوي الدائرة الأكبر بكتير اللي تحت لتكلفة تطور محوّل واحد، مهما كانت الطريقة المختارة.

قِس العائد​

الرتبة الأعلى بتكلّف عمق، فالسؤال مش هل هي أدق لكن هل هي أدق لكل بوابة. محتاجين رقمين لكل طريقة: خطأ الحالة اللي بتطلّعها، وتكلفة الكيوبتين للدائرة اللي طلّعتها.

الخطأ بيتقاس باستخدام ffsim، اللي بيحاكي FermionicCircuit مباشرةً (شوف إزاي الحزمة دي بترتبط بـ ffsim). مفيش تحويل فرميون-لكيوبت داخل، فالمقارنة بتعزل صيغة الضرب الفرميونية عن أي حاجة ممكن التحويل أو التركيب على جانب الكيوبتات يضيفها. وكمان بتشتغل في قطاع عدد الجسيمات الثابت، اللي هو أصغر من الفضاء الكامل 2N2^N وهو الفضاء اللي التطور بيفضل فيه فعلًا.

>>> import ffsim
>>> import numpy as np
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> norb, nelec = num_sites, (3, 0) # three spinless fermions on six modes
>>> initial = ffsim.hartree_fock_state(norb, nelec)
>>>
>>> hamiltonian_operator = ffsim.linear_operator(hamiltonian, norb=norb, nelec=nelec)
>>> exact = expm_multiply(
... -1j * total_time * hamiltonian_operator,
... initial,
... traceA=-1j * total_time * ffsim.trace(hamiltonian, norb=norb, nelec=nelec),
... )
>>>
>>> def cost_and_error(synthesis):
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(num_sites, hamiltonian, time=total_time, synthesis=synthesis),
... circuit.modes,
... )
... # the method is set on the gate, so decompose() is what expands it here; a pipeline
... # would reach for F2FSynthesis instead (see the note above)
... circuit = circuit.decompose()
... evolved = ffsim.apply_unitary(initial, circuit, norb=norb, nelec=nelec)
... error = 1 - abs(np.vdot(evolved, exact)) ** 2
... qubit_circuit = generate_preset_jw_pass_manager().run(circuit).decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return error, qubit_circuit.count_ops()["cx"], qubit_circuit.depth(two_qubit)
>>>
>>> methods = [
... ("order 1, reps 1 ", FermionicLieTrotter()),
... ("order 2, reps 1 ", FermionicSuzukiTrotter(order=2)),
... ("order 1, reps 10", FermionicLieTrotter(reps=10)),
... ("order 2, reps 4 ", FermionicSuzukiTrotter(order=2, reps=4)),
... ]
>>> costs = {label: cost_and_error(synthesis) for label, synthesis in methods}
>>> labels, counts, depths = [], [], []
>>> for label, (error, gates, depth) in costs.items():
... labels.append(label.strip().replace(", ", "\n"))
... counts.append(gates)
... depths.append(depth)
... print(f"{label} error {error:.6f}")
order 1, reps 1 error 0.515997
order 2, reps 1 error 0.180958
order 1, reps 10 error 0.004026
order 2, reps 4 error 0.000421

تكلفة الكيوبتين اتساب برّه الجدول ده عن قصد. بتعتمد على التركيب على جانب الكيوبتات اللي Qiskit بتختاره، واللي بيتحسن بين الإصدارات، فالأرقام المطبوعة هنا هتبقى لقطة مش خاصية لصيغة الضرب. الشكل التالي بيرسمهم بدل كده، والحاجة اللي المقارنة قايمة عليها بتتأكد كعلاقة:

Grouped bars of CX count and depth per method, with order two at four repetitions clearly shorter than order one at ten.

اقرأ آخر طريقتين مع بعض، لأنهم المقارنة اللي تهم. الرتبة التانية بأربع تكرارات أدق بحوالي عشر مرات من الرتبة الأولى بعشر تكرارات، وبتوصل لكده ببوابات تشابك أقل مش أكتر، زي ما الشكل بيوضّح. صرف ميزانية البوابات على رتبة أعلى أحسن من صرفها على خطوات أكتر من الرتبة الأولى هنا.

النتيجة دي مش عامة. زيادة reps وزيادة order بيقلّلوا الخطأ بمعدلات مختلفة لنفس نوع التكلفة، واللي بيكسب بيعتمد على المُشغّل وعلى الزمن اللي بيتطور. الصفين اللي فوق هم الطريقة عشان تعرف ده لمسألة معينة.

ملاحظة

الصيغة الفرميونية بتتركّب مع اللي اتختارت لمرحلة الفرميون للكيوبت (شوف MapperFnEvolutionSynthesis.product_formula). حيث الاتنين بيقرّبوا، دقة النتيجة بتتحكم فيها الأضعف بينهم، فرفع الرتبة هنا بينما صيغة جانب الكيوبتات تفضل من الرتبة الأولى مش بيفيد كتير.

قصر التمريرة على البوابات اللي بتستفيد​

الرتبة الأعلى بتفيد بس لما العوامل مش بتتبادل. لو كلها بتتبادل، صيغة الرتبة الأولى بتبقى دقيقة أصلًا والعوامل الزيادة بتبقى عبء صافي. مُشغّل قطري بحت بيوضّح ده، لأن مُشغّلات العدد بتتبادل مع بعض:

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> diagonal = FermionOperator.from_terms(
... [([cre(j), ann(j), cre(j + 1), ann(j + 1)], 1.0 + 0.1 * j) for j in range(num_sites - 1)]
... )
>>> diagonal.groups = list(range(num_sites - 1))
>>>
>>> def diagonal_cost(synthesis):
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(num_sites, diagonal, time=total_time, synthesis=synthesis), circuit.modes
... )
... transpiled = generate_preset_jw_pass_manager().run(circuit.decompose())
... return transpiled.decompose(reps=6).count_ops()["cx"]
>>>
>>> diagonal_cost(FermionicLieTrotter())
10
>>> diagonal_cost(FermionicSuzukiTrotter(order=2))
18

الدائرتين دقيقتين، فالتانية بتصرف 80% بوابات تشابك زيادة من غير فايدة. مُشغّلات Coulomb القطرية في UCJ ليها الشكل ده، وده سبب وجود F2FSynthesis.filter: بياخد دالة شرط على DAGOpNode وبيسيب البوابات المرفوضة من غير لمس، فتمريرة واحدة تقدر ترفع الرتبة على البوابات اللي بتستفيد وتسيب الباقي على الرتبة الأولى.

دائرة فيها النوعين من التطور بتوضّح اللي ده بيوفّره. دالة الشرط اللي تحت بتقبل بوابة بس لما مُشغّلها بيحرّك جسيمات بين الأنماط، وده بالظبط لما المجموعات مش بتتبادل وفيه حاجة تشتريها الرتبة الأعلى:

>>> def is_diagonal(operator):
... """Whether every term only counts particles, rather than moving them between modes."""
... return all(
... sorted(mode for created, mode in actions if created)
... == sorted(mode for created, mode in actions if not created)
... for actions, _ in operator.iter_terms()
... )
>>>
>>> def mixed_circuit():
... circuit = FermionicCircuit(num_sites)
... circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
... circuit.append(Evolution(num_sites, diagonal, time=total_time), circuit.modes)
... return circuit
>>>
>>> def two_qubit_cost(circuit):
... transpiled = generate_preset_jw_pass_manager().run(circuit).decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return transpiled.count_ops()["cx"], transpiled.depth(two_qubit)
>>>
>>> order_2 = FermionicSuzukiTrotter(order=2)
>>>
>>> everywhere = two_qubit_cost(
... FermionicPassManager(F2FSynthesis({"Evolution": order_2})).run(mixed_circuit())
... )
>>> restricted = two_qubit_cost(
... FermionicPassManager(
... F2FSynthesis(
... {"Evolution": order_2}, filter=lambda node: not is_diagonal(node.op.operator)
... )
... ).run(mixed_circuit())
... )
>>> everywhere[0] - restricted[0] # the gates the filter saved
8
>>> restricted[0] < everywhere[0] and restricted[1] < everywhere[1]
True

البوابات اللي الفلتر بيوفّرها هي بالظبط اللي الرتبة التانية صرفتها على التطور القطري، واللي مقارنة القطري اللي فوق بتسعّرها بتمانية (18 مقابل 10). ماشترتش حاجة هناك، لأن التطور ده دقيق أصلًا في الرتبة الأولى، فحذفها مش بيكلّف أي دقة.

ملاحظة

قارن دائرتي الرتبة التانية ببعض، مش بالدائرة غير الموسّعة. ترك البوابتين غير موسّعتين بيبعت كل واحدة لمرحلة الفرميون للكيوبت كاملة، وده الغلطة اللي ملاحظة apply=False فوق بتوصفها: ممكن توصل لنفس عدد بوابات التشابك زي الدائرة المفلترة وهي دائرة مختلفة بعمق مختلف، فقراءة فايدة الفلتر من الرقم ده بتقيس لا شيء.

تحذير

ولا واحد من المفتاحين بيتحقق إن العوامل هيرميتية، والمُشغّل المجمّع غلط ممكن يبان أحسن في رتبة زوجية منه في الرتبة الأولى وهو لسه غلط، لأن التماثل بيلغي الخطأ جزئيًا. جمّع الشركاء المرافقين مع بعض، زي ما fermi_hubbard_chain فوق بيعمل.

الخطوات الجاية​