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

تحويل الدارات البوزونية

يشرح هذا الدليل كيفية تحويل BosonicCircuit إلى QuantumCircuit قياسية. الدارة البوزونية لا تحمل اقتطاعًا ولا ترميزًا ولا عددًا للكيوبتات (انظر العمل مع الدارات البوزونية)، لذلك فالمترجم (transpiler) هو المكان الذي تُتَّخذ فيه هذه الخيارات. يُوصَف الاقتطاع والترميز بواسطة LevelSpec (انظر وصف الاقتطاع باستخدام LevelSpec)، ويُوصَف المُحوِّل (mapper) الذي يحوّل المؤثرات إلى مؤثرات كيوبتات في تحويل تمثيلات المؤثرات. والنتيجة QuantumCircuit عادية، لذلك يمكن لأي محاكٍ متوافق مع Qiskit تشغيلها.

يستخدم الدليل مثالًا صغيرًا واحدًا طوال الوقت: نمطان بوزونيان، وحالة فيها بوزون واحد في كل منهما، وتطور زمني واحد في ظل هاميلتوني بوز-هوبارد حدود الروابط وحدود الموقع فيه في مجموعتين منفصلتين (انظر تجميع حدود المؤثر: استخدام بنية المؤثر):

>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import Evolution, InitializeModes
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> terms = {(cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0}
>>> terms.update({(cre(mode), cre(mode), ann(mode), ann(mode)): 0.5 for mode in range(2)})
>>> hamiltonian = BosonOperator.from_dict(terms)
>>> # the bond is group 0 and the on-site interaction is group 1; derive the index from the term,
>>> # because the stored order of a dictionary is not specified
>>> hamiltonian.groups = [
... 0 if len({mode for _, mode in term}) == 2 else 1 for term, _ in hamiltonian.iter_terms()
... ]
>>>
>>> circuit = BosonicCircuit(2)
>>> circuit.append(InitializeModes([1, 1]), circuit.modes)
>>> circuit.append(Evolution(2, hamiltonian, time=0.1), circuit.modes)
>>> dict(circuit.count_ops())
{'InitializeModes': 1, 'Evolution': 1}

مراحل التحويل​

كما هو موضح في qiskit_bosons.transpiler، تقسّم مديرات التمريرات الجاهزة في هذه الحزمة التحويل إلى المراحل التالية:

الإدخال​

تحوّل الدارة إلى بنية بيانات رسم بياني موجَّه لا دوري (DAG)، وهو التمثيل الذي تعمل عليه كل التمريرات اللاحقة.

التحسين​

تُبقي الدارة في الفضاء البوزوني. تستخدم هذه التمريرات بنية المؤثرات البوزونية، وخاصة مجموعاتها، بينما لا توجد أي معلومات عن الكيوبتات بعد. B2BSynthesis هي التمريرة التي تفكّك البوابات هنا، بإضافة لكل نوع من البوابات: صيغة ضرب أو qDRIFT لـ Evolution، وشبكة كليمنتس (Clements) لـ ModeRotation (انظر تركيب بوابة Evolution في الفضاء البوزوني لمعرفة كيفية اختيار الطريقة). RelabelModes تعيد ترتيب الأنماط. يحلّل الإعداد الجاهز دورانات الأنماط في هذه المرحلة ويترك كل شيء آخر كما هو.

التخطيط​

تحوّل كل سجل أنماط بوزونية إلى سجل كمّي. النمط البوزوني يشغل عمومًا عدة كيوبتات (dd منها في الترميزات الأحادية (unary)، و⌈log⁡2d⌉\lceil \log_2 d \rceil في الترميزات المدمجة)، لذلك يكون السجل الكمّي أعرض من سجل الأنماط. OccupationB2QLayout تشتق عرض السجل من LevelSpec.

التركيب​

تحوّل البوابات البوزونية إلى عمليات كيوبتات، بإضافة واحدة لكل نوع بوابة. يحدث الترميز في هذه الخطوة: إضافة بوابة Evolution تحوّل مؤثرها إلى مؤثر كيوبتات، وإضافة بوابة InitializeModes تضبط الكيوبتات التي ترمّز كل إشغال. ويُنقل الحاجز (barrier) إلى كل كيوبت في كل سجل مسّه.

الكيوبت​

تحوّل دارة الكيوبتات الناتجة بطرق Qiskit نفسها، مثلًا إلى مجموعة بوابات أساسية أو إلى جهاز. يتضمنها الإعداد الجاهز دائمًا؛ وافتراضيًا هي مدير التمريرات الجاهز في Qiskit، الذي يركّب كل تطور إلى بوابات (انظر المتابعة على مستوى الكيوبت).

الإخراج​

تحوّل بنية بيانات DAG الداخلية مرة أخرى إلى QuantumCircuit.

الاقتطاع والترميز لا يُخزَّنان في الدارة. يُعطيان للمترجم، ويجب أن يتفق عليهما التخطيط وتحضير الحالة والمُحوِّل جميعًا. يضمن الإعداد الجاهز ذلك بتمرير LevelSpec نفسها إلى الثلاثة.

مثال عملي​

generate_preset_occupation_pass_manager() تبني خط المعالجة من مواصفة. تحويل المثال إلى مستوى أدنى تحت ترميزين لا يغيّر سوى المواصفة. ينتهي الإعداد الجاهز بتحويل Qiskit نفسه على مستوى الكيوبت، الذي سيركّب التطور إلى بوابات. لرؤية الدارة تمامًا كما تُخرجها المراحل البوزونية، فرّغ تلك المرحلة أولًا:

>>> from qiskit.transpiler import PassManager
>>> from qiskit_bosons.mappers import LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> for encoding in ("unary_unprojected", "gray"):
... pass_manager = generate_preset_occupation_pass_manager(LevelSpec(2, encoding))
... pass_manager.qubit = PassManager()
... lowered = pass_manager.run(circuit)
... print(f"{encoding:18s} qubits={lowered.num_qubits} ops={dict(lowered.count_ops())}")
unary_unprojected qubits=6 ops={'x': 2, 'PauliEvolution': 1}
gray qubits=4 ops={'x': 2, 'PauliEvolution': 1}

يستخدم الترميزان ستة وأربعة كيوبتات على التوالي (ثلاثة مستويات لكل نمط في الترميز الأحادي، وكيوبتان لكل نمط في رمز غراي)، لكن بنية الدارة واحدة: عمليتان من XGate تحضّران الإشغالات، وبوابة PauliEvolutionGate واحدة تحمل التطور.

مقارنة مع سير العمل التقليدي​

يصف دليل الدارات البوزونية سير العمل التقليدي، الذي يحوّل الهاميلتوني إلى مؤثر كيوبتات قبل أن يبني QuantumCircuit. المساران ينتجان دارات متكافئة. ويختلفان فيما يجب إعادته عندما يتغير الاقتطاع. الدارة البوزونية تُبنى مرة واحدة وتُحوَّل عند كل حد أقصى، بينما يحتاج مسار التحويل أولًا إلى إعادة تحويل المؤثر وإعادة بناء الدارة:

>>> from qiskit import QuantumCircuit
>>> from qiskit.circuit.library import PauliEvolutionGate
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>>
>>> basis = ["rz", "sx", "x", "cx"]
>>> cx_counts = []
>>> for max_occupation in (1, 2, 3):
... spec = LevelSpec(max_occupation, "gray")
... # the bosonic circuit is reused as it is
... lowered = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=basis
... ).run(circuit)
... # the map-first route maps the operator and builds a new circuit for every cutoff
... qubit_hamiltonian = occupation_encoding(hamiltonian, spec)
... by_hand = QuantumCircuit(qubit_hamiltonian.num_qubits)
... _ = by_hand.append(PauliEvolutionGate(qubit_hamiltonian, time=0.1), by_hand.qubits)
... cx_counts.append(lowered.count_ops().get("cx", 0))
... print(
... f"d={max_occupation + 1}: {lowered.num_qubits} qubits; map-first: "
... f"{len(qubit_hamiltonian)} Pauli terms to place by hand"
... )
d=2: 2 qubits; map-first: 2 Pauli terms to place by hand
d=3: 4 qubits; map-first: 10 Pauli terms to place by hand
d=4: 4 qubits; map-first: 22 Pauli terms to place by hand

