محاكاة ديناميكيات Fermi-Hubbard ثنائية الأبعاد بمجموعات التدفق
المفاهيم في هذا الدليل متاحة حالياً فقط في واجهة Python البرمجية. ستتوفر وظائف مكافئة في واجهة C في إصدار قادم.
اقرأ دليل مجموعات التدفق أحادي البعد أولاً. هذا الدليل هو تتمته ثنائية الأبعاد ولا يكرر المعلومات المطورة هناك: مؤثرات النقل والرأس، وما هي مجموعة التدفق، وكيف تنجو groups إلى الدائرة، وكيف يُربط ترميز مخصص بالمترجم (Transpiler) عبر MapperFnEvolutionSynthesis.
انظر دليل التحويل لمراحل المترجم (Transpiler) المشار إليها طوال الدليل، ودليل المحولات للوصفة العامة لكتابة محول مخصص.
دليل البعد الواحد انتهى بالإشارة إلى أن الترميزات المحلية مطلوبة عند العمل في بعدين. هذا الدليل يخطو هذه الخطوة، لـ نموذج Fermi-Hubbard ثنائي الأبعاد على شبكة مربعة، باستخدام ترميز Verstraete-Cirac (VC) [2].
هناك ثلاثة فروق عن مسألة البعد الواحد، وهي أساس هذا العمل:
-
مجموعات التدفق تتضاعف. السلسلة فيها اتجاهان للسهم؛ والشبكة المربعة فيها أربعة (شرق، غرب، شمال، جنوب)، وكل منها اتحاد مسارات موجهة منفصلة الرؤوس (واحد لكل صف أو عمود) بدل مسار واحد.
-
الترميز يكسب ancilla لكل موقع، وليس واحداً للنظام كله، فيعطي
نسبة كيوبت-إلى-نمط قدرها . مؤثرات النقل تصير Paulis بوزن 3 (أفقية) ووزن 4 (عمودية)، وكل مجموعة من مجموعات التدفق الأربع تُحوَّل إلى شكل Pauli خاص بها، فكل منها يحتاج Clifford مقطِّراً خاصاً به.
-
الهاملتونية المرمزة أمينة فقط على فضاء جزئي: الفضاء الذاتي المشترك
لقيود plaquette اللي يحملها كل ترميز محلي ثنائي الأبعاد [2] [3]. في البعد الواحد كان الترميز إيزومترية عادية وكان التحقق فحصاً للطيف. هنا، المقارنة لازم تُسقَط، كما توضح الخطوة 9.
المكسب له نفس الشكل كما في البعد الواحد: العمق ثنائي الكيوبت لخطوة Trotter واحدة يصير ثابتاً بالنسبة لحجم الشبكة، عند ( للقفز، مطابقاً للمرجع [1]، زائد للتفاعل)، بينما Trotterization حداً بحد لنفس الهاملتونية المرمزة تنمو خطياً. عدد البوابات ما زال ينمو مع عدد الروابط، طبعاً؛ اللي يصير مستقلاً عن الحجم هو عدد الطبقات اللي تشغلها هذه البوابات. الأعماق في كل مكان تُحسب على سجل مجرد متصل بالكامل؛ أما تحويل الترميز إلى خريطة اقتران محدودة لجهاز فهو مسألة مستقلة، نناقشها في النهاية.
الشبكة واتجاهاتها الأربعة
التغيير الأول يظهر على بيان التفاعل الموجه. المواقع مرقمة بترتيب الصفوف، site(r, c) = r * cols + c، وكل رابط يحمل الاتجاهين، كما في السلسلة بالبعد الواحد، لكن الآن الروابط تمتد على محورين:

قاعدة التبادل هي نفسها في البعد الواحد، بدون تغيير: مؤثرا نقل يتشاركان موقعاً يتبادلان عندما تمر الأسهم عبره ويتضادان عندما تتصادم. لكن الموقع الآن له حتى أربعة روابط واردة بدل اثنين، فالأسهم عبره قد تتصادم بطرق أكثر، ومجموعة أسهم لا تتصادم أبداً لم تعد مساراً واحداً. قراءة الأسهم الأفقية من اليسار إلى اليمين تعطي مجموعة كهذه، والأسهم العمودية من الأعلى إلى الأسفل تعطي أخرى، وعكس كل منهما يعطي اثنتين إضافيتين. هذه هي مجموعات التدفق شرق وغرب وشمال وجنوب التي ينتظم حولها كل ما يلي.
ترميز Verstraete-Cirac
Verstraete-Cirac (VC) يقرن كل نمط فرميوني بكيوبت مساعد خاص به. مع موقع في الشبكة، يعيش النمط على الكيوبت وancilla الخاص به على الكيوبت ، فيحمل السجل كيوبت. مؤثرات الرأس تبقى بأبسط ما يمكن،
وهذا ما يجعل الإشغالات مقروءة مباشرة لاحقاً. مؤثرات النقل تعتمد على اتجاه الحافة. لحافة أفقية حيث الموقع الغربي، ولحافة عمودية حيث الموقع الشمالي،
“مثلث” بوزن 3 و“مربع” بوزن 4. تحديد كل سهم مهم: مع كون الموقع الغربي، يشير شرقاً، لكن مع كون الموقع الشمالي، يشير جنوباً. الاتجاهات المعكوسة تنتج من المطابقة ، التي تحول مثلث XXZ إلى YYZ، ومربع XYXY إلى YXXY. هذه التسميات الأربع المتمايزة هي مجموعات التدفق الأربع، وباقي هذا الدليل يتتبعها.
هذه ليست تماماً المعادلة (A1) من المرجع. [1]، التي تنص على "حتى دورانات الكيوبت الواحد" وتختلف عنها في ثلاثة أمور تستحق الذكر، لأن القارئ الذي يراجع الورقة سيلاحظها:
-
العامل السابق: المعادلة (A1) لا تحمل أي عامل. الـ هنا هو ما يجعل ، ليتطابق مع التطبيع الذي يستخدمه
TransferVertexOperator. الورقة لا تحتاج مؤثرات النقل إلا داخل ، حيث يُمتص عامل القياس في زاوية الدوران؛ أما هنا فيجب أن تكون مؤثرات تحقق متطابقة معيّنة. -
الاتجاه: المعادلة (A1) تختار الحروف حسب تكافؤ الصف لـ ، وهو في الحافة الأفقية نفسه لكلا الطرفين، فلا تميّز بين و . الورقة تستعيد الاتجاه بشكل منفصل، عبر الاقتران بطبقة من . هذا الدليل بدلاً من ذلك يدمجه في الترميز، ويشتق المعكوس من ، وهذا ما يجعل الاتجاهين يقعان على Paulis مختلفة في الخطوة 3.
-
الشكل العمودي: المعادلة (A1) تبدّل بين
XXXXوYYYYحسب تكافؤ الصف، بينما يستخدم هذا الدليلXYXYواحداً. هما طريقتان للحصول على الشيء نفسه: عدم تناظر في الحروف يجعل الحافة العمودية تتعارض بشكل صحيح مع الحواف الأفقية التي تلتقي بها. لا تعتبر هذا الفرق اختياراً حراً للأساس. إسقاط تبديل التكافؤ واستخدامXXXXواحد يكسر الجبر تماماً، ولهذا يتم التحقق من اختلاف الحروف بدلاً من افتراضه.
المساعد (ancilla) مرتبط بـ موقع، مما يعطي النسبة ؛ أما ترميز Derby-Klassen فيضع كيوبتاً مساعداً لكل خلية (فردية)، للحصول على نسبة أصغر هي . يُستخدم VC هنا لأن مؤثرات النقل فيه هي التي يعطي المرجع [1] كليفوردات قطرنتها بصيغة مغلقة.
1. الهاميلتوني على شبكة مربعة
البناء هو نفسه كما في البعد الواحد، لكن مع قائمة حواف ثنائية الأبعاد. تحتفظ المواقع بالترقيم صفاً بصف المستخدم في الشكل أعلاه، site(r, c) = r * cols + c، بحيث تربط الروابط الأفقية الفهارس المتتالية، وتربط الروابط العمودية الفهارس التي تختلف بمقدار cols:
>>> from collections import defaultdict
>>> from qiskit_fermions.operators import TransferVertexOperator
>>>
>>> def fermi_hubbard_2d(rows, cols, tunneling, interaction):
... site = lambda r, c: r * cols + c
... edges = [(site(r, c), site(r, c + 1)) for r in range(rows) for c in range(cols - 1)]
... edges += [(site(r, c), site(r + 1, c)) for r in range(rows - 1) for c in range(cols)]
... data: defaultdict[tuple[tuple[int, int], ...], complex] = defaultdict(complex)
... for j, k in edges:
... # hopping: -t (a^dag_j a_k + a^dag_k a_j) = t (T_jk + T_kj)
... data[((j, k),)] += tunneling
... data[((k, j),)] += tunneling
... # interaction: U n_j n_k with n_j = (1 - V_j) / 2
... data[()] += interaction / 4
... data[((j, j),)] -= interaction / 4
... data[((k, k),)] -= interaction / 4
... data[((j, j), (k, k))] += interaction / 4
... return TransferVertexOperator.from_dict(data)
>>>
>>> rows, cols = 3, 3
>>> num_sites = rows * cols
>>> hamiltonian = fermi_hubbard_2d(rows, cols, tunneling=1.0, interaction=2.0)
>>> print(f"Number of terms: {len(hamiltonian)}")
Number of terms: 46
2. أربع مجموعات تدفق بدلاً من اثنتين
يُصنَّف كل حد قفز حسب الاتجاه الذي يشير إليه سهمه على الشبكة. مقارنة إحداثيات الصف/العمود divmod للطرفين تفصل الحواف الأفقية عن العمودية، وإشارة الفرق تحدد الاتجاه. كل ما هو قطري (الوحدة، وحدود و ) يذهب إلى مجموعة خامسة:
اصطلاح الاتجاه. الأسماء مأخوذة ببساطة من الشكل أعلاه: السهم شرقي أو غربي حسب إشارته إلى اليمين أو اليسار على طول صف، وشمالي أو جنوبي حسب إشارته إلى الأعلى أو الأسفل على طول عمود. وبما أن الترقيم صفاً بصف، فالشرق والجنوب هما اتجاها زيادة الفهرس. لا شيء لاحقاً يعتمد على الأسماء (فهي مجرد تسميات للمجموعات)، لكن من السهل الخلط بينها، لذا من الأفضل تثبيتها بالنظر إلى الصورة. المرجع [1] يسمّي مجموعات التدفق على الشبكة المربعة بالطريقة نفسها.
>>> EAST, WEST, NORTH, SOUTH, DIAGONAL = range(5)
>>>
>>> def flow_set_groups(operator, cols):
... groups = []
... for terms, _ in operator.iter_terms():
... if len(terms) == 1 and terms[0][0] != terms[0][1]:
... j, k = terms[0]
... row_j, col_j = divmod(j, cols)
... row_k, col_k = divmod(k, cols)
... if row_j == row_k:
... groups.append(EAST if col_k > col_j else WEST)
... else:
... groups.append(SOUTH if row_k > row_j else NORTH)
... else:
... groups.append(DIAGONAL)
... return groups
>>>
>>> hamiltonian.groups = flow_set_groups(hamiltonian, cols)
>>>
>>> names = ["east", "west", "north", "south", "diagonal"]
>>> for index, name in enumerate(names):
... count = sum(1 for group in hamiltonian.groups if group == index)
... print(f"{name:9s} {count:2d}")
east 6
west 6
north 6
south 6
diagonal 22
داخل مجموعة تدفق واحدة، كل سهم إما يلتقي بسهم آخر رأساً بذيل أو لا يلتقي به أبداً، كما في البعد الواحد، لذا تبقى مجموعة التدفق تتبادل حداً بحد ولا يحمل تطورها أي خطأ Trotter. الجديد هو أن مجموعة التدفق صارت الآن غير متصلة: المجموعة الشرقية في شبكة rows x cols تتألف من rows مسارات مستقلة من اليسار إلى اليمين. هذا الانفصال هدية وليس تعقيداً، كما توضح الخطوة 6.
3. ترميز المؤثر
الترميز دالة واحدة تحوّل مؤثر نقل معمماً واحداً إلى سلسلة Pauli، تُمرَّر إلى map_transfer_vertex_generators(). مقارنةً بنسخة البعد الواحد، يحتوي الجسم على حالات أكثر: القطرية والأفقية والعمودية والاتجاهات المعكوسة عبر .
هناك مشكلة واحدة. map_transfer_vertex_generators() تستدعي المرجع بمولّد فقط ولا شيء غيره، بينما تحتاج سلسلة Pauli لرابط ما أيضاً إلى cols لاستعادة إحداثيات الصف والعمود من فهرس الوضع. بدلاً من تداخل الدوال لإغلاقها عليه، تربطه functools.partial() مما يُبقي الدالتين على المستوى الأعلى وقابلتين للاستدعاء باستقلال.
>>> from functools import partial
>>>
>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_transfer_vertex_generators
>>>
>>> def ancilla_qubit(mode, num_sites):
... """The auxiliary qubit paired with fermionic mode `mode`."""
... return num_sites + mode
>>>
>>> def vc_action(generator, width, cols):
... """Map one generalized transfer operator to its Verstraete-Cirac Pauli."""
... num_sites = width // 2
... ancilla = partial(ancilla_qubit, num_sites=num_sites)
... j, k = generator
... if j == k: # vertex operator V_j = Z_j
... return SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... row_j, col_j = divmod(j, cols)
... row_k, col_k = divmod(k, cols)
... if row_j == row_k: # horizontal: weight-3 triangle
... left, right = (j, k) if col_j < col_k else (k, j)
... operator = SparseObservable.from_sparse_list(
... [("XXZ", [left, right, ancilla(left)], 0.5)], num_qubits=width
... )
... forward = (j, k) == (left, right)
... else: # vertical: weight-4 square
... up, down = (j, k) if row_j < row_k else (k, j)
... operator = SparseObservable.from_sparse_list(
... [("XYXY", [up, down, ancilla(up), ancilla(down)], 0.5)], num_qubits=width
... )
... forward = (j, k) == (up, down)
... if forward:
... return operator
... # reverse orientation: T_kj = -V_j V_k T_jk
... v_j = SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... v_k = SparseObservable.from_sparse_list([("Z", [k], 1.0)], num_qubits=width)
... return (-1.0 * v_j.compose(v_k).compose(operator)).simplify()
>>>
>>> def vc_encoding(operator, width, cols):
... """Map a TransferVertexOperator to a qubit observable, Verstraete-Cirac style."""
... return map_transfer_vertex_generators(
... operator,
... partial(vc_action, width=width, cols=cols),
... identity=lambda: SparseObservable.identity(width),
... compose=SparseObservable.compose,
... ).simplify()
>>>
>>> # bind the lattice width once; the transpiler only ever passes (operator, width)
>>> encoding = partial(vc_encoding, cols=cols)
>>> num_qubits = 2 * num_sites
>>> num_qubits
18
يمكن رؤية فائدة التجميع قبل التعيين فوراً: كل مجموعة تدفق تُرمَّز إلى شكل Pauli واحد، موحّد على المجموعة كلها.
>>> for index, name in enumerate(names):
... observable = encoding(hamiltonian.split_out_groups()[index], num_qubits)
... shapes = sorted({label for label, _, _ in observable.to_sparse_list()})
... print(f"{name:9s} {shapes}")
east ['XXZ']
west ['YYZ']
north ['YXXY']
south ['XYXY']
diagonal ['', 'Z', 'ZZ']
أشكال القفز الأربعة مختلفة، لذا تحصل كل مجموعة تدفق على كليفورد خاص بها في الخطوة 5. الاتجاهات المعكوسة هي Paulis مختلفة وليست نفسها بعد إعادة التسمية؛ ولهذا يحتاج تركيب مجموعات التدفق ثنائي الأبعاد إلى أربع دارات، بينما يحتاج تركيب البعد الواحد إلى دارة واحدة.
4. التحويل (Transpile) بالترميز المخصص
هذه المرحلة هي مرحلة البعد الواحد مع سجل كيوبتات أوسع: CustomF2QLayout تعيّن سجل وضعاً على كيوبت، وMapperFnEvolutionSynthesis تحمل الترميز، ووسيطها product_formula يحدد كيف تُفكَّك PauliEvolutionGate الناتجة. قارن الخطوة 6 من دليل البعد الواحد التي تبني خط المعالجة نفسه لكيوبت مساعد واحد.
>>> from qiskit.circuit import QuantumRegister
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.synthesis import LieTrotter
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import FermionicLieTrotter
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG, QuantumDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import (
... CustomF2QLayout,
... F2QSynthesis,
... MapperFnEvolutionSynthesis,
... )
>>>
>>> total_time = 1.0
>>>
>>> def flow_set_pass_manager(mode_register, encoding, product_formula=None):
... """Build the pipeline mapping `mode_register` with the VC `encoding`."""
... synthesis = F2QSynthesis()
... synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(
... encoding, product_formula=product_formula
... )
... # this encoding spends one ancilla per fermionic mode
... qubit_register = QuantumRegister(2 * mode_register.size, "q")
... return MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... layout=CustomF2QLayout({mode_register: qubit_register}),
... synthesis=synthesis,
... output=QuantumDAGToCircuit(),
... )
>>>
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(
... Evolution(
... num_sites, hamiltonian, time=total_time, synthesis=FermionicLieTrotter()
... ),
... circuit.modes,
... )
>>>
>>> # as in 1D, one decompose() splits the Evolution group by group (five gates here,
>>> # the four hopping flow sets in group order, then the diagonal interaction)
>>> flow_set_circuit = circuit.decompose()
>>> flow_set_circuit.count_ops()["Evolution"]
5
خمس بوابات، تحمل الحدود 6 و6 و6 و6 و22 المحسوبة في الخطوة 2. تُعيَّن كل منها وتُركَّب باستقلال، لذا يبقى التقسيم المقرر على المستوى الفرميوني حتى داخل الدارة:
>>> pass_manager = flow_set_pass_manager(flow_set_circuit.register, encoding, LieTrotter())
>>> qubit_circuit = pass_manager.run(flow_set_circuit)
>>> qubit_circuit.num_qubits
18
>>>
>>> def two_qubit_stats(circuit):
... decomposed = circuit.decompose(reps=6)
... two_qubit = lambda instruction: len(instruction.qubits) == 2
... return decomposed.count_ops().get("cx", 0), decomposed.depth(two_qubit)
>>>
>>> two_qubit_stats(qubit_circuit)
(144, 47)
هذا هو خط الأساس: الترميز والتجميع كلاهما في مكانه، لكن LieTrotter تركّب كل مجموعة تدفق حداً بحد، ويزداد العمق مع الشبكة.
5. كليفورد بعمق 2 لكل مجموعة تدفق
الآلية هي نفسها كما في البعد الواحد: نقترن بمجموعة تدفق كاملة متبادلة بكليفورد يعيّن كل حد فيها إلى Pauli مميز بوزن 1، بحيث لا يكلّف تطور المجموعة كلها سوى دورانات كيوبت واحد. في البعد الواحد، كان هذا يتم بطبقة واحدة من بوابات CZ.
في البعد الثنائي، تحتاج كل مجموعة تدفق إلى دارتها الخاصة، وفق القسم IV C والشكل 7 من المرجع [1]. تعمل الأربع كلها على خط واحد من الشبكة في كل مرة (صف للشرق/الغرب، وعمود للشمال/الجنوب) مع كيوبتات المساعدة لذلك الخط، وللأربع الشكل الثلاثي الأجزاء نفسه:
-
طبقة من دورانات الكيوبت الواحد، تضع كل كيوبت في الأساس الذي تحتاجه مجموعة تدفقه.
-
طبقة
CXأولى، دائماً من الموقع إلى المساعد في الموقع نفسه. -
طبقة
CXثانية، تقرن الجيران على طول الخط.
الخطوتان 2 و3 هما الطبقتان الوحيدتان ثنائيتا الكيوبت، لذا فعمق الكيوبتين هو ، بغض النظر عن طول الخط. وهي مكتوبة أدناه في دالة لكل مجموعة تدفق. الدورانات على ما هي عليه لأنه تم إيجادها، عبر اقتران حدود كل مجموعة تدفق واشتراط صور مميزة بوزن 1، لذا اعتبرها معطاة واعتمد على الفحص الذي يلي.
المجموعتان الأفقيتان هما الأبسط. حدودهما بوزن 3، XXZ وYYZ، ولا تختلفان إلا بـ إضافية تحوّل إلى على المواقع:
>>> from qiskit.circuit import QuantumCircuit
>>>
>>> def east_clifford(circuit, line, ancilla):
... """Diagonalize the east flow set (XXZ) on one lattice row."""
... for j in line:
... circuit.h(ancilla(j))
... for j in line[:-1]: # layer 1: each site with its own ancilla
... circuit.cx(ancilla(j), j)
... for j, k in zip(line, line[1:]): # layer 2: ancilla with the next site along
... circuit.cx(k, ancilla(j))
>>>
>>> def west_clifford(circuit, line, ancilla):
... """Diagonalize the west flow set (YYZ) on one lattice row."""
... for j in line:
... circuit.s(j) # the only difference from east: X -> Y on the sites
... circuit.h(ancilla(j))
... for j in line[:-1]:
... circuit.cx(ancilla(j), j)
... for j, k in zip(line, line[1:]):
... circuit.cx(k, ancilla(j))
المجموعتان العموديتان بوزن 4، XYXY وYXXY، وهما أكثر انشغالاً بالتبعية: تتناوب الدورانات مع الموضع على طول الخط، وتتناوب طبقة CX الثانية أيضاً، فتقرن المواقع في الأماكن الزوجية والمساعدات في الفردية (أو العكس):
>>> def south_clifford(circuit, line, ancilla):
... """Diagonalize the south flow set (XYXY) on one lattice column."""
... for position, j in enumerate(line):
... if position % 2 == 0:
... circuit.sdg(ancilla(j))
... circuit.h(ancilla(j))
... else:
... circuit.h(j)
... circuit.s(j)
... circuit.h(ancilla(j))
... circuit.s(ancilla(j))
... for j in line: # layer 1
... circuit.cx(ancilla(j), j)
... for position, (j, k) in enumerate(zip(line, line[1:])): # layer 2
... if position % 2 == 0:
... circuit.cx(ancilla(j), ancilla(k)) # ancilla to ancilla
... else:
... circuit.cx(j, k) # site to site
>>>
>>> def north_clifford(circuit, line, ancilla):
... """Diagonalize the north flow set (YXXY) on one lattice column."""
... for position, j in enumerate(line):
... if position % 2 == 0:
... circuit.h(ancilla(j))
... circuit.s(ancilla(j))
... else:
... circuit.h(j)
... circuit.s(j)
... circuit.sdg(ancilla(j))
... circuit.h(ancilla(j))
... for j in line: # layer 1
... circuit.cx(ancilla(j), j)
... for position, (j, k) in enumerate(zip(line, line[1:])): # layer 2
... if position % 2 == 0:
... circuit.cx(k, j) # site to site, and the other way round from south
... else:
... circuit.cx(ancilla(j), ancilla(k))
عند رسمها على خط واحد من ثلاثة مواقع (المواقع أولاً ثم مساعداتها) تتسع الآلية كلها في صورة واحدة. الشكل المثلثي هو الأوضح من الاثنين: تغيير أساس على المساعدات، ثم طبقتا CX، الأولى تقرن كل موقع بمساعده، والثانية تمتد إلى الموقع التالي على طول الخط.
>>> line = [0, 1, 2] # three sites, with ancillas on qubits 3, 4, 5
>>> triangle = QuantumCircuit(6)
>>> east_clifford(triangle, line, lambda mode: 3 + mode)
>>> triangle.draw("mpl")
<Figure size ... with 1 Axes>
الشكل المربع يحتاج عملاً أكثر على الكيوبت الواحد (تتناوب الدورانات على طول الخط) وتتناوب طبقة CX الثانية فيه بين قرن المواقع وقرن المساعدات:
>>> square = QuantumCircuit(6)
>>> south_clifford(square, line, lambda mode: 3 + mode)
>>> square.draw("mpl")
<Figure size ... with 1 Axes>
كلاهما بعمق طبقتي CX، وإضافة مواقع إلى الخط توسّعهما دون تعميقهما؛ هذا هو ادعاء العمق الثابت كله، ظاهر بالمعاينة. كليفوردا الغرب والشمال يختلفان في طبقات الكيوبت الواحد فقط، لذا لا يُرسمان منفصلين.
ما لا يزال التركيب يحتاجه هو تحديد أين ينتهي كل حد. خط من موقعاً يحمل رابطاً، يساهم كل منها بحد واحد، لذا تُرجع الدالة المساعدة أدناه مدخلاً واحداً لكل رابط: الكيوبت الذي هبط عليه الحد، وPauli الكيوبت الواحد الذي صار إليه. والأهم أن هذه تُقرأ من الاقتران ولا تُتوقع. بخلاف البعد الواحد، حيث تعيّن طبقة CZ واحدة بصيغة مغلقة، لا توجد صيغة كهذه هنا، ومساعدات خط العمود غير متجاورة. الشيء الوحيد الذي يختلف بين مجموعات التدفق هو سلسلة Pauli الداخلة، التي طبعتها الخطوة 3 بالفعل:
>>> from qiskit.quantum_info import Clifford, SparsePauliOp
>>>
>>> def clifford_supports(clifford, pauli, line, ancilla, num_qubits):
... """Conjugate a flow set's terms to find which qubit each one lands on.
...
... `pauli` is the flow set's Pauli string, as printed in step 3. Its letters apply
... to the i-th term's qubits in the order below: the two sites of the bond, then
... their ancillas.
...
... This is the one check that needs SparsePauliOp: Clifford conjugation wants a
... `Pauli` and its symplectic bits, and SparseObservable exposes neither -- its own
... `evolve` conjugates by a Pauli, not by a Clifford.
... """
... supports = []
... for i in range(len(line) - 1):
... qubits = [line[i], line[i + 1], ancilla(line[i]), ancilla(line[i + 1])]
... term = SparsePauliOp.from_sparse_list(
... [(pauli, qubits[: len(pauli)], 1.0)], num_qubits=num_qubits
... )
... image = term.paulis[0].evolve(clifford, frame="s")
... # a weight-1 image has exactly one qubit with an X and/or Z bit set
... (support,) = np.nonzero(image.x | image.z)
... assert len(support) == 1, f"term {i} is not weight-1"
... supports.append((int(support[0]), image[support].to_label()))
... return supports
يجب أن تخرج الصور مميزة وبوزن 1، بحيث يمكن تدوير كل حد باستقلال على كيوبته الخاص؛ وعمق الكيوبتين ظاهر بالفعل في التركيبات الأربعة أعلاه، فكل منها يُصدر طبقتي CX بصرف النظر عن طول الخط. أدناه، يُتحقق من الخاصيتين عبر كل مجموعة تدفق وكل خط وعدة أحجام للشبكة. ثم تستدعي الخطوة 6 clifford_supports، فهي جزء عامل من التركيب وليست مجرد عرض توضيحي.
6. تركيب واعٍ بمجموعات التدفق
يحصل التركيب على بداية مفيدة. بما أن Evolution تقسّم نفسها مجموعة بمجموعة، تُستدعى synthesize مرة لكل مجموعة تدفق (خمس مرات هنا، أربع مجموعات قفز زائد المجموعة القطرية) فيتشارك كل حد قفز تراه شكل Pauli واحداً، ويقوم كليفورد واحد بقطرنتها جميعاً. ما عليها إلا أن تقرأ ذلك الشكل من الحد الأول وتفرز الباقي حسب الخط الذي يقع عليه. ثم تأتي الملاحظة التي تجعل البعدين أرخص مما قد يُخشى:
مكوّنات مجموعة تدفق واحدة منفصلة الرؤوس: صفوف المجموعة الغربية لا تتشارك كيوبتات. لذا يمكن إصدار كل كليفوردات مجموعة التدفق معاً وتتشارك طبقتي الكيوبتين نفسيهما، بدلاً من الاصطفاف صفاً بعد صف. وبذلك تكلّف مجموعة التدفق عدداً ثابتاً من الطبقات (اثنتان للكليفورد واثنتان لمعكوسه) مهما بلغ عدد صفوف الشبكة أو أعمدتها، ولهذا ينتهي العمق الكلي ثابتاً.
>>> from qiskit.synthesis.evolution import EvolutionSynthesis
>>>
>>> # The interaction couples nearest neighbors only, so every interior site carries four
>>> # Rzz gates -- one per bond -- and can only do one at a time: four layers is the best
>>> # any schedule can achieve. Horizontal bonds split by column parity, vertical ones by
>>> # row parity, which achieves it.
>>> def rzz_layers(circuit, angles, cols):
... """Emit the diagonal Rzz gates in four layers of disjoint qubit pairs."""
... horizontal = [(j, k) for j, k in angles if k - j == 1]
... vertical = [(j, k) for j, k in angles if k - j == cols]
... schedule = [
... [(j, k) for j, k in horizontal if (j % cols) % 2 == 0],
... [(j, k) for j, k in horizontal if (j % cols) % 2 == 1],
... [(j, k) for j, k in vertical if (j // cols) % 2 == 0],
... [(j, k) for j, k in vertical if (j // cols) % 2 == 1],
... ]
... # emit layer by layer, since Qiskit's depth follows the order gates were added
... for layer in schedule:
... for j, k in layer:
... circuit.rzz(angles[j, k], j, k)
>>>
>>> class FlowSetSynthesis(EvolutionSynthesis):
... """Synthesize one 2D line flow set at a time, via its depth-2 Clifford."""
...
... preserve_order = True
...
... def __init__(self, rows, cols):
... self.rows, self.cols = rows, cols
...
... def rows_of_lattice(self):
... return [[r * self.cols + c for c in range(self.cols)] for r in range(self.rows)]
...
... def columns_of_lattice(self):
... return [[r * self.cols + c for r in range(self.rows)] for c in range(self.cols)]
...
... def synthesize(self, evolution):
... num_qubits = 2 * self.rows * self.cols
... ancilla = partial(ancilla_qubit, num_sites=self.rows * self.cols)
... circuit = QuantumCircuit(evolution.operator.num_qubits)
... terms = [
... (label, indices, 2 * evolution.time * coeff.real)
... for label, indices, coeff in evolution.operator.to_sparse_list()
... ]
...
... # `Evolution.decompose()` hands us one gate per flow set, so every hopping term
... # here shares a single Pauli shape and one Clifford diagonalizes all of them
... shapes = tuple({label for label, _, _ in terms} - {"", "Z", "ZZ"})
... match shapes:
... case ("XXZ",):
... clifford_function, lines = east_clifford, self.rows_of_lattice()
... along_rows = True
... case ("YYZ",):
... clifford_function, lines = west_clifford, self.rows_of_lattice()
... along_rows = True
... case ("XYXY",):
... clifford_function, lines = south_clifford, self.columns_of_lattice()
... along_rows = False
... case ("YXXY",):
... clifford_function, lines = north_clifford, self.columns_of_lattice()
... along_rows = False
... case (): # the diagonal group: no conjugation needed
... self.diagonal(circuit, terms)
... return circuit
... case _:
... raise ValueError(f"expected one known flow set per gate, got {shapes}")
...
... # a term's line is the row it sits in and its slot the column, or vice versa
... rotations: dict[int, list] = defaultdict(list)
... for _, indices, angle in terms:
... row, col = divmod(indices[0], self.cols)
... line, slot = (row, col) if along_rows else (col, row)
... rotations[line].append((slot, angle))
...
... active = sorted(rotations)
... # the lines of one flow set are vertex-disjoint, so all of their Cliffords
... # are emitted together and share the same two two-qubit layers
... conjugation = QuantumCircuit(num_qubits)
... for line in active:
... clifford_function(conjugation, lines[line], ancilla)
... circuit.compose(conjugation, inplace=True)
... for line in active:
... supports = clifford_supports(
... Clifford(conjugation), shapes[0], lines[line], ancilla, num_qubits
... )
... for slot, angle in rotations[line]:
... match supports[slot]:
... case qubit, "X":
... circuit.rx(angle, qubit)
... case qubit, "Y":
... circuit.ry(angle, qubit)
... case qubit, "Z":
... circuit.rz(angle, qubit)
... circuit.compose(conjugation.inverse(), inplace=True)
... return circuit
...
... def diagonal(self, circuit, terms):
... """Synthesize the diagonal group: a phase, single-qubit Z and paired Rzz."""
... rzz = {}
... for label, indices, angle in terms:
... match label, indices:
... case "", _: # the identity contributes only a global phase
... circuit.global_phase -= angle / 2
... case "Z", [qubit]: # a lone vertex operator
... circuit.rz(angle, qubit)
... case "ZZ", [j, k]: # interaction, see rzz_layers above
... rzz[min(j, k), max(j, k)] = angle
... case _:
... raise ValueError(f"unexpected Pauli term: {label} on {indices}")
... rzz_layers(circuit, rzz, self.cols)
كما في البعد الواحد، هذا مكتوب عمداً لهذا الترميز: يتعرف على أشكال Pauli الأربعة ويُطلق خطأً عند أي شيء آخر بدلاً من الرجوع بصمت. ويفترض أيضاً تقسيم مجموعة تدفق واحدة لكل بوابة أعلاه، ويقول ذلك؛ أكثر من شكل قفز واحد في بوابة واحدة يعني أن التجميع لم يُطبَّق، وهذا خطأ وليس شيئاً يُلتف حوله.
مثل نظيره في البعد الواحد، لا يأخذ هذا التركيب عدد خطوات Trotter. خطوة Trotter يجب أن تتنقل بين مجموعات التدفق، لكن transpile_trotter_step أدناه تفكّك الدارة الفرميونية إلى بوابة Evolution واحدة لكل مجموعة، لذا ترى هذه الطريقة مجموعة واحدة فقط في كل مرة ولا تستطيع التكرار إلا داخلها، وهذا لا يغيّر شيئاً: تكرار مؤثر واحد يقسم تطوره فقط إلى أجزاء متساوية.
لذلك يحتاج Trotter متعدد الخطوات إلى صيغة منتَج تحترم groups للمؤثر داخل بوابة واحدة، قبل التفكيك. FermionicLieTrotter تفعل ذلك، والخطوة 10 تستخدمها لقياس التقارب.
لاحظ الفئتين المتشابهتين في الاسم هنا. LieTrotter من Qiskit، المستخدمة كخط أساس أعلاه، تعمل على الهاميلتوني المعيَّن، حيث اختفى التجميع بالفعل. أما FermionicLieTrotter فهي النظير الفرميوني، وهي الوحيدة التي ما زالت ترى مجموعات التدفق.
7. النتيجة: عمق ثابت
كل ما سبق جرى على شبكة الواحدة المبنية في الخطوة 1. مقارنة مساري التركيب عبر شبكات متزايدة تحتاج تكرار تلك السلسلة كلها (المؤثر، التجميع، الترميز، الدارة، مدير التمريرات) لكل حجم، لذا يستحق الأمر تغليفها مرة واحدة:
>>> def transpile_trotter_step(rows, cols, product_formula, steps=1):
... """Transpile `steps` Trotter steps on a `rows` x `cols` lattice."""
... num_sites = rows * cols
... operator = fermi_hubbard_2d(rows, cols, 1.0, 2.0)
... operator.groups = flow_set_groups(operator, cols)
... encoding = partial(vc_encoding, cols=cols)
...
... circuit = FermionicCircuit(num_sites)
... circuit.append(
... Evolution(
... num_sites,
... operator,
... time=total_time,
... synthesis=FermionicLieTrotter(reps=steps),
... ),
... circuit.modes,
... )
... circuit = circuit.decompose() # split into one Evolution per flow set
...
... pass_manager = flow_set_pass_manager(circuit.register, encoding, product_formula)
... return pass_manager.run(circuit)
جمع المقياسين عبر مدى من أحجام الشبكة ورسمهما مقابل حافة الشبكة يجعل الفرق واضحاً:
>>> sizes = [3, 4, 5, 6, 7, 8]
>>> lie_trotter = [
... two_qubit_stats(transpile_trotter_step(n, n, LieTrotter())) for n in sizes
... ]
>>> flow = [
... two_qubit_stats(transpile_trotter_step(n, n, FlowSetSynthesis(n, n))) for n in sizes
... ]
>>>
>>> figure, axes = plt.subplots(1, 2, figsize=(9, 3.5), layout="constrained")
>>> for panel, index, title in zip(axes, (1, 0), ("two-qubit depth", "CX count")):
... _ = panel.plot(sizes, [stat[index] for stat in lie_trotter], "o-", label="qubit-level")
... _ = panel.plot(sizes, [stat[index] for stat in flow], "s-", label="flow set")
... _ = panel.set(xlabel="lattice edge $L$ (an $L \\times L$ lattice)", title=title)
... _ = panel.legend()
>>> figure
<Figure size ... with 2 Axes>
عمق الكيوبتين هو 24 وثابت من إلى (أي من 18 كيوبت إلى 128)، بينما تنمو LieTrotter المطبّقة على الهاميلتوني المرمَّز بالطريقة نفسها خطياً مع حافة الشبكة، من 47 إلى 92. يزداد عدد البوابات في الحالتين، لأن هناك من الحدود لتطبيقها في كل حال؛ ما تزيله مجموعات التدفق هو نمو عدد الطبقات التي تشغلها تلك البوابات.
تنقسم الميزانية إلى 16 للقفز وثمانية للتفاعل:
-
تكلّف كل مجموعة من مجموعات التدفق الأربع من طبقات الكيوبتين: كليفوردها الداخل ومعكوسه الخارج، كلاهما بعمق اثنين بالبناء، مع دورانات كيوبت واحد فقط بينهما. هذا ، وهو الرقم الذي يذكره المرجع [1]، لأنه يحصي حدود القفز فقط. ضبط في الشيفرة أعلاه يعيد إنتاجه.
-
تضيف المجموعة القطرية الثمانية المتبقية. بوابات
Rzzفيها تتكدس في أربع طبقات، وكلRzzتتفكك إلى بوابتيCXحول دوران كيوبت واحد، لذا . أربع طبقات هو الأمثل: على شبكة مربعة، لا يصل التفاعل إلا إلى أقرب الجيران، فيحمل كل موقع داخلي أربع بواباتRzz(واحدة لكل رابط) ولا يستطيع المشاركة إلا في واحدة في كل مرة.
لا شيء في أي من المجموعين يشير إلى حجم الشبكة، وهذا هو بيت القصيد.
8. التحقق من الدارة
تركيب مجموعات التدفق وLieTrotter تصفان خطوة Trotter نفسها للهاميلتوني المرمَّز نفسه، لذا يجب أن تتفق وحدويتاهما حتى طور عام. المتجهات الحالية العشوائية تكشف أي اختلاف بتكلفة أقل بكثير من بناء الوحدوية الكاملة:
تُفكَّك الدارتان كلتاهما إلى بوابات من كيوبت واحد وكيوبتين قبل تطبيقهما على حالة. إذا تُركت بلا تفكيك، فإن كل تعليمة Evolution عالية المستوى تجبر Statevector على بناء مؤثرها الكثيف الكامل ؛ أما التفكيك أولاً فيتيح لها تطبيق البوابات واحدة تلو الأخرى، وهو أسرع بمئات المرات هنا.
>>> import numpy as np
>>> from qiskit.quantum_info import Statevector
>>>
>>> rng = np.random.default_rng(1234)
>>>
>>> def agrees_up_to_one_phase(rows, cols, samples=3):
... """Compare both syntheses on random states.
...
... Returns whether they agree up to a phase, and whether it is the *same* phase
... for every input state.
... """
... lie_trotter = transpile_trotter_step(
... rows, cols, LieTrotter()
... ).decompose(reps=6)
... flow = transpile_trotter_step(
... rows, cols, FlowSetSynthesis(rows, cols)
... ).decompose(reps=6)
... dimension = 2**lie_trotter.num_qubits
... residuals, phases = [], []
... for _ in range(samples):
... vector = rng.normal(size=dimension) + 1j * rng.normal(size=dimension)
... state = Statevector(vector / np.linalg.norm(vector))
... a = state.evolve(lie_trotter).data
... b = state.evolve(flow).data
... overlap = np.vdot(a, b)
... phases.append(overlap / abs(overlap))
... residuals.append(np.linalg.norm(b - a * overlap / abs(overlap)))
... # the SAME global phase must work for every input state
... spread = max(abs(phase - phases[0]) for phase in phases)
... return bool(max(residuals) < 1e-9), bool(spread < 1e-9)
>>>
>>> agrees_up_to_one_phase(rows, cols)
(True, True)
كلا نصفي هذا الفحص مهم. تداخل بمعيار واحد على حالة واحدة ضروري لكنه غير كافٍ: تركيب أخطأ في طور يعتمد على الحالة سيمر مع ذلك. اشتراط الطور نفسه عبر عدة حالات عشوائية هو ما يسد هذه الثغرة.
9. التحقق من الفيزياء على الفضاء الجزئي المثبِّت
هذه هي العقدة ثنائية الأبعاد. في البعد الواحد، كان الترميز إيزومتريا، وكانت مقارنة الأطياف مع مرجع Jordan-Wigner كافية. VC ليس إيزومتريا: فهو يستخدم كيوبت لـ وضعاً، لذا يحتوي فضاء بُعده على فضاء فيزيائي بُعده . هذا عام في الترميزات المحلية ثنائية الأبعاد (الكيوبتات الفائضة مقيَّدة بتماثلات متبادلة محلية، واحد لكل خلية [2] [3]) ويعني أن الهاميلتوني المرمَّز يمثّل الفرميوني بأمانة فقط على الفضاء الذاتي المشترك لتلك القيود.
كلمة المثبِّت (stabilizer) تؤدي معنيين في هذا الموضوع، وهما غير مترابطين. يستخدمها المرجع [1] لمؤثرات النقل المتبادلة في مجموعة تدفق، التي تشكّل زمرة مثبِّتات تقطرنها كليفوردات الخطوة 5 (أداة تصريف، وهو المعنى المقصود في كل ما سبق). يستخدمها هذا القسم بمعنى تصحيح الأخطاء: مؤثرات فضاؤها الذاتي هو الفضاء الفيزيائي. المرجع [1] ينبّه صراحة إلى هذا التصادم ويضع المعنى الثاني جانباً؛ قيود الخلايا أدناه خاصية في ترميز VC [2]، وليست نتيجة لإطار مجموعات التدفق.
تُبنى المولّدات هنا ولا تُقتبس: واحد لكل وجه مربع، وكل منها ببساطة حلقة مؤثر الحافة المرمَّزة حول ذلك الوجه.
مؤثرات الحافة هي الكائن الطبيعي لهذا. الحلقة المغلقة منها تماثل لأي هاميلتوني يحفظ عدد الجسيمات، بينما حلقة مؤثرات النقل المقابلة ليست كذلك: الاثنتان تختلفان بمؤثر رأس على موقع مصدر كل رابط،
حول حلقة مغلقة يكون كل موقع مصدر رابط واحد بالضبط، لذا تجمع الحلقة واحداً لكل موقع، وهو التكافؤ المحلي لمواقع الخلية الأربعة. ذلك التكافؤ المحلي هو ما يصنع الفرق: الهاميلتوني يقفز بالجسيمات عبر حدود الخلية، لذا فهو يحفظ التكافؤ الكلي فقط. لذا فحلقة النقل المجردة لا تتبادل معه، بينما حلقة الحافة (التي تحمل عامل التكافؤ) تتبادل.
بما أن الترميز معرَّف على مؤثرات النقل، فإن تعيين مؤثر حافة يحتاج العلاقة أعلاه. هذا استدعاء مرجعي من خمسة أسطر، ويدخل في map_edge_vertex_generators() تماماً كما تدخل vc_action في map_transfer_vertex_generators():
>>> from qiskit_fermions.mappers import map_edge_vertex_generators
>>> from qiskit_fermions.operators import EdgeVertexOperator
>>>
>>> def vc_edge_action(generator, width, cols):
... """Map one generalized *edge* operator to its Verstraete-Cirac Pauli."""
... j, k = generator
... if j == k: # a vertex operator is the same object in both algebras
... return SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... # The library decomposes T_jk = (i/2) V_j E_jk, so E_jk = -2i V_j T_jk. Reuse the
... # transfer encoding for T_jk rather than repeating its four cases here. Note that
... # `compose` left-multiplies its argument, so this composes to V_j T_jk; the other
... # order carries the opposite sign, since a vertex and a transfer operator sharing a
... # mode anticommute.
... vertex = SparseObservable.from_sparse_list([("Z", [j], 1.0)], num_qubits=width)
... return (-2.0j * vc_action(generator, width, cols).compose(vertex)).simplify()
>>>
>>> def vc_edge_encoding(operator, width, cols):
... """Map an EdgeVertexOperator to a qubit observable, Verstraete-Cirac style."""
... return map_edge_vertex_generators(
... operator,
... partial(vc_edge_action, width=width, cols=cols),
... identity=lambda: SparseObservable.identity(width),
... compose=SparseObservable.compose,
... ).simplify()
>>>
>>> def plaquette_loops(rows, cols):
... """The directed edge loop around each square face."""
... at = lambda r, c: r * cols + c
... return [
... [(at(r, c), at(r, c + 1)), (at(r, c + 1), at(r + 1, c + 1)),
... (at(r + 1, c + 1), at(r + 1, c)), (at(r + 1, c), at(r, c))]
... for r in range(rows - 1)
... for c in range(cols - 1)
... ]
>>>
>>> def stabilizers(rows, cols):
... """One plaquette stabilizer per square face of the lattice."""
... num_qubits = 2 * rows * cols
... out = []
... for loop in plaquette_loops(rows, cols):
... loop_operator = EdgeVertexOperator.from_dict({tuple(loop): 1.0})
... encoded_loop = vc_edge_encoding(loop_operator, num_qubits, cols)
... out.append(encoded_loop / complex(encoded_loop.coeffs[0]))
... return out
>>>
>>> plaquette_stabilizers = stabilizers(rows, cols)
>>> for stabilizer in plaquette_stabilizers:
... print(stabilizer.to_sparse_list())
[('ZZYXXY', [1, 4, 9, 10, 12, 13], (1+0j))]
[('ZZYXXY', [2, 5, 10, 11, 13, 14], (1+0j))]
[('ZZYXXY', [4, 7, 12, 13, 15, 16], (1+0j))]
[('ZZYXXY', [5, 8, 13, 14, 16, 17], (1+0j))]
لكل مولّد وزن ، موزّع على اثنين من كيوبتات خليته الفيزيائية وأربعة مساعدات. هي انطوائية وتتبادل مع الهاميلتوني المرمَّز، لذا يُحفظ الفضاء الفيزيائي بالديناميكيات. كلتا الخاصيتين عبارتان عن مؤثرات، فيُتحقق منهما رمزياً على جبر SparseObservable، دون الحاجة إلى مصفوفات :
>>> encoded = encoding(hamiltonian, num_qubits)
>>> identity = SparseObservable.identity(num_qubits)
>>> for stabilizer in plaquette_stabilizers:
... square = stabilizer.compose(stabilizer).simplify()
... print(square == identity, stabilizer.commutes(encoded))
True True
True True
True True
True True
مع المثبِّتات في اليد، لا تحتاج المقارنة إلى تقابل أساسات، لأن تعني أن الإشغالات تعيش على الكيوبتات الفيزيائية نفسها في الصورتين. إذن: نكتب حالة جداء مباشرة على الكيوبتات الفيزيائية مع المساعدات في ، ونُسقطها على الفضاء الجزئي المثبِّت، ونطوّرها بالهاميلتوني المرمَّز، ونقرأ كثافات المواقع من الجانبين.
كل خطوة هنا تبقى على مستوى المتجه. يُطبَّق المسقِط عاملاً في كل مرة بدلاً من ضربه كله، ويستخدم التطور الزمني expm_multiply()، التي تحسب دون تكوين أبداً. وبما أن الهاميلتوني المرمَّز مؤثر متناثر ببُعد ، فإن هذا التمييز هو ما يُبقي هذا القسم رخيصاً.
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_fermions.mappers.library import transfer_vertex_jordan_wigner
>>>
>>> # an exact evolution time, unrelated to the Trotter step above
>>> evolution_time = 0.6
>>> reference = transfer_vertex_jordan_wigner(hamiltonian, num_sites).simplify()
>>>
>>> def to_sparse_matrix(observable):
... # SparseObservable carries no dense- or sparse-matrix method, so route through
... # SparsePauliOp. This is only ever needed for the numerical checks below --
... # the encoding itself never leaves SparseObservable.
... return SparsePauliOp.from_sparse_observable(observable).to_matrix(sparse=True)
>>>
>>> def basis_state(occupation, num_qubits):
... """The computational basis state with `occupation` on the first qubits.
...
... Qubit `q` carries bit `2**q`, and the ancillas are the *high* qubits `N..2N-1`,
... so the same integer indexes the same site occupations on either register and
... leaves every ancilla in |0>.
... """
... index = int("".join(str(bit) for bit in reversed(occupation)), 2)
... state = np.zeros(2**num_qubits, dtype=complex)
... state[index] = 1.0
... return state
>>>
>>> def project_onto_codespace(state, stabilizers):
... """Apply the projector one (1 + S) factor at a time, then renormalize.
...
... The factors commute, so their product is itself a projector and the order does
... not matter. Multiplying it out would build a dense 2^2N x 2^2N operator; applied
... factor by factor this stays a handful of sparse matrix-vector products.
...
... Each factor is really (1 + S)/2, but every one contributes the same scalar, so the
... halvings only rescale the result and the final normalization absorbs them.
... """
... for stabilizer in stabilizers:
... state += to_sparse_matrix(stabilizer) @ state
... return state / np.linalg.norm(state)
>>>
>>> occupation = [1, 0, 1, 0, 1, 0, 0, 1, 0] # site occupations of the initial state
>>> initial_reference = basis_state(occupation, num_sites)
>>> initial_encoded = project_onto_codespace(
... basis_state(occupation, num_qubits), plaquette_stabilizers
... )
>>>
>>> evolved_encoded = expm_multiply(
... -1j * evolution_time * to_sparse_matrix(encoded), initial_encoded
... )
>>> evolved_reference = expm_multiply(
... -1j * evolution_time * to_sparse_matrix(reference), initial_reference
... )
>>>
>>> def density(state, qubit, width):
... """Site density <n_j> = (1 - <Z_j>) / 2 of a statevector."""
... z = to_sparse_matrix(
... SparseObservable.from_sparse_list([("Z", [qubit], 1.0)], num_qubits=width)
... )
... return (1 - (state.conj() @ (z @ state)).real) / 2
>>>
>>> encoded_densities = [density(evolved_encoded, j, num_qubits) for j in range(num_sites)]
>>> reference_densities = [density(evolved_reference, j, num_sites) for j in range(num_sites)]
>>>
>>> for j, density_value in enumerate(encoded_densities):
... print(f"site {j}: {density_value:.10f}")
site 0: 0.6750993356
site 1: 0.3689791258
site 2: 0.6750993356
site 3: 0.3818130396
site 4: 0.4868585996
site 5: 0.3818130396
site 6: 0.2693544696
site 7: 0.4916285849
site 8: 0.2693544696
>>> np.allclose(encoded_densities, reference_densities, atol=1e-12)
True
هذه هي الكثافات المرمَّزة؛ وهي تطابق مرجع Jordan-Wigner بدقة الآلة، على شبكة لا يكون ترميزها أميناً إلا بعد الإسقاط.
10. قياس تقارب Trotter
الخطوة 8 أثبتت أن التركيبين يتفقان فيما بينهما، وهذا كلام عن التكافؤ لا عن الدقة: كلاهما يصف خطوة Trotter واحدة نفسها، فكلاهما يحمل خطأ Trotter نفسه. قياس ذلك الخطأ يحتاج مرجعاً دقيقاً، والخطوة 9 بنت واحداً للتو.
عدد الخطوات يأتي من FermionicLieTrotter، النظير الفرميوني للصيغة على مستوى الكيوبت المستخدمة كخط أساس سابقاً. عند تطبيقها على المؤثر المجمَّع كله قبل التفكيك، يتنقل تكرارها بين مجموعات التدفق الأربع والمجموعة القطرية بدلاً من التكرار داخل أي منها:
>>> initial_state = project_onto_codespace(
... basis_state(occupation, num_qubits), plaquette_stabilizers
... )
>>> exact = expm_multiply(
... -1j * total_time * to_sparse_matrix(encoded), initial_state
... )
>>>
>>> def trotter_fidelity(steps):
... # `steps` goes to the fermionic formula inside transpile_trotter_step, so the
... # repetition cycles between the flow sets rather than within any one of them
... circuit = transpile_trotter_step(
... rows, cols, FlowSetSynthesis(rows, cols), steps=steps
... )
... evolved = Statevector(initial_state).evolve(circuit.decompose(reps=6))
... return abs(np.vdot(evolved.data, exact)) ** 2
>>>
>>> for steps in (1, 2, 4, 8):
... print(f"{steps} step(s): fidelity {trotter_fidelity(steps):.6f}")
1 step(s): fidelity 0.053820
2 step(s): fidelity 0.704014
4 step(s): fidelity 0.932937
8 step(s): fidelity 0.982687
تتصاعد الدقة (fidelity) رتابةً نحو واحد مع قصر الخطوات، وهو السلوك الذي يجب أن تظهره أي صيغة منتَج. تقصير الخطوات ليس إلا أحد مقبضين؛ FermionicSuzukiTrotter توازن بينهما عند رتبة أعلى، كما يقيس تركيب بوابة Evolution في الفضاء الفرميوني.
خطوة واحدة من هذا الهاميلتوني عند تقريب ضعيف (مجموعات القفز الأربع والتفاعل لا تتبادل) لذا الصف الأول بعيد عن واحد، وهذا هو المقصود: التقارب هو ما يؤكد البناء، لا أي صف منفرد.
كل عامل في الجدول الزمني يمتد على كل أوضاع هذه الشبكة، لذا فتضييق العامل إلى دعمه لا يغيّر شيئاً هنا، وتبقى فهارس الكيوبتات المطلقة التي تطابقها FlowSetSynthesis كما هي. تركيب مكتوب لمؤثر تمس مجموعاته جزءاً فقط من السجل سيتعين عليه أن يراعي إعادة التسمية تلك.
ما نخرج به
-
البعدان يغيّران المحاسبة، لا الوصفة. كل مكوّن من دليل البعد الواحد ينتقل دون تعديل:
groupsللتجميع، وmap_transfer_vertex_generators()للترميز، وCustomF2QLayoutللسجل غير المتطابق، وMapperFnEvolutionSynthesisلربطه بالـ Transpiler. ما كبر هو عدد الحالات داخل كل منها. -
الانفصال هو ما يجعل البعدين بعمق ثابت. مجموعة التدفق تتفكك إلى خطوط منفصلة الرؤوس، لذا تُصدَر كليفورداتها بالتوازي وتتشارك طبقتي كيوبتين مهما كبرت الشبكة. أربع مجموعات تدفق ومجموعة قطرية تعطي ميزانية ثابتة: هنا لمجموعات التدفق الأربع زائد لمجموعة التفاعل القطرية.
-
كل اتجاه هو شكل Pauli خاص به. تحوّل
XXZإلىYYZوXYXYإلىYXXY، لذا تحتاج مجموعات التدفق الأربع إلى أربعة كليفوردات. افتراض أن كليفورداً واحداً يغطي اتجاهي محور هو أسهل طريقة لتخطئ في تركيب مجموعات التدفق ثنائي الأبعاد. -
اقرأ الدعوم من الاقتران. في البعد الواحد، تعطي طبقة
CZواحدة الصور بصيغة مغلقة. في البعدين لا توجد صيغة كهذه، وخطوط الأعمدة لها مساعدات غير متجاورة، لذا فالتخمين اليدوي لمكان هبوط كل حد هش. الاقتران والفحص يكلّفان بناءCliffordواحداً ولا يمكن أن يكونا خاطئين؛ كما يعني أن كليفورداً صالحاً بالقدر نفسه يهبط بالحدود في مكان آخر يستمر في العمل. -
دع التجميع يتولى الفرز. بما أن
Evolutionتقسّم نفسها لكل مجموعة، يُسلَّم التركيب مجموعة تدفق واحدة في كل مرة ولا يضطر أبداً إلى إعادة اشتقاق التقسيم: يقرأ شكل Pauli واحداً من الحد الأول ويختار الكليفورد المطابق. افتراض ذلك، وإطلاق خطأ عندما لا يصح، هو ما يُبقيsynthesizeقصيرة. -
عدد الخطوات يقع فوق التقسيم. التركيب هنا يرى مجموعة تدفق واحدة لكل بوابة، فلا يستطيع تقسيم التطور عبر المجموعات.
FermionicLieTrotterتفعل ذلك في الفضاء الفرميوني، قبل التفكيك، وهذا ما يجعل اختبار التقارب ممكناً أصلاً. -
تحقق على الفضاء الجزئي. ترميز بـ كيوبت لـ وضعاً أمين فقط حيث تقول المثبِّتات ذلك. وبما أن VC يُبقي ، فأرخص فحص موثوق هو إسقاط حالة جداء ومقارنة الكثافات: دون مبدّل متشابك ودون مطابقة أطياف، وهي غير موثوقة عندما يكون الهاميلتوني المقيَّد منحلاً.
الهاميلتوني المرمَّز يتبادل مع المثبِّتات، لذا لا يلزم تطبيق الإسقاط إلا على الحالة الابتدائية. التطور يحفظ الفضاء الجزئي من تلقاء نفسه. وعلى العتاد الحقيقي هذا أيضاً ما يجعل الترميز مفيداً كمورد لكشف الأخطاء: يمكن قياس المثبِّتات.
التالي
هناك اتجاهان يوسّعان ما سبق، وكلاهما تغيير في المكوّن لا في الطريقة. الأول هو الترميز: اخترنا Verstraete-Cirac هنا لأن المرجع [1] يعطي كليفوردات قطرنته بصيغة مغلقة، لكن Derby-Klassen ينفق كيوبتاً مساعداً واحداً لكل خلية فردية بدلاً من واحد لكل موقع، لنسبة كيوبت إلى وضع تبلغ بدلاً من . مؤثرات النقل فيه لها أشكال Pauli مختلفة، لذا فالنقل إليه يعني إعادة كتابة دالة الترميز في الخطوة 3 وإيجاد الكليفورد لكل شكل جديد؛ أما التجميع والتخطيط وهيكل التركيب فتنتقل دون مساس.
الثاني هو الشبكة نفسها. كل ما هنا يفترض حدوداً مفتوحة ونوعاً فرميونياً واحداً. الحدود الدورية تغلق كل خط في حلقة: الأسهم تتبادل كلها بعد، لأن كلاً منها يلتقي بالتالي رأساً بذيل، لكن شكلي كليفورد الخطوة 5 مبنيان لخط له طرفان. حلقة من موقعاً تحمل رابطاً لكل اتجاه حيث يحمل الخط ، لذا لا مكان لرابط الالتفاف في البناء ويحتاج معالجة خاصة به. النموذج ذو السبين أسهل: يضاعف السجل ويضيف حدود تفاعل في الموقع إلى المجموعة القطرية، دون المساس بمجموعات التدفق. لا أي منهما يزعزع حجة العمق الثابت، لكن كليهما يغيّر المحاسبة، التي هي، كما تقول الخلاصة الأولى، الجزء الذي يكبّره البعدان.
اتصالية العتاد غير معالَجة هنا. كل دارة أعلاه مبنية على سجل مجرد يمكن فيه لأي زوج من الكيوبتات أن يتفاعل، والأعماق المذكورة محسوبة في ذلك الإعداد. للأجهزة الحقيقية خريطة اقتران مقيَّدة، وتعيين هذا الترميز عليها مشكلة كبيرة بحد ذاتها: سجل VC رسم بياني من كيوبت تجري بواباته بين موقع ومساعده، وبين مواقع متجاورة، وبين مساعدات متجاورة، لذا يجب على التخطيط إبقاء الأنواع الثلاثة من الأزواج متجاورة في آن واحد. إذا تعذر ذلك، يُدخل الـ Transpiler بوابات SWAP، والطبقات التي تضيفها هي ما كان بناء العمق الثابت يستردّه. المرجع [1] كذلك يذكر رقم عمقه تحت رسم اتصال مربع مفترض مع CX كمُشبِك أصلي وحيد. إيجاد تخطيطات واستراتيجيات توجيه جيدة للترميزات المحلية على طوبولوجيا جهاز معيّن خارج نطاق هذا الدليل.
A. Gandon, S. Piccinelli, M. Rossmannek, F. Tacchino, A. Baiardi, J. Nys, and I. Tavernelli, Stabilizer-based quantum simulation of fermion dynamics with local qubit encodings (2026), arXiv:2512.11418v2.
F. Verstraete and J. I. Cirac, Mapping local Hamiltonians of fermions to local Hamiltonians of spins, J. Stat. Mech. 2005, P09012 (2005).
Y.-A. Chen and Y. Xu, Equivalence between fermion-to-qubit mappings in two spatial dimensions, PRX Quantum 4, 010326 (2023).