توليف بوابة Evolution في الفضاء البوزوني
التطور الزمني لهاملتوني هو مجموع أجزاء لا تتبادل لا يمكن تنفيذه بدقة بوابةً بوابة. صيغة الضرب تقرّبه بسلسلة من التطورات القصيرة تحت الأجزاء الفردية، وخطأ التقريب يسمى خطأ Trotter.
ممكن تطبّق صيغة الضرب في مكانين، وما هما قابلين للتبديل:
-
في الفضاء البوزوني، قبل ما يتم اختيار أي اقتطاع أو ترميز. الصيغة ترى مجموعات المؤثر (شوف تجميع حدود المؤثر: استخدم بنية المؤثر) ويمكنها تداخلها.
-
في فضاء الكيوبتات، داخل
PauliEvolutionGateفي Qiskit، بعد تعيين المؤثر. الصيغة ترى مؤثر باولي واحد لكل مجموعة ولا تقدر إلا على تقسيم الحدود داخلها.
فقط الأول يقدر يقلل الخطأ بين المجموعات التي لا تتبادل، وهو عادة الخطأ المهيمن. هذا الدليل يوضح كيف تستخدمه.
الخطوتان اختياريتان. بوابة Evolution التي لا تُفكَّك في الفضاء البوزوني تصل كاملة إلى مرحلة البوزون-إلى-كيوبت وتُعيَّن وتُوَلَّف هناك. وهذا لا يسبب أي خطأ على المستوى البوزوني، فقط الخطأ الذي تُدخله صيغة مستوى الكيوبتات.
الإعداد
سلسلة Bose-Hubbard من موقعين، وهي أصغر مثال لا تتبادل فيه المجموعات (شوف محاكاة ديناميكيات Bose-Hubbard مع اختيار الترميز للنموذج الكامل). تُجمَّع في مجموعة هرميتية واحدة للرابطة ومجموعة قطرية واحدة للتفاعل داخل الموقع:
>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp, Statevector
>>> from qiskit.transpiler import PassManager
>>> from scipy.sparse import csc_matrix
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> num_sites, max_occupation, total_time = 2, 2, 0.8
>>> bonds = [(0, 1)]
>>> terms = {}
>>> for left, right in bonds:
... terms[(cre(left), ann(right))] = -1.0
... terms[(cre(right), ann(left))] = -1.0
>>> for site in range(num_sites):
... terms[(cre(site), cre(site), ann(site), ann(site))] = 1.0
>>> hamiltonian = BosonOperator.from_dict(terms)
>>> hamiltonian.groups = [
... len(bonds)
... if len({mode for _, mode in term}) == 1
... else bonds.index(tuple(sorted({mode for _, mode in term})))
... for term, _ in hamiltonian.iter_terms()
... ]
>>> hamiltonian.num_groups()
2
لقياس خطأ لازم يكون عندك مرجع. التطور الدقيق للهاملتوني كما عُيِّن عند نفس الاقتطاع هو المرجع الصحيح، لأن الاقتطاع تقريب منفصل:
>>> spec = LevelSpec(max_occupation, "unary_projected")
>>> mapped = occupation_encoding(hamiltonian, spec).simplify()
>>> matrix = csc_matrix(SparsePauliOp.from_sparse_observable(mapped).to_matrix(sparse=True))
>>> pm = generate_preset_occupation_pass_manager(spec)
>>> pm.qubit = PassManager() # keep each group as one exact PauliEvolutionGate
>>>
>>> def build(**evolution_kwargs):
... circuit = BosonicCircuit(num_sites)
... circuit.append(InitializeModes([1, 1]), circuit.modes)
... circuit.append(
... Evolution(num_sites, hamiltonian, time=total_time, **evolution_kwargs), circuit.modes
... )
... return circuit
>>>
>>> # Under the unary encodings, occupation ``n`` of mode ``m`` sets qubit ``m * d + n``, with
>>> # ``d = max_occupation + 1`` levels per mode. That places |1, 1> on the basis state below.
>>> levels = max_occupation + 1
>>> index = sum(1 << (mode * levels + n) for mode, n in enumerate([1, 1]))
>>> initial = Statevector.from_int(index, 2**(num_sites * levels)).data
>>> exact = expm_multiply(-1j * total_time * matrix, initial)
>>> def error(circuit):
... """Distance to the exact evolution, of the circuit's product of group exponentials."""
... lowered = pm.run(circuit)
... state = Statevector.from_int(0, 2**lowered.num_qubits).evolve(lowered).data
... return float(np.max(np.abs(state - exact)))
error يحاكي الدائرة المخفَّضة عمدًا بدون تفكيك نسخ PauliEvolutionGate فيها. Statevector يطبّق كل واحدة كأُسّ دقيق لمجموعتها، فالشيء الوحيد المتبقي هو حاصل ضرب أسس المجموعات الذي اختاره التوليف البوزوني.
الافتراضي
بدون وسيط synthesis تتفكك بوابة Evolution باستخدام BosonicLieTrotter، وهي صيغة الرتبة الأولى: عامل واحد لكل مجموعة، كل واحد للزمن كله.
>>> dict(build().decompose().count_ops())
{'Evolution': 2, 'InitializeModes': 1}
بدون تفكيك، تُسلَّم البوابة كلها إلى مرحلة الكيوبتات وما في أي خطأ Trotter بوزوني أبدًا:
>>> error(build()) < 1e-6
True
بعد التفكيك يظهر خطأ حقيقي، لأن المجموعتين لا تتبادلان:
>>> error(build().decompose()) > 0.1
True
قلّل الخطأ
مرّر طريقة توليف مختلفة للبوابة. الرتبة الأعلى تركّب العوامل بشكل متماثل بحيث تلغي حدود الخطأ الرئيسية بعضها؛ والتكرارات الأكثر تقسم الزمن إلى خطوات أقصر.
>>> from qiskit_bosons.circuit.library.synthesis import BosonicLieTrotter, BosonicSuzukiTrotter
>>>
>>> for label, synthesis in [
... ("Lie-Trotter", BosonicLieTrotter()),
... ("Suzuki, order 2", BosonicSuzukiTrotter(order=2)),
... ("Suzuki, order 4", BosonicSuzukiTrotter(order=4)),
... ("Suzuki, order 4*", BosonicSuzukiTrotter(order=4, merge_adjacent=False)),
... ]:
... circuit = build(synthesis=synthesis).decompose()
... print(f"{label:16s} {circuit.count_ops()['Evolution']:3d} factors")
Lie-Trotter 2 factors
Suzuki, order 2 3 factors
Suzuki, order 4 11 factors
Suzuki, order 4* 15 factors
كل زيادة في الرتبة بمقدار اثنين تركّب خمس مسحات من الرتبة اللي تحتها (نمو في تعاودية Suzuki، مطبّقًا على العوامل من الرتبة 2)، وهذا هو العدد في السطر الأخير، المعلَّم بنجمة. المسحات تلتقي عند نفس العامل، فالبوابات المجاورة تتطور تحت مؤثر واحد وتُدمج في بوابة واحدة بالزمن المجموع. هذا يوفر بوابة عند كل وصلة وهو دقيق. مرّر merge_adjacent=False للحفاظ على التسلسل الكلاسيكي. في الحالتين، الرتبة الأعلى تعطي خطأ أصغر:
>>> lie = error(build(synthesis=BosonicLieTrotter()).decompose())
>>> second = error(build(synthesis=BosonicSuzukiTrotter(order=2)).decompose())
>>> fourth = error(build(synthesis=BosonicSuzukiTrotter(order=4)).decompose())
>>> lie > second > fourth
True
التكرارات تبادل العمق بالدقة بمعدل مختلف. خطأ الرتبة الأولى ينخفض خطيًا مع حجم الخطوة، وخطأ الرتبة الثانية تربيعيًا:
>>> lie_errors = [error(build(synthesis=BosonicLieTrotter(reps=r)).decompose()) for r in (1, 2, 4, 8)]
>>> bool(all(a > b for a, b in zip(lie_errors, lie_errors[1:])))
True
>>> bool(lie_errors[0] / lie_errors[-1] > 6) # about 8, i.e. linear in 1 / reps
True
>>> suzuki_errors = [
... error(build(synthesis=BosonicSuzukiTrotter(order=2, reps=r)).decompose()) for r in (1, 2, 4)
... ]
>>> bool(suzuki_errors[0] / suzuki_errors[-1] > 10) # about 16, i.e. quadratic in 1 / reps
True
أي مقايضة أفضل تعتمد على المؤثر. لاحظ أن المؤثر ذا المجموعة الواحدة دقيق عند أي رتبة، لأن العامل المنفرد يتبادل مع نفسه، وعندها تنهار الصيغة إلى بوابة واحدة بدل ما تقسّمها من جديد.
قِس الكلفة
الخطأ الأصغر يكلّف عوامل أكثر، وكل عامل يكلّف بوابات تشابك بمجرد ما تُوَلِّفه مرحلة الكيوبتات. كم عددها يعتمد على الترميز، فعدّها عبر خط الأنابيب الجاهز، مع تثبيت الأساس وبدون تحسين على مستوى الكيوبتات (optimization_level=0)، لترى سعر كل طريقة:
>>> basis = ["rz", "sx", "x", "cx"]
>>>
>>> def cx_count(circuit, encoding):
... spec = LevelSpec(max_occupation, encoding)
... lowered = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=basis
... ).run(circuit)
... return lowered.count_ops().get("cx", 0)
>>>
>>> methods = [
... ("Lie-Trotter", BosonicLieTrotter()),
... ("Suzuki, order 2", BosonicSuzukiTrotter(order=2)),
... ("Suzuki, order 4", BosonicSuzukiTrotter(order=4)),
... ]
>>> encodings = ("gray", "unary_unprojected")
>>> counts = {
... encoding: [
... cx_count(build(synthesis=method).decompose(), encoding) for _, method in methods
... ]
... for encoding in encodings
... }
>>> # the order buys accuracy at a price that grows with it
>>> bool(counts["gray"][0] < counts["gray"][1] < counts["gray"][2])
True
الأعداد الدقيقة تعتمد على كيف يوَلِّف Qiskit بوابات التطور، وهذا يتغير بين الإصدارات، فالرسم يعرضها بدل جدول:
كل مجموعة أعمدة هي طريقة: Lie-Trotter وSuzuki من الرتبة 2 وSuzuki من الرتبة 4. الرتبة تشتري الدقة بسعر ينمو بعامل التعاودية، والترميز الأحادي يدفع أكثر من شيفرة Gray لكل طريقة لأن كل عامل فيه يُعيَّن إلى بوابات تشابك أكثر. اختر الطريقة حسب الدقة اللي تحتاجها والترميز حسب العتاد، وخلّي الاختيارين منفصلين: الدائرة البوزونية أعلاه ما تتغير أبدًا.
طبّق طريقة واحدة على دائرة كاملة
لتعيين synthesis على كل بوابة، لازم تمرّر الاختيار عبر كل شيء يبني واحدة. التمريرة B2BSynthesis تتخذ الاختيار مرة واحدة لخط الأنابيب كله بدل ذلك. الوسيط config فيها يسمّي الطريقة لكل نوع بوابة، وهي تختار الطريقة وتوسّع البوابات معًا:
>>> from qiskit_bosons.transpiler import BosonicPassManager
>>> from qiskit_bosons.transpiler.passes import B2BSynthesis
>>>
>>> trotterize = BosonicPassManager([B2BSynthesis({"Evolution": BosonicSuzukiTrotter(order=2)})])
>>> dict(trotterize.run(build()).count_ops())
{'Evolution': 3, 'InitializeModes': 1}
ويمكن كمان تسمية الطريقة، مع وسائط __init__ الخاصة بها، بدل بنائها: B2BSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}). الأسماء المتاحة هي أسماء B2BSynthesisPluginManager، والطريقة غير المسجَّلة في أي مكان يمكن تمريرها كنسخة، كما فوق.
مع apply=False التمريرة تختار الطريقة فقط وتترك التوسيع للمكان اللي تريده فيه، مثلًا لفحص الدائرة أولًا. البوابة الذرية لا تُمَس أبدًا، ولا حتى توسَم:
>>> selected = BosonicPassManager(
... [B2BSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}, apply=False)]
... ).run(build())
>>> selected.count_ops()["Evolution"] # selected, not yet expanded
1
>>> selected.decompose().count_ops()["Evolution"] # expanded on demand
3
البوابة غير الموسَّعة تصل كاملة إلى مرحلة البوزون-إلى-كيوبت، وهناك تُعيَّن بدون الرجوع إلى synthesis الخاص فيها، فكل طريقة تنتج نفس المخرجات عندها.
filter اختياري يقيّدها على البوابات اللي تستفيد. التطورات التي تتبادل كل مجموعاتها، مثل هاملتوني مكوَّن من مؤثرات العدد فقط، تُوَلَّف بدقة عند أي رتبة، فالرتبة الأعلى ما بتضيف هناك غير العمق.
قيّد التمريرة على البوابات التي تستفيد
المؤثر القطري تمامًا يوضّح الفكرة، لأن مؤثرات العدد تتبادل مع بعضها. مجموعاته دقيقة عند الرتبة الأولى، والرتبة الثانية تصرف بوابات تشابك بدون فائدة:
>>> # one group per term, each Hermitian; the order is fixed because order two evolves the first
>>> # group twice, so which group comes first changes the cost
>>> diagonal = BosonOperator.from_terms_with_groups(
... [([cre(0), ann(0), cre(1), ann(1)], 1.0, 0), ([cre(0), ann(0)], 0.5, 1)]
... )
>>>
>>> def diagonal_cost(synthesis):
... circuit = BosonicCircuit(num_sites)
... circuit.append(Evolution(num_sites, diagonal, time=total_time, synthesis=synthesis), circuit.modes)
... return cx_count(circuit.decompose(), "gray")
>>>
>>> bool(diagonal_cost(BosonicSuzukiTrotter(order=2)) > diagonal_cost(BosonicLieTrotter()))
True
كلا الدائرتين دقيقة، فالثانية تصرف بوابات تشابك أكثر بدون مكسب. (صيغة الرتبة الثانية المتماثلة تطبّق المجموعة الأولى مرتين بنصف الزمن في كل مرة، ولذلك الكلفة تعتمد على أي مجموعة تأتي أولًا.) الـ filter يأخذ شرطًا على DAGOpNode ويترك البوابات المرفوضة بدون لمس، فتمريرة واحدة ترفع الرتبة على البوابات التي تستفيد وتبقي الباقي عند الرتبة الأولى. الشرط أدناه يقبل بوابة فقط حين يحرّك مؤثرها بوزونات بين الأنماط، وهذا هو الوقت اللي لا تتبادل فيه المجموعات:
>>> def is_diagonal(operator):
... """Whether every term only counts bosons, 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 = BosonicCircuit(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
>>>
>>> order_2 = BosonicSuzukiTrotter(order=2)
>>> everywhere = cx_count(
... BosonicPassManager([B2BSynthesis({"Evolution": order_2})]).run(mixed_circuit()), "gray"
... )
>>> restricted = cx_count(
... BosonicPassManager(
... [
... B2BSynthesis(
... {"Evolution": order_2},
... filter=lambda node: not is_diagonal(node.op.operator),
... )
... ]
... ).run(mixed_circuit()),
... "gray",
... )
>>> bool(everywhere > restricted) # the filter saves entangling gates
True
إذا شغّلت التمريرة مرة ثانية، ما يتغير شيء: العوامل التي تُخرجها ذرية.
العوامل الذرية
بوابة Evolution الموسومة atomic ما إلها تعريف، فـ decompose() يتركها في مكانها مهما تكرر تطبيقه. التوليف يوسم العوامل اللي يخرجها بهذه الطريقة، وكذلك يفعل BosonicQDrift للحدود التي يأخذ عيناتها.
هذا أكثر من مجرد ترتيب. تقسيم مجموعة هرميتية أكثر، حدًّا حدًّا، ينتج عوامل غير هرميتية كل واحد لحاله، وأُسّ مؤثر غير هرميتي ليس أحاديًا:
>>> circuit = build()
>>> once = circuit.decompose()
>>> dict(circuit.decompose(reps=5).count_ops()) == dict(once.count_ops())
True
كل عامل لازم يكون هرميتيًا بمفرده. المؤثر الهرميتي المقسَّم إلى مجموعات قد ينتج مجموعات غير هرميتية، مثل حد قفز مفصول عن مرافقه. تحقق باستخدام groups_are_hermitian(). المؤثر بدون مجموعات يُقسَّم حدًّا حدًّا، ثم ترفض مرحلة الكيوبتات العوامل بالرسالة Operator contains complex coefficients. تماثل الرتبة الزوجية يلغي جزئيًا خطأ عامل غير هرميتي، فالمؤثر المجمَّع خطأ قد يبدو أفضل عند الرتبة 2 منه عند الرتبة 1 وهو ما زال خطأ.
كرّر خطوة قصيرة
كبديل على مستوى الدائرة، ابنِ خطوة Trotter قصيرة واحدة وكرّرها. repeat() يسطّح التكرارات في دائرة واحدة ويترك الحدود متجاورة، فالتمريرات تقدر تعمل عبرها:
>>> step = BosonicCircuit(num_sites)
>>> step.append(Evolution(num_sites, hamiltonian, time=total_time / 4), step.modes)
>>> dict(step.repeat(4).count_ops())
{'Evolution': 4}
مرّر insert_barriers=True لإبقاء التكرارات منفصلة. الحاجز يُعيَّن على كل كيوبت في السجلات التي يلمسها، لأن الترميز يحتاج عدة كيوبتات لكل نمط وما في للنمط صورة لكل كيوبت خاصة فيه:
>>> [inst.operation.name for inst in step.repeat(3, insert_barriers=True)._inner.data]
['Evolution', 'barrier', 'Evolution', 'barrier', 'Evolution']
repeat لا علاقة له بـ reps الخاص بطريقة التوليف. التوليف يقسم تطورًا واحدًا إلى عوامل أكثر وأقصر. أما تكرار دائرة فيها تطور واحد فيضاعف زمنه الكلي فقط.
ادمج مع صيغة على مستوى الكيوبتات
المستويان يتركّبان، وحيث يقرّب كلاهما، تتحدد دقة النتيجة بالأضعف منهما. رفع الرتبة البوزونية بينما صيغة جانب الكيوبتات تبقى من الرتبة الأولى يعطي فائدة قليلة. شوف MapperFnEvolutionSynthesis لصيغة جانب الكيوبتات. هي تحافظ على ترتيب حدود باولي الذي اختاره الـ Mapper، وبالتالي group_wise() يقدر يُخرج حدود كل مجموعة متجاورة، وsimplify() يقدر يثبّت ترتيبًا قانونيًا بدل ذلك.
الخطوات التالية
-
عمل Transpile للدوائر البوزونية يشرح المراحل التي تلي التوليف البوزوني، وأين تقع
B2BSynthesisبينها. -
تجميع حدود المؤثر: استخدم بنية المؤثر يشرح كيف تعيّن المجموعات التي تتداخلها صيغة الضرب وتتحقق منها.
-
محاكاة ديناميكيات Bose-Hubbard مع اختيار الترميز يطبّق مخطط التكرار على مثال كامل.