عدد بوابات التشابك يعتمد على كيفية تركيب Qiskit لبوابة التطور، وهو يتغير بين الإصدارات، لذلك ارسمه بيانيًا:

CX count of the lowered circuit against the number of levels d, growing quickly with d.

يزداد العدد بسرعة مع الحد الأقصى، وهذا سبب إبقاء الحد الأقصى معاملًا للتحويل وليس للدارة. المساران متكافئان هنا، لكن المسار البوزوني وحده يمكنه أيضًا تداخل تجميع المؤثر مع صيغة الضرب (انظر تركيب بوابة Evolution في الفضاء البوزوني) أو تسليم الترميز إلى مواصفة مختلفة دون تغيير سطر واحد من الدارة.

المواصفة ذات الحد الأقصى الواحد تترك عدد الأنماط مفتوحًا، لذلك يخدم مدير تمريرات واحد دارات بأي عرض. أما المواصفة ذات حد أقصى لكل نمط فتثبّت العرض:

>>> wide = BosonicCircuit(3)
>>> wide.append(InitializeModes([1, 0, 1]), wide.modes)
>>> shared = generate_preset_occupation_pass_manager(LevelSpec(2, "gray"))
>>> shared.run(wide).num_qubits
6
>>> per_mode = generate_preset_occupation_pass_manager(LevelSpec([2, 1], "gray"))
>>> per_mode.run(circuit).num_qubits
3
>>> try:
... per_mode.run(wide)
... except ValueError as error:
... print(error)
the spec covers 2 modes, but 3 were expected

