تدوير الأنماط البوزونية
يخلط ModeRotation الأنماط البوزونية بمصفوفة وحدوية ، كما تفعل شبكة من مقسّمات الحزمة ومزيحات الطور. ويسمى سلبيًا لأنه يحفظ العدد الكلي للبوزونات: فهو ينقل البوزونات بين الأنماط ولا ينشئ أيًّا منها ولا يفنيه. (الانضغاط والإزاحة هما العمليتان النشطتان، ولا يستطيع تدوير الأنماط التعبير عنهما.) ويعيّن مؤثرات الإنشاء كما يلي
وهو على فضاء Fock الكامل المؤثر الوحدوي متعدد الأجسام
هذه هي البوابة وراء مقياس التداخل البصري الخطي: أرسل حالات Fock، وأدِر الأنماط، واقرأ كيف تُعاد توزيع البوزونات. وكما هو الحال مع كل بوابة في BosonicCircuit (راجع العمل مع الدوائر البوزونية)، فهي لا تحمل بترًا ولا ترميزًا. كلاهما يدخلان وقت التحويل (transpile)، ويبيّن هذا الدليل لماذا يحتاج البتر إلى عناية خاصة في حالة التدوير.
بناء تدوير
يأخذ المُنشئ المصفوفة الوحدوية . تعمل البوابة على من الأنماط. لا يتحقق الصنف من أن المصفوفة وحدوية، فهذه مسؤوليتك.
>>> import numpy as np
>>> from qiskit_bosons.circuit.library import ModeRotation
>>>
>>> unitary = np.array([[1, -1], [1, 1]]) / np.sqrt(2)
>>> rotation = ModeRotation(unitary)
>>> rotation.num_modes # the gate acts on two modes
2
للتدويرات الشائعة مُنشئات مخصصة. يأخذ ModeRotation.beamsplitter() زاوية خلط وطورًا اختياريًا ، وتعطي مقسّم الحزمة المتوازن 50:50. ويأخذ ModeRotation.phase_shift() طورًا واحدًا لكل نمط.
>>> beamsplitter = ModeRotation.beamsplitter(np.pi / 4)
>>> np.round(beamsplitter.rotation_unitary.real, 4)
array([[ 0.7071, -0.7071],
[ 0.7071, 0.7071]])
>>> bool(np.allclose(beamsplitter.rotation_unitary, unitary))
True
>>> shift = ModeRotation.phase_shift([0.0, np.pi / 2])
>>> np.round(np.diag(shift.rotation_unitary), 4)
array([1.+0.j, 0.+1.j])
يمكنك أيضًا أن تبدأ من المولِّد ضد الهيرميتي وتترك البوابة تأخذ أسّه، وهذا يعطي التدوير . ويُستعاد المولِّد بـ generator().
>>> generator = np.array([[0, -0.3], [0.3, 0]], dtype=complex)
>>> small = ModeRotation.from_generator(generator)
>>> np.round(small.rotation_unitary.real, 4)
array([[ 0.9553, -0.2955],
[ 0.2955, 0.9553]])
>>> bool(np.allclose(small.generator(), generator))
True
الجسر إلى جبر المؤثرات هو to_operator()، الذي يُرجع المولِّد الهيرميتي كـ BosonOperator، بحيث تكون البوابة . وبالنسبة لمقسّم الحزمة 50:50، يكون ذلك زوجًا من حدود القفز بمعاملات تخيلية متعاكسة. المؤثر هيرميتي، وهذا ما تحتاجه بوابة Evolution، وهو يحفظ عدد الجسيمات.
>>> operator = beamsplitter.to_operator()
>>> # The stored order of the terms is not specified, so sort them before printing.
>>> sorted((term[0], round(term[1].imag, 4)) for term in operator.iter_terms())
[([(True, 0), (False, 1)], -0.7854), ([(True, 1), (False, 0)], 0.7854)]
>>> operator.is_hermitian()
True
>>> operator.conserves_particle_number()
True
يُقرأ كل حد (is_creation, mode) من اليسار إلى اليمين، لذا فالثاني هو .
التأثير على حالات Fock
ينقل التدوير البوزونات بين الأنماط، لذا يسهل رؤية تأثيره على حالات Fock. تأثير Hong-Ou-Mandel هو المثال المعياري. أرسل بوزونًا واحدًا إلى كل نمط دخل في مقسّم حزمة متوازن. المساران اللذان يتركان البوزونين في نمطي خرج مختلفين يتداخلان هدّامًا، فتختفي الحالة ويغادر البوزونان دائمًا معًا، في أو ، لكل منهما احتمال النصف.
ابنِ الدائرة على الأنماط البوزونية وأنزلها بمدير التمريرات المسبق الإعداد. تحدد LevelSpec البتر والترميز عند هذه النقطة. بتر max_occupation=2 يحتفظ بالمستويات 0 و1 و2 لكل نمط.
>>> from qiskit_bosons.circuit import BosonicCircuit
>>> from qiskit_bosons.circuit.library import InitializeModes
>>> from qiskit_bosons.mappers import LevelEncoding, LevelSpec
>>> from qiskit_bosons.transpiler.presets import generate_preset_occupation_pass_manager
>>>
>>> circuit = BosonicCircuit(2)
>>> circuit.append(InitializeModes([1, 1]), circuit.modes)
>>> circuit.append(ModeRotation.beamsplitter(np.pi / 4), circuit.modes)
يُرجع المسار دائرة كيوبتات عادية. اقرأ احتمالات النتائج من الحالة النهائية، هنا باستخدام Statevector من Qiskit، وهو دقيق عند هذا الحجم. يعمل أي محاكي يُرجع السعات أو العدّادات. حالة Fock هي حالة أساس حسابية واحدة، ويُعطى فهرسها بأنماط البتات في LevelEncoding.level_to_bits()؛ وتتبع كيوبتات النمط الثاني كيوبتات النمط الأول.
>>> from qiskit.quantum_info import Statevector
>>> from qiskit.transpiler import PassManager
>>>
>>> def fock_probabilities(circuit, max_occupation):
... """Maps each pair of occupations to the probability of that outcome."""
... encoding = LevelEncoding.UNARY_UNPROJECTED
... spec = LevelSpec(max_occupation, encoding)
... pass_manager = generate_preset_occupation_pass_manager(spec)
... pass_manager.qubit = PassManager() # keep the evolutions as the pipeline emitted them
... qubit_circuit = pass_manager.run(circuit)
... state = Statevector(qubit_circuit)
... width = encoding.num_qubits(max_occupation + 1)
... probabilities = {}
... for n0 in range(max_occupation + 1):
... for n1 in range(max_occupation + 1):
... index = encoding.level_to_bits(n0) | (encoding.level_to_bits(n1) << width)
... probability = round(float(abs(state.data[index]) ** 2), 4)
... if probability > 0:
... probabilities[(n0, n1)] = probability
... return probabilities
>>>
>>> fock_probabilities(circuit, max_occupation=2)
{(0, 2): 0.5, (2, 0): 0.5}
النتيجة احتمالها صفر، وتتقاسم النتيجتان المتكتلتان الاحتمال بالتساوي.
اختيار البتر
النتيجة السابقة تحتاج max_occupation=2. شغّل الدائرة نفسها ببتر قدره 1. هذا كافٍ لاحتواء حالة الدخل ، لكنه لا يكفي للحالة التي ينتجها التدوير منها، والتي لها وزن على و:
>>> fock_probabilities(circuit, max_occupation=1)
{(1, 1): 1.0}
تعمل الدائرة دون أي خطأ، والجواب خاطئ: تبقى الحالة عند باحتمال واحد. يخلط التدوير قطاعات الإشغال داخل النمط. يمكن أن ينتهي بوزونان في نمط الخرج نفسه، فيكون للحالة الدقيقة وزن على الإشغال 2 في نمط بدأ ببوزون واحد. ومع بتر قدره 1 لا يوجد ذلك المستوى، ولا يستطيع المولِّد المبتور نقل الحالة إليه.
لذلك فإن حدًّا لإشغال الحالة الابتدائية لا يكفي. يجب أن يغطي البتر العدد الكلي للبوزونات الذي يمكن أن يصل إلى أي نمط منفرد في التدوير. وبالنسبة لـ بوزونًا في المجموع، فالاختيار الآمن هو بتر قدره .
ينطبق المنطق نفسه على كل نمط على حدة، لذا فإن مزيجًا من بترات مختلفة لكل نمط (راجع وصف البتر باستخدام LevelSpec) يجعل تدويرًا سهل التعيين بشكل خاطئ. النمط الذي يمكن أن يستقبل بوزونات يحتاج بترًا يغطي ما يمكن أن يستقبله، لا ما يحمله ابتداءً فقط. يقارن الفحص LevelSpec.truncation() مواصفة بالحد الأدنى لإشغال المؤثر. ولا يستطيع رصد الحالة أعلاه. فكل حد من مولِّد مقسّم الحزمة يحمل مؤثر إنشاء واحدًا، لذا فالحد واحد ولا يُبلغ الفحص عن أي نقص عند بتر قدره 1، مع أن النتيجة هناك خاطئة:
>>> LevelSpec(1, "unary_unprojected").truncation(beamsplitter.to_operator())
[0, 0]
وهناك تحذير إضافي ينشأ من الطريقة التي يبني بها المحوِّل التدوير (القسم التالي). فهو يطبّق التدوير كسلسلة من عوامل ثنائية النمط، وتحذّر وثائق الإضافة من أن عاملًا واحدًا قد يملأ مستوى لا يبلغه التدوير الكامل أبدًا. عندئذ يكون التسلسل المبتور والتطور المنفرد المبتور تقريبين مختلفين للوحدوي نفسه. لم يُظهر بحث في 1,080 تدويرًا عشوائيًا ثلاثي الأنماط، مع بوزون واحد في كل من نمطي دخل ودون إشغال مزدوج في الخرج، حالة كهذه، لذا عامل هذا على أنه احتياط وليس فشلًا شائعًا. اجعل البتر يتقارب كما تفعل مع أي دائرة أخرى، بزيادته إلى أن تتوقف النتيجة عن التغير.
كيف يركّب المحوِّل تدويرًا
تفكّك مرحلة optimization من المسار المسبق الإعداد ModeRotation أولًا في الفضاء البوزوني، بتمريرة B2BSynthesis وطريقة BosonicClementsDecomposition (المسجّلة باسم ModeRotation.Clements). هذا اختيار من الإعداد المسبق وليس شرطًا من الإطار: فقسم لاحق ينزل البوابة نفسها دون أي تفكيك. لا يأخذ التفكيك أسّ المولِّد الكامل في خطوة واحدة. بل يحلّل الوحدوي أولًا إلى ما لا يزيد على من التدويرات بين الأنماط المتجاورة إضافةً إلى من أطوار النمط المنفرد، متبعًا الشبكة المستطيلة لـ Clements وآخرين [1]. يصبح كل عامل بوابة Evolution ذرية، تعيّنها مرحلة البوزون إلى الكيوبت كأي بوابة أخرى.
التحليل إلى عوامل نفسه تنفّذه givens_decomposition()، التي يستدعيها التحليل. التطبيق الذي يحوّل مصفوفة وحدوية إلى الدوران متعدد الجسيمات هو تماثل زمري (group homomorphism)، لذلك فإن حاصل ضرب دورانات العوامل يساوي الدوران الكامل تمامًا على الفضاء غير المقتطع. وبالتالي لا تضيف الشبكة (mesh) أي خطأ من عندها. فائدتها هي المحلية. كل تطور ناتج يشمل فقط حدود باولي الخاصة بالنمطين اللذين يخلطهما (أو نمط واحد في حالة الطور)، بدلًا من حدود تمتد عبر كل الأنماط.
حوّل دورانًا عشوائيًا لثلاثة أنماط وافحص ما أخرجه خط المعالجة. المصفوفة الوحدوية العامة تحتاج إلى الحد الأقصى من ثلاثة دورانات، بالإضافة إلى ثلاثة أطوار، وكل عامل هو PauliEvolutionGate مستقل. كلها معرّفة على السجل بأكمله، لكن المؤثرات بداخلها تعمل على عدد قليل من الكيوبتات فقط:
>>> from qiskit.quantum_info import random_unitary
>>>
>>> random = np.asarray(random_unitary(3, seed=7).data)
>>> spec = LevelSpec(2, "unary_unprojected")
>>> rotation_circuit = BosonicCircuit(3)
>>> rotation_circuit.append(InitializeModes([1, 0, 0]), rotation_circuit.modes)
>>> rotation_circuit.append(ModeRotation(random), rotation_circuit.modes)
>>> pass_manager = generate_preset_occupation_pass_manager(spec)
>>> pass_manager.qubit = PassManager()
>>> qubit_circuit = pass_manager.run(rotation_circuit)
>>> dict(qubit_circuit.count_ops())
{'PauliEvolution': 6, 'x': 3}
>>> for instruction in qubit_circuit.data:
... if instruction.operation.name == "PauliEvolution":
... terms = instruction.operation.operator.to_sparse_list()
... print(len(terms), sorted({qubit for _, indices, _ in terms for qubit in indices}))
64 [0, 1, 2, 3, 4, 5]
64 [3, 4, 5, 6, 7, 8]
64 [0, 1, 2, 3, 4, 5]
2 [1, 2]
2 [4, 5]
2 [7, 8]
مع ثلاثة مستويات لكل نمط، يشغل كل نمط ثلاثة كيوبتات. مقسّم الحزمة (beamsplitter) بين نمطين يمس الكيوبتات الستة الخاصة بهذين النمطين، والطور يمس كيوبتات نمط واحد.
العوامل تكون دقيقة فقط عندما يُركَّب كل تطور بشكل جيد. حدود مولّد مقسّم الحزمة لا تتبادلية، لذلك تترك صيغة الضرب الافتراضية ذات الخطوة الواحدة خطأً في كل عامل. مرّر صيغة ضرب بتكرارات أكثر إلى الإضافة Evolution التي تحوّل العوامل لتقليل الخطأ:
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.synthesis import LieTrotter
>>> from qiskit_bosons.circuit.library.synthesis import BosonicClementsDecomposition
>>> from qiskit_bosons.mappers.library import occupation_encoding
>>> from qiskit_bosons.transpiler import (
... BosonicCircuitToDAG,
... BosonicPassManager,
... QuantumDAGToCircuit,
... )
>>> from qiskit_bosons.transpiler.passes import (
... B2BSynthesis,
... B2QSynthesis,
... MapperFnEvolutionSynthesis,
... OccupationB2QLayout,
... OccupationInitializeModesSynthesis,
... )
>>>
>>> def with_product_formula(product_formula):
... synthesis = B2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... lambda operator, num_qubits: occupation_encoding(operator, spec, num_qubits),
... product_formula,
... )
... synthesis.methods["InitializeModes"] = OccupationInitializeModesSynthesis(spec)
... return MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... optimization=BosonicPassManager(
... B2BSynthesis({"ModeRotation": BosonicClementsDecomposition()})
... ),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
بوزون واحد يدخل النمط 0 يخرج في النمط باحتمال . قارن الدارة المركّبة بهذه النتيجة الدقيقة لأعداد تكرار متزايدة. حلّل الدارة أولًا حتى تُطبَّق صيغة الضرب؛ فبدون ذلك، يطبّق محاكٍ دقيق مثل Statevector البوابة كأُسّها الكامل ويخفي الخطأ.
>>> encoding = LevelEncoding.UNARY_UNPROJECTED
>>> width = encoding.num_qubits(3)
>>> exact = np.abs(random[:, 0]) ** 2
>>>
>>> def one_boson_probabilities(qubit_circuit):
... """Probability that the single boson sits in mode j, for each j."""
... state = Statevector(qubit_circuit)
... probabilities = []
... for j in range(3):
... occupations = [1 if mode == j else 0 for mode in range(3)]
... index = sum(
... encoding.level_to_bits(n) << (width * mode) for mode, n in enumerate(occupations)
... )
... probabilities.append(abs(state.data[index]) ** 2)
... return np.array(probabilities)
>>>
>>> for repetitions in (1, 4, 16):
... lowered = with_product_formula(LieTrotter(reps=repetitions)).run(rotation_circuit)
... error = np.max(np.abs(one_boson_probabilities(lowered.decompose(reps=2)) - exact))
... print(f"{repetitions:2d} repetitions: error {error:.1e}")
1 repetitions: error 1.2e-03
4 repetitions: error 7.6e-05
16 repetitions: error 4.7e-06
ينخفض الخطأ بعامل أربعة تقريبًا مع كل زيادة بأربعة أضعاف في التكرارات، كما هو متوقع لصيغة من الرتبة الأولى. وتزداد الكلفة بالتناسب، لأن كل عامل يتكرر.
تحويل دوران إلى مستوى أدنى دون تحليله
التحليل في الفضاء البوزوني هو طريقة واحدة لتحويل الدوران إلى مستوى أدنى. (المكتبة لا تتضمن طريقة أخرى، لكن مرحلة التحويل من بوزون إلى كيوبت لا تعتمد عليها.) تبحث B2QSynthesis عن إضافة باسم البوابة، وModeRotation التي تُركت كاملة تصل إليها مثل أي بوابة أخرى. إضافة لها تحوّل مولّد ModeRotation.to_operator() دفعة واحدة وتُخرج PauliEvolutionGate واحدة عند زمن الوحدة. هذا هو كل شيء:
>>> from qiskit.circuit.library import PauliEvolutionGate
>>> from qiskit_bosons.transpiler.passes.utils import map_node_single_register
>>>
>>> class DirectModeRotationSynthesis:
... """Maps the generator of the whole rotation, with no bosonic decomposition."""
...
... def __init__(self, mapper_fn):
... self.mapper_fn = mapper_fn
...
... def run(self, in_node, out_dag, *, b2q_layout):
... modes, qreg = map_node_single_register(in_node, b2q_layout)
... generator = in_node.op.to_operator().relabel_modes(list(modes))
... observable = self.mapper_fn(generator, len(qreg))
... out_dag.apply_operation_back(PauliEvolutionGate(observable, time=1.0), tuple(qreg))
سجّلها باسم البوابة في مدير تمريرات (pass manager) لا يحلّلها:
>>> 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)
>>> synthesis.methods["ModeRotation"] = DirectModeRotationSynthesis(mapper_fn)
>>> direct = MultiStagePassManager(
... input=BosonicCircuitToDAG(),
... layout=OccupationB2QLayout(spec),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>> direct_circuit = direct.run(rotation_circuit)
>>> dict(direct_circuit.count_ops())
{'x': 3, 'PauliEvolution': 1}
>>> terms = direct_circuit.data[-1].operation.operator.to_sparse_list()
>>> len(terms), sorted({qubit for _, indices, _ in terms for qubit in indices})
(198, [0, 1, 2, 3, 4, 5, 6, 7, 8])
الدوران الآن تطور واحد على جميع الكيوبتات التسعة، بينما الشبكة لديها ستة، ولا يمتد أي منها على أكثر من ستة. التقريبان ليسا متماثلين. الاقتطاع يُطبَّق هنا مرة واحدة على مولّد الدوران بأكمله، وعلى كل عامل بشكل منفصل في الشبكة (ومن هنا التحذير بشأن الحد الأقصى أعلاه). المسار المباشر يساوي occupation_encoding() للمؤثر عند أي حد أقصى، لذا هو المرجع الذي تُقارن به الشبكة. وثمنه هو المحلية: حدوده تمتد عبر كل الأنماط، ولدى صيغة الضرب حدود أكثر لتقسيمها.
لا أحدهما أفضل بشكل عام، وهذا هو سبب إبقاء الاختيار مفتوحًا. اكتب إضافة مثل هذه للمقارنة بين طرق التحويل، أو لاستخدام دوران خاص بترميز معيّن، أو لتسليم البوابة إلى شيء يفهمها بشكل أصلي.
الخطوات التالية
-
تحويل الدارات البوزونية يشرح المراحل التي يشغّلها مدير التمريرات الجاهز، وكيفية استبدال الإضافة التي تعالج بوابة ما.
-
تحويل تمثيلات المؤثرات يغطي الاقتطاع والترميزات، و
LevelSpecالتي تحدد تخطيط كل نمط. -
العمل مع الدارات البوزونية يوضح كيفية تجميع الدارات البوزونية ودمجها.
-
توثيق واجهة البرمجة يسرد
qiskit_bosons.circuit.libraryللبوابات، وqiskit_bosons.linalgللتحليل، وqiskit_bosons.transpiler.passesلإضافات التركيب.
[1]
W. R. Clements, P. C. Humphreys, B. J. Metcalf, W. S. Kolthammer, and I. A. Walmsley, Optimal design for universal multiport interferometers, Optica 3, 1460 (2016), arXiv:1603.08788.