تعيين تمثيلات المؤثرات
يحوّل المُعيِّن (mapper) كائن BosonOperator إلى مؤثر كيوبتات، وهو هنا SparseObservable. المؤثر نفسه لا يعرف شيئًا عن الكيوبتات: فهو يخزّن مؤثرات السلّم على الأنماط، والنمط البوزوني يحمل عددًا اعتباطيًا من الجسيمات. كل ما يجعل المؤثر منتهيًا (عدد مستويات الإشغال التي تحتفظ بها، وكيف تُوزَّع هذه المستويات على الكيوبتات) يتحدد عند التعيين.
يعمل المُعيِّن المكتبي occupation_encoding() في أساس الإشغال (Fock): المستوى من النمط هو الحالة التي فيها بوزونات. ويأخذ المؤثر وLevelSpec، الذي يحمل البتر والترميز. يشرح هذا الدليل معنى هذين الخيارين وتكلفة كل منهما. للاطلاع على مسار متكامل يستخدمهما، راجع محاكاة ديناميكيات Bose-Hubbard مع اختيار الترميز؛ ولمعرفة كيف يطبّق المحوِّل (transpiler) الخيارات نفسها على دائرة كاملة، راجع تحويل الدوائر البوزونية (Transpile).
بتر فضاء Fock
النمط الذي يُحتفَظ به عند مستوى، حيث هو max_occupation الخاص به، لم يعد يحقق علاقة التبادل القانونية. بكتابة لمؤثر الإفناء المبتور إلى تلك المستويات،
فإن العلاقة تصح على كل مستوى ما عدا الأعلى [1]. يمكنك رؤية ذلك بتعيين المبدّل (commutator) لمؤثري السلّم وقراءة قطره على مستويات نمط واحد. تبني الدالة المساعدة التالية المصفوفة الكثيفة لمؤثر معيَّن لنمط واحد، وهذا معقول فقط للسجلات الصغيرة جدًا:
>>> import numpy as np
>>> from qiskit.quantum_info import SparsePauliOp
>>> from qiskit_bosons.mappers import LevelEncoding, LevelSpec
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> def level_diagonal(operator, max_occupation, encoding):
... """The diagonal of a mapped one-mode operator on the codewords of the levels."""
... spec = LevelSpec(max_occupation, encoding)
... matrix = SparsePauliOp.from_sparse_observable(occupation_encoding(operator, spec)).to_matrix()
... codewords = [spec.encoding.level_to_bits(level) for level in range(max_occupation + 1)]
... return [round(float(matrix[c, c].real), 6) for c in codewords]
>>>
>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dagger = BosonOperator.from_dict({(cre(0),): 1.0})
>>> commutator = (b @ b_dagger) - (b_dagger @ b)
>>> level_diagonal(commutator, 2, "unary_unprojected") # d = 3
[1.0, 1.0, -2.0]
المدخل الأخير هو . ولأن هذا يعني أن امتداد ليس مغلقًا تحت المبدّل، فلا يوجد ترميز يمثّل تماثلًا جبريًا. الترميز يعيّن الحالات، مع المؤثرات التي تعمل داخل الفضاء الجزئي المبتور. والنتيجتان التاليتان مهمتان عمليًا.
يجب أن يحدّ البتر الإشغال الذي يمكن بلوغه. لا يكفي أن تحدّ الإشغال الذي تبدأ به. مقسّم الحزمة (beamsplitter) الذي يؤثر على يملأ و، لذلك ينتج بتر عند واحد حالة خاطئة بصمت:
>>> from scipy.linalg import expm
>>>
>>> beamsplitter = BosonOperator.from_dict({(cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0})
>>>
>>> def stays_in_11(max_occupation):
... """Probability to remain in |1, 1> after a 50:50 beamsplitter."""
... spec = LevelSpec(max_occupation, "gray").resolve(2)
... observable = occupation_encoding(beamsplitter, spec, spec.total_width)
... matrix = SparsePauliOp.from_sparse_observable(observable).to_matrix()
... unitary = expm(-1j * (np.pi / 4) * matrix)
... index = sum(spec.encoding.level_to_bits(1) << offset for offset in spec.offsets)
... return round(float(abs(unitary[index, index]) ** 2), 6)
>>>
>>> stays_in_11(1), stays_in_11(2)
(1.0, 0.0)
مع بتر عند واحد، يكون احتمال البقاء في يساوي واحدًا، لأن الحالتين اللتين ستملؤهما الديناميكيات غير موجودتين. ومع بتر عند اثنين، يتلاشى هذا الاحتمال، وهذا هو تأثير Hong-Ou-Mandel. ولهذا فإن max_occupation وسيط مطلوب وليس له قيمة افتراضية أبدًا: فهو قرار نمذجة ومعامل تقارب يُتوقع منك مسحه.
التعيين لا يتبادل مع الترتيب الطبيعي. المؤثران و متساويان قبل البتر، لكنهما يُعيَّنان إلى مؤثرَي كيوبتات مختلفين:
>>> product = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> ordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0}) + BosonOperator.one()
>>> bool(product.equiv(ordered))
True
>>> level_diagonal(product, 3, "unary_unprojected") # d = 4
[1.0, 2.0, 3.0, 0.0]
>>> level_diagonal(ordered, 3, "unary_unprojected")
[1.0, 2.0, 3.0, 4.0]
يتفق الاثنان على كل مستوى عدا الأعلى. عند التأثير على المستوى الأعلى، لا يجد مكانًا يذهب إليه ويُقصّ السعة، فيكون صفرًا هناك، بينما يعطي القيمة . قرِّر مرة واحدة هل هاميلتونيانك معرَّف بصيغة مرتبة طبيعيًا، ثم عيِّن تلك الصيغة بانتظام.
تساعدك ثلاث طرق على اختيار البتر، وكلها حدود دنيا. يُبلغ المؤثر عن أكبر عدد من مؤثرات الإنشاء يطبّقه على كل نمط ضمن حد واحد، وتقارن المواصفة ذلك ببتراتها:
>>> quartic = BosonOperator.from_dict({(cre(0), cre(0), ann(0), ann(0)): 1.0})
>>> quartic.mode_occupations(), quartic.max_occupation()
([2], 2)
>>> LevelSpec(1, "gray").truncation(quartic) # one level short
[1]
>>> LevelSpec(2, "gray").truncation(quartic)
[0]
المدخل غير الصفري يعني أن البتر أدنى من الحد، والقيمة هي مقدار النقص. هذه الدوال المساعدة لا تعرف شيئًا عن الديناميكيات، لذلك فاجتياز الفحص لا يجعل البتر كافيًا؛ فمقسّم الحزمة أعلاه حده واحد لكنه يحتاج بترًا عند اثنين. ويمكنها أيضًا الإبلاغ عن نقص لحد يساوي صفرًا على المستويات المحتفَظ بها، كما هو الحال مع عند بتر واحد.
وصف البتر باستخدام LevelSpec
تجمع LevelSpec بتر كل نمط مع الترميز، وتشتق منهما عدد الكيوبتات التي يشغلها كل نمط وأين تبدأ كيوبتاته. وكل ما يجب أن يتفق على ذلك (المُعيِّن، وتخطيط المحوِّل، وتحضير الحالة) يحصل على المواصفة نفسها. الترميز وسيط مطلوب، وكذلك البتر: ليس لأي منهما قيمة افتراضية.
البتر إما عدد صحيح واحد تشترك فيه كل الأنماط، أو عدد صحيح لكل نمط:
>>> shared = LevelSpec(2, "gray")
>>> per_mode = LevelSpec([4, 2, 2], "gray")
>>> shared.num_modes is None, per_mode.num_modes
(True, 3)
المواصفة المبنية من عدد صحيح واحد تترك عدد الأنماط مفتوحًا، لذا عليك حسمه قبل طلب العروض. ولا يُعمَّم التسلسل أبدًا، أيًّا كان طوله، وهو يحدد عدد الأنماط:
>>> per_mode.widths, per_mode.offsets, per_mode.total_width
([3, 2, 2], [0, 3, 5], 7)
>>> shared.resolve(3).widths
[2, 2, 2]
>>> try:
... shared.widths
... except ValueError as error:
... print(error)
this spec was built from a single cutoff, so its number of modes is open; call `resolve(num_modes)` first
يشغل النمط الكيوبتات من offsets[m] إلى offsets[m] + widths[m] - 1، لذا يمكن للأنماط ذات البترات المختلفة أن تأخذ أعدادًا مختلفة من الكيوبتات، وتكون الإزاحات هي المجموع التراكمي للعروض. لا تحتاج إلى تمرير عدد الأنماط إلى المُعيِّن. فهو يأخذه من المواصفة، أو من المؤثر عبر min_num_modes() إذا كانت المواصفة ببتر واحد. يمكن أن يكون السجل أعرض مما تحتاجه الأنماط؛ والكيوبتات الفائضة خاملة وتقع في النهاية:
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> number.min_num_modes()
1
>>> occupation_encoding(number, LevelSpec(1, "gray")).num_qubits
1
>>> occupation_encoding(number, LevelSpec(1, "gray"), 4).num_qubits
4
يُرفَض المؤثر الذي يؤثر على نمط يقع بعد آخر نمط في مواصفة لكل نمط:
>>> far = BosonOperator.from_dict({(cre(5), ann(5)): 1.0})
>>> try:
... occupation_encoding(far, LevelSpec([1, 1], "gray"))
... except ValueError as error:
... print(error)
the operator acts on 6 modes, but the spec covers only 2
تنشأ بعض القيود من طريقة عدّ الترميزات للكيوبتات. في الترميز المضغوط، النمط ذو المستوى الواحد (max_occupation = 0) لا يأخذ أي كيوبت، لأن مستواه الوحيد لا يحتاج إلى شيء لتمييزه عن غيره. هذا النمط مسموح ولا يزيح كيوبتات الأنماط التي بعده، لكن المواصفة التي لا يأخذ فيها أي نمط كيوبتًا تُرفَض. تحمل الترميزات الأحادية (unary) 128 مستوى كحد أقصى لكل نمط، وأي بتر أكبر يثير خطأً يقترح ترميزًا مضغوطًا:
>>> LevelSpec([0, 1], "gray").resolve(2).widths
[0, 1]
>>> try:
... LevelSpec(0, "gray")
... except ValueError as error:
... print(str(error).split(":")[0])
the spec would span no qubits
>>> try:
... LevelSpec(128, "unary_projected")
... except ValueError as error:
... print(str(error).split(" (")[0])
max_occupation = 128 is too large for the unary encodings, which hold at most 128 levels per mode
اختيار ترميز
يحدد LevelEncoding أي حالة أساس من كتلة كيوبتات تمثّل كل مستوى. هناك أربعة، ولا يوجد واحد هو الأفضل عمومًا [2]. يتبع عدد الكيوبتات لكل نمط ونمط البتات لكل مستوى من الترميز:
>>> encodings = (
... LevelEncoding.UNARY_PROJECTED,
... LevelEncoding.UNARY_UNPROJECTED,
... LevelEncoding.STANDARD_BINARY,
... LevelEncoding.GRAY,
... )
>>> rows = ["encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns"]
>>> for encoding in encodings:
... widths = [encoding.num_qubits(levels) for levels in (1, 2, 3, 4, 5, 8)]
... patterns = [f"{encoding.level_to_bits(level):05b}" for level in range(5)]
... rows.append(f"{encoding!s:18s} {widths!s:34s} {patterns}")
>>> print("\n".join(rows))
encoding qubits for d = 1, 2, 3, 4, 5, 8 level 0 to 4 as bit patterns
unary_projected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
unary_unprojected [1, 2, 3, 4, 5, 8] ['00001', '00010', '00100', '01000', '10000']
standard_binary [0, 1, 2, 2, 3, 3] ['00000', '00001', '00010', '00011', '00100']
gray [0, 1, 2, 2, 3, 3] ['00000', '00001', '00011', '00010', '00110']
أنماط البتات أعداد صحيحة تمثّل فيها البتة قيمة الكيوبت . تصرف الترميزات الأحادية كيوبتًا واحدًا لكل مستوى وتضبط واحدًا منها. وتصرف الترميزات المضغوطة كيوبتًا وتخزّن رقم المستوى، بالنظام الثنائي أو بشيفرة Gray التي يختلف فيها المستويان المتجاوران في بتة واحدة. ويُقبل اسم الترميز النصي في أي موضع يُقبل فيه الترميز.
المقايضة هي بين الكيوبتات ووزن المؤثر. تحتاج الترميزات المضغوطة كيوبتات أقل بشكل أسّي، لكن مؤثر السلّم يربط مستويات قد تختلف أنماطها في عدة بتات، فتعمل حدوده على كيوبتات أكثر. تتناول الأقسام التالية تكلفة ذلك، بدءًا بالترميزين الأحاديين اللذين لهما تخطيطان متطابقان ويختلفان في المؤثر فقط.
مقارنة الترميزين الأحاديين
في الترميز الأحادي يحرّك مؤثر السلّم إثارة واحدة بين كيوبتين متجاورين. ولكتابة ذلك كمؤثر على النمط كله، عليك أن تختار ما تفعله بالكيوبتات الأخرى. يختار النوعان بشكل مختلف:
-
يضع
unary_projectedمسقِطًا على كل كيوبت آخر في النمط. وهذا يفرض قيد الترميز أحادي الساخن (one-hot) في المؤثر نفسه: يعمل الحد فقط إذا كان بقية النمط فارغًا. -
يترك
unary_unprojectedهذه الكيوبتات كما هي، فيعمل الحد عليها كمؤثر الوحدة.
انظر إلى واحد عند في الحالتين. يُرجع المُعيِّن SparseObservable، وحدوده سلاسل من حدود البتات أحادية الكيوبت. فيما يلي أول أربعة حدود من كل منهما، مع الكيوبتات التي تعمل عليها:
>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(b_dagger, LevelSpec(3, encoding))
... terms = observable.to_sparse_list()
... print(encoding, "->", len(terms), "terms on", observable.num_qubits, "qubits")
... for label, qubits, coefficient in terms[:4]:
... print(f" {label:5s} on qubits {qubits} coefficient {complex(coefficient):.4f}")
unary_projected -> 12 terms on 4 qubits
XX00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
XY00 on qubits [0, 1, 2, 3] coefficient 0.0000-0.2500j
YX00 on qubits [0, 1, 2, 3] coefficient 0.0000+0.2500j
YY00 on qubits [0, 1, 2, 3] coefficient 0.2500+0.0000j
unary_unprojected -> 12 terms on 4 qubits
XX on qubits [0, 1] coefficient 0.2500+0.0000j
XY on qubits [0, 1] coefficient 0.0000-0.2500j
YX on qubits [0, 1] coefficient 0.0000+0.2500j
YY on qubits [0, 1] coefficient 0.2500+0.0000j
لكل منهما اثنا عشر حدًا بالمعاملات نفسها. تحمل الحدود المسقَطة حد بتة 0 على الكيوبتين اللذين لا تذكرهما الحدود غير المسقَطة، فيمس كل حد مسقَط الكيوبتات الأربعة كلها ويمس كل حد غير مسقَط اثنين. عمومًا يمس الحد المسقَط كل كيوبتات النمط البالغ عددها ، لأنه يحمل عاملين، كل منهما أو ، ومسقِطًا على كل كيوبت آخر.
المؤثران متساويان على الحالات التي صُمم الترميز لها، وهي حالات الأساس أحادية الساخن (الكلمات الرمزية)، ويختلفان خارجها. للتحقق، خذ مؤثرًا صغيرًا على نمطين عند وقارن المصفوفتين على الكلمات الرمزية التسع:
>>> hamiltonian = BosonOperator.from_dict({
... (cre(0), ann(1)): 1.0, (cre(1), ann(0)): 1.0,
... (cre(0), ann(0)): 0.5, (cre(1), cre(1), ann(1), ann(1)): 0.25,
... })
>>> matrices = {}
>>> for encoding in ("unary_projected", "unary_unprojected"):
... observable = occupation_encoding(hamiltonian, LevelSpec(2, encoding))
... matrices[encoding] = SparsePauliOp.from_sparse_observable(observable).to_matrix()
>>> unary = LevelEncoding.UNARY_PROJECTED
>>> codewords = sorted(
... unary.level_to_bits(first) | (unary.level_to_bits(second) << 3)
... for first in range(3) for second in range(3)
... )
>>> projected, unprojected = matrices["unary_projected"], matrices["unary_unprojected"]
>>> bool(np.allclose(projected[np.ix_(codewords, codewords)], unprojected[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(projected, unprojected))
False
كلاهما يترك أيضًا فضاء الكلمات الرمزية الجزئي ثابتًا، فتبقى الحالة المحضّرة على كلمة رمزية على كلمات رمزية. لا يهم الاختيار إلا للحالات التي تغادر ذلك الفضاء الجزئي، مثلًا بسبب الضوضاء. هناك يكبح المؤثر المسقَط التطور لأن مسقِطاته تفشل، بينما يستمر غير المسقَط في العمل. ولا يمثّل أي من السلوكين مطلبًا فيزيائيًا. المؤثر غير المسقَط معرَّف فقط على الفضاء الجزئي أحادي الساخن، لذا فاختياره قرار نمذجة وليس تحسينًا مجانيًا.
ما تكلفه المسقِطات هو البوابات. طوِّر الهيرميتي على نمط واحد لزمن قصير، وعُدَّ بوابات الكيوبتين بعد تحويل Qiskit (transpilation):
>>> from qiskit import QuantumCircuit, transpile
>>> from qiskit.circuit.library import PauliEvolutionGate
>>>
>>> def cx_count(observable):
... circuit = QuantumCircuit(observable.num_qubits)
... circuit.append(PauliEvolutionGate(observable, time=0.1), range(observable.num_qubits))
... basis = ["cx", "rz", "sx", "x"]
... return transpile(circuit, basis_gates=basis, optimization_level=1).count_ops().get("cx", 0)
>>>
>>> hermitian = b + b_dagger
>>> levels = (3, 4, 5, 6)
>>> counts = {
... str(encoding): [
... cx_count(occupation_encoding(hermitian, LevelSpec(d - 1, encoding))) for d in levels
... ]
... for encoding in encodings
... }
تعتمد الأعداد الدقيقة على كيفية تركيب Qiskit لبوابة التطور، وهي تتغير بين الإصدارات، لذا ارسمها بيانيًا:
ينمو العدد غير المسقَط خطيًا مع ، لأن كل انتقال من الانتقالات البالغ عددها هو دوران بكيوبتين. أما العدد المسقَط فينمو أسرع بكثير، لأن كل حد يعمل على كل الكيوبتات البالغ عددها . تحتاج الترميزات المضغوطة كيوبتات أقل، لكن وزن حدودها يعتمد على كيفية اختلاف أنماط بتات المستويات المتجاورة: شيفرة Gray، التي يختلفان فيها في بتة واحدة، هي الأرخص بين الترميزين المضغوطين، ويقفز الثنائي القياسي عندما يقلب الحمل عدة بتات دفعة واحدة (). وهل يتفوق الترميز المضغوط أيضًا على الأحادي غير المسقَط يعتمد على وعلى كيفية تركيب Qiskit للتطور، لذا اقرأ الاتجاه لا الترتيب عند حجم واحد.
تهم المسقِطات أيضًا عند توسيع مؤثر معيَّن إلى مؤثرات Pauli، وهو ما تفعله from_sparse_observable() باستبدال كل مسقِط بمجموع مؤثري Pauli. الحد الذي فيه من المسقِطات يصبح من حدود Pauli. إليك واحدًا عند صغير، وهو الحجم الوحيد الذي ينبغي أن تجرّب هذا عنده:
>>> rows = [f"{'d':>2s} {'stored (both)':>14s} {'expanded, projected':>20s} {'expanded, unprojected':>22s}"]
>>> for levels in (3, 4, 5):
... stored = len(occupation_encoding(b_dagger, LevelSpec(levels - 1, "unary_projected")))
... expanded = [
... len(SparsePauliOp.from_sparse_observable(
... occupation_encoding(b_dagger, LevelSpec(levels - 1, encoding))).simplify())
... for encoding in ("unary_projected", "unary_unprojected")
... ]
... rows.append(f"{levels:2d} {stored:14d} {expanded[0]:20d} {expanded[1]:22d}")
>>> print("\n".join(rows))
d stored (both) expanded, projected expanded, unprojected
3 8 16 8
4 12 48 12
5 16 128 16
يخزّن النوعان العدد نفسه من الحدود، لكن توسيع النوع المسقَط ينمو أسّيًا في . وفي أي شيء أكبر من مثال لعبة، اقرأ حدود SparseObservable مباشرة، كما يفعل العرض أعلاه، بدلًا من تحويله.
لا تستطيع الترميزات المضغوطة التخلي عن مسقِطاتها. هناك تكون البتات الصفرية جزءًا من رقم المستوى، فإزالة تغيّر المستويات التي يربطها الحد، ولا يعود المؤثر نفسه على الكلمات الرمزية. ولهذا فإن الترميز الأحادي وحده له نوعان.
حاصل ضرب السلالم يُبتر أولًا
يظهر خيار تصميمي آخر في تكلفة الحدود القطرية. لا يعيّن المُعيِّن كل مؤثر سلّم ثم يضرب النتائج. بل يجمع مؤثرات السلّم التي يطبّقها الحد على نمط واحد، ويضربها كمصفوفة في أساس Fock، ويبتر ذلك الجداء مرة واحدة، ثم يرمّز النتيجة [2]. لذلك يبقى جداء مثل مؤثر العدد قطريًا:
>>> number_observable = occupation_encoding(number, LevelSpec(3, "unary_unprojected"))
>>> [(label, qubits) for label, qubits, _ in number_observable.to_sparse_list()]
[('1', [1]), ('1', [2]), ('1', [3])]
>>> cx_count(number_observable)
0
حدوده هي المسقِطات على كيوبت كل مستوى فوق الصفر، موزونة برقم المستوى (المستوى صفر وزنه صفر فلا حد له). والتطور الزمني له مجموعة من الدورانات أحادية الكيوبت ولا يحتاج إلى بوابة ثنائية الكيوبت. أما ضرب مؤثري السلّم المعيَّنين بدلًا من ذلك فيعطي المؤثر نفسه على الكلمات الرمزية، لكنه مختلف خارجها، مع سلاسل غير قطرية تكلّف بوابات:
>>> unprojected = LevelSpec(3, "unary_unprojected")
>>> as_pauli = lambda operator: SparsePauliOp.from_sparse_observable(
... occupation_encoding(operator, unprojected, 4)).simplify()
>>> composed = as_pauli(b_dagger).dot(as_pauli(b)).simplify()
>>> direct = as_pauli(number)
>>> len(direct), len(composed)
(4, 16)
>>> cx_count(direct)
0
>>> bool(cx_count(composed) > 0)
True
>>> codewords = [LevelEncoding.UNARY_UNPROJECTED.level_to_bits(level) for level in range(4)]
>>> bool(np.allclose(direct.to_matrix()[np.ix_(codewords, codewords)],
... composed.to_matrix()[np.ix_(codewords, codewords)]))
True
>>> bool(np.allclose(direct.to_matrix(), composed.to_matrix()))
False
الحدود القطرية، مثل التفاعل الموضعي في نموذج Bose-Hubbard، هي هذه الجداءات، وهنا يهم هذا الخيار.
كتابة مُعيِّن مخصص
إذا أردت ترميزًا لا توفره المكتبة، فابنه من صورة مؤثر سلّم واحد. تأخذ map_boson_action_generators() دالة تعيّن BosonAction واحدًا، ودالة تُرجع مؤثر الوحدة، وتبني منهما مُعيِّن المؤثرات الكاملة. فهي تضرب صور أفعال كل حد بالترتيب، وتضرب بالمعامل، وتجمع الحدود. يمكن أن يكون الناتج أي نوع يدعم الضرب بعدد قياسي والتركيب.
كمثال، خذ الترميز عند الذي يخزّن نمطًا في كيوبت واحد. مؤثر الإنشاء هو ، أي بتقليد Qiskit، ومؤثر الإفناء هو مرافقه:
>>> from qiskit_bosons.mappers import map_boson_action_generators
>>> from qiskit_bosons.operators import BosonAction
>>>
>>> num_qubits = 2
>>>
>>> def map_action(action: BosonAction) -> SparsePauliOp:
... is_creation, mode = action
... sign = -1j if is_creation else 1j
... return SparsePauliOp.from_sparse_list(
... [("X", [mode], 0.5), ("Y", [mode], 0.5 * sign)], num_qubits=num_qubits
... )
>>>
>>> def identity() -> SparsePauliOp:
... return SparsePauliOp.from_sparse_list([("", [], 1.0)], num_qubits)
>>>
>>> model = BosonOperator.from_dict({
... (cre(0), ann(1)): -1.0, (cre(1), ann(0)): -1.0, (ann(0), cre(0)): 0.7, (cre(1), ann(1)): 0.3,
... })
>>> custom = map_boson_action_generators(model, map_action, identity)
>>> library = SparsePauliOp.from_sparse_observable(occupation_encoding(model, LevelSpec(1, "gray")))
>>> bool(np.allclose(custom.to_matrix(), library.to_matrix()))
True
يضرب هذا المُعيِّن المكتوب يدويًا مؤثري السلّم المعيَّنين، ويتفق مع المُعيِّن المكتبي هنا لأن كل نمط له مستويان ويحمل كيوبت النمط كله. عند أكبر، كما أظهر القسم السابق، فإن تركيب السلالم المعيَّنة وبتر الجداء أولًا ليسا المؤثر نفسه خارج الكلمات الرمزية.
لاستخدام مُعيِّن كهذا في دائرة، غلّفه في دالة بالتوقيع (operator, num_qubits) -> SparseObservable ومرّره إلى MapperFnEvolutionSynthesis، التي تعيّن به مؤثر كل بوابة Evolution. يُوصف مسار المحوِّل في تحويل الدوائر البوزونية (Transpile).
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_bosons.transpiler.passes import MapperFnEvolutionSynthesis
>>>
>>> def mapper_fn(operator, num_qubits):
... return SparseObservable.from_sparse_pauli_op(
... map_boson_action_generators(operator, map_action, identity)
... )
>>>
>>> synthesis = MapperFnEvolutionSynthesis(mapper_fn)
هناك تفصيلان يستحقان المعرفة. أولًا، يجب أن يكون لمؤثر الكيوبتات معاملات حقيقية، لأن PauliEvolutionGate ترفض الجزء التخيلي. المؤثر الهيرميتي يُعيَّن إلى مؤثر حقيقي، لكن فقط بعد دمج الحدود التي تتلاشى. هذا المُعيِّن المكتوب يدويًا لا يدمجها، فيترك حد القفز ومرافقه معاملات تخيلية إلى أن تبسّط النتيجة. يدمج المُعيِّن المكتبي نتيجته بنفسه؛ أما المخصص فعليه أن يفعل ذلك، مثلًا بتغليفه في simplify():
>>> hopping = BosonOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> hermitian_hopping = hopping + hopping.adjoint()
>>> raw = mapper_fn(hermitian_hopping, 2)
>>> bool(np.allclose(np.asarray(raw.coeffs).imag, 0))
False
>>> merged = raw.simplify()
>>> bool(np.allclose(np.asarray(merged.coeffs).imag, 0))
True
>>> sorted(label for label, _, _ in merged.to_sparse_list())
['XX', 'YY']
ثانيًا، تركّب صيغة المنتج الحدود بالترتيب الذي تستلمه به، ولا يُضمن أن يكون الترتيب الذي ينتجه المُعيِّن مستقرًا. تغلّف الدالتان المساعدتان simplify() وgroup_wise() دالة مُعيِّن للتحكم فيه: simplify تدمج السلاسل المكررة وترتّب الحدود بترتيب قانوني، بينما group_wise تعيّن مجموعة واحدة من المؤثر في كل مرة بحيث تبقى حدود المجموعة متجاورة. لا تجمع بين الاثنتين، لأن الفرز يُلغي التجميع. يوضح دليل التجميع كيف تُسند المجموعات، ويوضح دليل التركيب كيف يؤثر الترتيب في الدائرة.
الخطوات التالية
-
اتبع محاكاة ديناميكيات Bose-Hubbard مع اختيار الترميز للاطلاع على محاكاة كاملة تمسح البتر وتقارن الترميزات بالديناميكيات الدقيقة.
-
راجع تحويل الدوائر البوزونية (Transpile) لمعرفة كيف تُطبَّق
LevelSpecنفسها على دائرة بوزونية. -
اقرأ مبادئ تصميم تمثيلات المؤثرات لمعرفة كيف يخزّن
BosonOperatorحدوده ويرتّبها، بما في ذلك قاعدة الاختزال وراء مثال الترتيب الطبيعي أعلاه. -
اقرأ تدوير الأنماط البوزونية للاطلاع على البوابة التي تنقل البوزونات بين الأنماط، وهو الموضع الذي يكون فيه تحذير الإشغال الممكن بلوغه أكثر أهمية.
-
راجع مرجع واجهة البرمجة (API) لـ
qiskit_bosons.mappersوqiskit_bosons.mappers.library.
[1]
R. Somma, G. Ortiz, E. Knill, and J. Gubernatis, Quantum Simulations of Physics Problems (2003), arXiv:quant-ph/0304063, Sec. 4, Eq. 22.
N. P. D. Sawaya, T. Menke, T. H. Kyaw, S. Johri, A. Aspuru-Guzik, and G. G. Guerreschi, Resource-efficient digital quantum simulation of d-level systems for photonic, vibrational, and spin-s Hamiltonians, npj Quantum Information 6, 49 (2020), arXiv:1909.12847.