بناء خط المعالجة يدويًا​

الإعداد المسبق هو MultiStagePassManager بإدخال واحد لكل مرحلة. تقدر تجمّع نفس خط المعالجة من أجزائه، وهذه هي نقطة البداية لكل التخصيصات أدناه:

>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.transpiler import BosonicCircuitToDAG, QuantumDAGToCircuit
>>> from qiskit_bosons.transpiler.passes import (
... B2QSynthesis,
... MapperFnEvolutionSynthesis,
... OccupationB2QLayout,
... OccupationInitializeModesSynthesis,
... )
>>>
>>> spec = LevelSpec(2, "gray")
>>>
>>> def mapper_fn(operator, num_qubits):
... return occupation_encoding(operator, spec, num_qubits)
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(mapper_fn)
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>>
>>> by_hand = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> preset = generate_preset_occupation_pass_manager(spec)
>>> preset.qubit = PassManager()
>>> by_hand.run(circuit) == preset.run(circuit)
True

دالة المُخطِّط (mapper) تستقبل مُشغِّلًا وعرض المسجّل الذي استنتجه التخطيط، وترجع SparseObservable. ولأنها تستلم العرض، فتمرير num_qubits إلى occupation_encoding() يخلّي المُخطِّط يتحقق من أن المواصفة تناسب المسجّل، فأي عدم تطابق بين التخطيط والترميز يفشل برسالة واضحة.

سير العمل المتقدم​

تحديد تخطيط صريح​

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

>>> from qiskit.circuit import QuantumRegister
>>> from qiskit_bosons.transpiler.passes import CustomB2QLayout
>>>
>>> (mode_register,) = BosonicCircuitToDAG().run(circuit).qregs.values()
>>> custom = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=CustomB2QLayout({mode_register: QuantumRegister(6, "q")}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> custom.run(circuit).num_qubits
6
تحذير

التخطيط المخصص مسؤوليتك. إضافات التركيب (synthesis plugins) ما زالت تتعامل مع الكيوبتات عبر المواصفة، فمسجّل لا يطابق عروض المواصفة يعطي دائرة خاطئة أو خطأ في أعماق الإضافة. والتخطيط المخصص كمان يتخطى الفحص الذي يجريه OccupationB2QLayout بعد RelabelModes (شوف القسم التالي).

إعادة ترتيب الأنماط​

RelabelModes هو تمريرة (pass) في مرحلة التحسين تعيد ترتيب الأنماط. التبديل يُقرأ كتعيين من فهرس النمط الأصلي إلى الجديد، ويُسجَّل في البيانات الوصفية للدائرة. إعادة الترتيب تغيّر الكيوبتات التي يشغلها كل نمط، ولذلك ممكن تقلل عمق الدائرة بعد الخفض.

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

>>> # BasicSimulator is a convenient pure-Python choice; any simulator works
>>> from qiskit.providers.basic_provider import BasicSimulator
>>> from qiskit_bosons.transpiler.passes import RelabelModes
>>>
>>> state = BosonicCircuit(3)
>>> state.append(InitializeModes([2, 0, 1]), state.modes)
>>> spec = LevelSpec(2, "gray") # three levels, two qubits per mode
>>> width = spec.resolve(3).widths[0]
>>> permutation = [2, 0, 1]
>>>
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> relabeled = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=RelabelModes(permutation),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... ).run(state)
>>> relabeled.metadata
{'permutation': [2, 0, 1]}
>>>
>>> def restore(bitstring, permutation, width):
... """Map a bitstring in the new mode order back to the original mode order."""
... qubits = bitstring[::-1] # character q is now the value of qubit q
... blocks = [qubits[mode * width : (mode + 1) * width] for mode in range(len(permutation))]
... return "".join(blocks[new] for new in permutation)[::-1]
>>>
>>> relabeled.measure_all()
>>> (measured,) = BasicSimulator().run(relabeled, shots=1).result().get_counts()
>>> measured
'110100'
>>> restore(measured, permutation, width)
'010011'

سلسلة البتات المستعادة هي ما تنتجه الدائرة بدون إعادة التسمية، مقروءة بترتيب Qiskit (little-endian): النمط 0 يحمل ترميز غراي للمستوى اثنين (11)، والنمط 1 يحمل المستوى صفر (00)، والنمط 2 يحمل المستوى واحد (01).

>>> plain = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... ).run(state)
>>> plain.measure_all()
>>> (reference,) = BasicSimulator().run(plain, shots=1).result().get_counts()
>>> restore(measured, permutation, width) == reference
True
تحذير

مواصفة تعطي الأنماط حدود قطع (cutoffs) مختلفة ما ينفع تجمعها مع RelabelModes. حدود القطع مرتبطة بالأنماط حسب موضعها، فإعادة ترتيب الأنماط تضع حد قطع على نمط خاطئ. OccupationB2QLayout يرفض دائرة أُعيدت تسميتها في هذه الحالة:

>>> mixed = LevelSpec([2, 1, 2], "gray")
>>> synthesis = B2QSynthesis()
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(mixed)
>>> refused = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=RelabelModes(permutation),
... layout=OccupationB2QLayout(mixed),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> try:
... refused.run(state)
... except ValueError as error:
... print(str(error).split(",")[0])
the circuit has been relabeled by RelabelModes

اختيار إضافات التركيب​

B2QSynthesis ما عنده إضافات افتراضية؛ الإعداد المسبق يسجّل واحدة لكل نوع بوابة. الإضافات المتاحة يديرها B2QSynthesisPluginManager، الذي يسردها باسم البوابة التي تعمل عليها:

>>> from qiskit_bosons.transpiler.passes import B2QSynthesisPluginManager
>>> manager = B2QSynthesisPluginManager()
>>> for gate in manager.op_names():
... print(gate, manager.method_names(gate))
Evolution ['MapperFn']
InitializeModes ['Occupation']

الطريقتان المدمجتان هما Evolution.MapperFn وInitializeModes.Occupation. الأولى تركّب تطورًا من أي دالة مُخطِّط، وهكذا يُوصَل ترميز مخصص (شوف Map operator representations). المكتبة ما تشحن أي إضافة لبوابة ModeRotation. الإعداد المسبق يفككها في الفضاء البوزوني إلى بوابات Evolution أولًا، وتتولاها الطريقة الأولى بعدها، لكن إضافة مسجّلة لـModeRotation تقدر تخفض البوابة مباشرة بدل ذلك؛ شوف Rotate bosonic modes وLower a rotation without decomposing it. تقدر تسجّل طريقة بالاسم عبر المعامل config في B2QSynthesis، أو تكتب كائن إضافة مباشرة في B2QSynthesis.methods، مثل ما تفعل الأمثلة السابقة.

المتابعة على مستوى الكيوبت​

الإعداد المسبق ينتهي دائمًا بمرحلة qubit، وهي مدير التمريرات المسبق الخاص بـ Qiskit. المعاملات المسماة في generate_preset_occupation_pass_manager() تُمرَّر إلى qiskit.transpiler.generate_preset_pass_manager()، فتقدر تترجم إلى مجموعة بوابات أساسية أو تستهدف جهازًا مباشرة. فضّل optimization_level=0 عشان تبقي الدائرة قريبة مما أنتجته المراحل البوزونية:

>>> from qiskit.transpiler import generate_preset_pass_manager
>>>
>>> spec = LevelSpec(2, "gray")
>>> translated = generate_preset_occupation_pass_manager(
... spec, optimization_level=0, basis_gates=["rz", "sx", "x", "cx"]
... ).run(circuit)
>>> sorted(translated.count_ops())
['cx', 'rz', 'sx', 'x']

فيه خياران شائعان آخران. عيّن PassManager فارغًا لمرحلة qubit لما تبغى تعرض الدائرة بالضبط كما ركّبها خط المعالجة، مثل ما تفعل الأمثلة أعلاه. أو جمّع خط المعالجة بنفسك وأضف أي مدير تمريرات من Qiskit كمرحلة qubit في MultiStagePassManager. المراحل تُشغَّل بالترتيب الذي تُعطى به، فلازم تأتي مرحلة qubit قبل output، لأن تمريرات Qiskit تتوقع DAG مش دائرة منتهية:

>>> synthesis = B2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... lambda operator, num_qubits: occupation_encoding(operator, spec, num_qubits)
... )
>>> synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
>>> staged = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(
... optimization_level=0, basis_gates=["rz", "sx", "x", "cx"]
... ),
... output=QuantumDAGToCircuit(),
... )
>>> sorted(staged.run(circuit).count_ops())
['cx', 'rz', 'sx', 'x']

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