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

توليد دوائر التطور الزمني لكريلوف (SKQD)

مهم

المفاهيم في هالدليل متاحة حاليًا فقط في Python API. راح تتوفر وظائف مكافئة في C API في إصدار قادم.

التقطير القطري الكمّي لكريلوف المعتمد على العيّنات (SKQD) هو صيغة متمحورة حول الحوسبة الكمّية من التقطير القطري للفضاء الجزئي. بدل أخذ عيّنات من Ansatz تباديلي واحد (كما في التقطير القطري الكمّي المعتمد على العيّنات العادي (SQD)) أو من مجموعة دوائر تطور زمني عشوائية (كما في SqDRIFT)، يأخذ عيّنات من أساس كريلوف: عائلة الحالات

∣ψk⟩=e−ik Δt H∣ψ0⟩,k=0,1,…,D−1,\lvert \psi_k \rangle = e^{-i k \, \Delta t \, H} \lvert \psi_0 \rangle, \qquad k = 0, 1, \dots, D - 1,

المتحصّل عليها بتطوير حالة مرجعية ∣ψ0⟩\lvert \psi_0 \rangle لمضاعفات متزايدة من خطوة زمنية ثابتة Δt\Delta t. السلاسل الثنائية (bitstrings) المأخوذة من هالدوائر الـ DD تملأ فضاء كريلوف الجزئي اللي يُقطَّر فيه الهاملتونيان لاحقًا بشكل كلاسيكي.

يعيد هالدليل إنتاج خطوة بناء الدوائر (circuits) في خوارزمية SKQD لنموذج أندرسون أحادي الشائبة (SIAM)، وهو شائبة مغناطيسية مقترنة بحمّام غير متفاعل، وذلك وفق منشور SKQD.

ملاحظة

خطوة أخذ العيّنات في SKQD تشتغل فقط إذا كانت الحالة الأرضية متناثرة في الأساس الحسابي: لازم يقدر التقطير القطري الكلاسيكي يعيد بناءها من عدد معقول من السلاسل الثنائية المأخوذة. وكما هو موضح تحت، هالتناثر مو تلقائي؛ هو خاصية للأساس أحادي الجسيم اللي تُصاغ فيه المسألة. اختيار الأساس الصحيح هو لبّ هذا البناء.

1. بناء هاملتونيان SIAM كمؤثر فرميوني​

يضع نموذج أندرسون أحادي الشائبة مدارًا واحدًا متفاعلًا للشائبة (الفهرس 00) على تماس مع حمّام غير متفاعل من LL موقعًا مرتبة كسلسلة. ينقسم هاملتونيانه إلى جزء أحادي الجسم H1H_1 وجزء ثنائي الجسم H2H_2،

H=H1+H2,H = H_1 + H_2,

حيث يجمع الجزء أحادي الجسم طاقة الشائبة الموقعية (الجهد الكيميائي μ\mu)، وتهجين الشائبة-الحمّام VV، وقفز الحمّام بين الجيران الأقرب tt،

H1=μ∑σa0σ†a0σ−V∑σ(a0σ†a1σ+h.c.)−t∑σ∑i=1L−1(aiσ†ai+1,σ+h.c.),H_1 = \mu \sum_\sigma a^\dagger_{0\sigma} a_{0\sigma} - V \sum_\sigma \left( a^\dagger_{0\sigma} a_{1\sigma} + \text{h.c.} \right) - t \sum_{\sigma} \sum_{i=1}^{L-1} \left( a^\dagger_{i\sigma} a_{i+1,\sigma} + \text{h.c.} \right),

ويحمل الجزء ثنائي الجسم تنافر كولوم الموقعي UU على الشائبة وحدها،

H2=U a0↑†a0↑a0↓†a0↓.H_2 = U \, a^\dagger_{0\uparrow} a_{0\uparrow} a^\dagger_{0\downarrow} a_{0\downarrow}.

يعمل هالدليل عند النقطة المتناظرة جسيم-ثقب μ=−U/2\mu = -U/2. ويقابل هذان الجزءان مباشرةً التكاملات أحادية الجسم h1e (قفز الحمّام، وتهجين الشائبة، وطاقة الشائبة الموقعية) والتكامل ثنائي الجسم الوحيد h2e (حد كولوم للشائبة). أولًا نجمّعهما في أساس الموضع، حيث يُعرَّف النموذج بشكل طبيعي.

>>> import numpy as np
>>>
>>> norb = 8
>>> nelec = (norb // 2, norb // 2) # half filling
>>> hopping, onsite, hybridization = 1.0, 10.0, 1.0
>>> chemical_potential = -0.5 * onsite
>>>
>>> # one-body integrals: bath hopping, impurity hybridization, and impurity on-site energy
>>> h1e = np.zeros((norb, norb))
>>> np.fill_diagonal(h1e[:, 1:], -hopping)
>>> np.fill_diagonal(h1e[1:, :], -hopping)
>>> h1e[0, 1] = h1e[1, 0] = -hybridization
>>> h1e[0, 0] = chemical_potential
>>>
>>> # two-body integrals: on-site Coulomb repulsion on the impurity orbital
>>> h2e = np.zeros((norb,) * 4)
>>> h2e[0, 0, 0, 0] = onsite

2. التغيير إلى أساس الزخم​

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

>>> def momentum_basis(norb):
... """Orbital rotation from the position to the momentum basis."""
... n_bath = norb - 1
... hopping_matrix = np.zeros((n_bath, n_bath))
... np.fill_diagonal(hopping_matrix[:, 1:], -1)
... np.fill_diagonal(hopping_matrix[1:, :], -1)
... _, vecs = np.linalg.eigh(hopping_matrix)
... orbital_rotation = np.zeros((norb, norb))
... orbital_rotation[0, 0] = 1.0
... orbital_rotation[1:, 1:] = vecs
... new_index = n_bath // 2
... perm = np.r_[1:(new_index + 1), 0, (new_index + 1):norb]
... return orbital_rotation[:, perm]
...
>>> orbital_rotation = momentum_basis(norb)

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

>>> rotation = orbital_rotation.T.conj()
>>> h1e_momentum = np.einsum("ab,Aa,Bb->AB", h1e, rotation, rotation.conj(), optimize="greedy")
>>> h2e_momentum = np.einsum(
... "abcd,Aa,Bb,Cc,Dd->ABCD",
... h2e,
... rotation,
... rotation.conj(),
... rotation,
... rotation.conj(),
... optimize="greedy",
... )

بما أن التدوير يترك مدار الشائبة ثابتًا (يؤثر فقط على الحمّام)، يبقى التفاعل الموقعي من غير تغيير. يبقى h2e_momentum حدًا واحدًا من نوع عدد-عدد، تمامًا مثل h2e. التغيير الوحيد هو أن الشائبة نُقلت إلى فهرس مركزي. وهذا يظهر مباشرةً في موترات التكاملات، اللي يحمل كل منها مدخلًا واحدًا غير صفري:

>>> print(f"h2e nonzero at: {np.argwhere(np.abs(h2e) > 1e-12).tolist()}")
h2e nonzero at: [[0, 0, 0, 0]]
>>> print(f"h2e_momentum nonzero at: {np.argwhere(np.abs(h2e_momentum) > 1e-12).tolist()}")
h2e_momentum nonzero at: [[3, 3, 3, 3]]

هالموضعية هي اللي تخلي تركيب التفاعل رخيصًا لاحقًا.

جمّع التكاملات أحادية الجسم في FermionOperator باستخدام مُنشئ التكاملات الإلكترونية from_1body_tril_spin_sym()، اللي يتوقع أن تكون معبّأة بترتيب المثلث السفلي. الجزء ثنائي الجسم هو حد واحد من نوع عدد-عدد، فبدل تمريره عبر آلية التكاملات ثنائية الجسم، ابنيه مباشرةً بـ cre()/ann(): مؤثر كولوم الموقعي U np↑np↓U \, n_{p\uparrow} n_{p\downarrow} لنمط الشائبة pp (نمطه ↑\uparrow عند الفهرس p، ونمطه ↓\downarrow عند الفهرس p + norb في تخطيط كتل السبين). كلا المُنشئين يضاعفان السبين للمدارات الفراغية norb إلى 2 * norb نمطًا فرميونيًا.

أبقِ الجزء أحادي الجسم H1H_1 والجزء ثنائي الجسم H2H_2 كمؤثرين منفصلين بدل جمعهما مباشرةً، بما يطابق الحدين H1H_1 وH2H_2 في معادلات القسم 1. بناء كل جزء مرة وحدة (في الأساسين) يخليهما قابلين لإعادة الاستخدام تحت. عند جمعهما، يعطيان الهاملتونيان الكامل للتقطير القطري الدقيق في أي من الأساسين. أما منفردًا، فـ H2H_2 (في أساس الزخم) هو المؤثر اللي تطوّره خطوة Trotter في الخطوة 4.

>>> from qiskit_fermions.operators import FermionOperator, ann, cre
>>>
>>> # U n_imp_up n_imp_down: the impurity's up/down modes sit at `imp` and `imp + norb` in the
>>> # block-spin layout (the impurity is orbital 0 in the position basis, orbital 3 in the
>>> # momentum basis -- the single relocated index found above)
>>> imp_pos, imp_mom = 0, 3
>>>
>>> one_body_position = FermionOperator.from_1body_tril_spin_sym(
... h1e[np.tril_indices(norb)], norb
... )
>>> two_body_position = FermionOperator.from_dict(
... {(cre(imp_pos), ann(imp_pos), cre(imp_pos + norb), ann(imp_pos + norb)): onsite}
... )
>>> one_body_momentum = FermionOperator.from_1body_tril_spin_sym(
... h1e_momentum[np.tril_indices(norb)], norb
... )
>>> two_body_momentum = FermionOperator.from_dict(
... {(cre(imp_mom), ann(imp_mom), cre(imp_mom + norb), ann(imp_mom + norb)): onsite}
... )
>>>
>>> hamiltonian_position = (
... (one_body_position + two_body_position).normal_ordered().simplify(atol=1e-14)
... )
>>> hamiltonian_momentum = (
... (one_body_momentum + two_body_momentum).normal_ordered().simplify(atol=1e-14)
... )

تغيير الأساس هو تحويل تشابه وحدوي، فما يغيّر الطيف. تأكد أن المؤثر صحيح بمقارنة أدنى قيمة ذاتية له في القطاع (norb,nelec)(\text{norb}, \text{nelec}) مع تقطير قطري دقيق. يوفّر FermionOperator كائن SciPy من نوع LinearOperator (مدعومًا بنواة أصلية لضرب المصفوفة في المتجه للتفاعل التشكيلي الكامل (FCI))، فتقدر تمرره مباشرةً إلى scipy.sparse.linalg.eigsh().

>>> import ffsim
>>> import scipy.sparse.linalg
>>>
>>> linop = ffsim.linear_operator(hamiltonian_momentum, norb, nelec)
>>> reference_energy, ground_state = scipy.sparse.linalg.eigsh(linop, k=1, which="SA")
>>> reference_energy = reference_energy[0]
>>> ground_state = ground_state[:, 0]
>>> print(f"exact ground-state energy: {reference_energy:.6f}")
exact ground-state energy: -13.422492

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

>>> def determinants_for(vec, fraction=0.99):
... weights = np.sort(np.abs(vec) ** 2)[::-1]
... return int(np.searchsorted(np.cumsum(weights), fraction) + 1)
...
>>> # the same Hamiltonian in the position basis has an identical spectrum, but a dense ground state
>>> position_linop = ffsim.linear_operator(hamiltonian_position, norb, nelec)
>>> position_energy, position_ground = scipy.sparse.linalg.eigsh(position_linop, k=1, which="SA")
>>>
>>> print(f"position-basis energy: {position_energy[0]:.6f}")
position-basis energy: -13.422492
>>> print(f"determinants for 99% weight: {determinants_for(position_ground[:, 0])} (position)")
determinants for 99% weight: 2563 (position)
>>> print(f"determinants for 99% weight: {determinants_for(ground_state)} (momentum)")
determinants for 99% weight: 21 (momentum)

المؤثران لهما نفس الطاقة، لكن الحالة الأرضية في أساس الموضع منتشرة على آلاف المحددات، بينما يركّزها أساس الزخم على حوالي 20، وهذا سبب اشتغال أخذ عيّنات SKQD في أساس الزخم وليس في أساس الموضع.

3. تحضير الحالة المرجعية​

مرجع SKQD ∣ψ0⟩\lvert \psi_0 \rangle هو تراكب كل إثارات الإلكترونات الأقرب إلى مستوى فيرمي إلى أقرب أنماط زخم فارغة. منشور SKQD يبنيه بشبكة مكتوبة يدويًا من XXPlusYYGate. تبدو الحالة شديدة الترابط (لها مئات السعات غير الصفرية في الأساس الحسابي)، لكن شبكة من التدويرات ثنائية النمط الحافظة للعدد هي عملية أحادية الجسيم (فرميونية غاوسية)، فالحالة الناتجة هي محدد سلاتر واحد. يعني تقدر تحضّرها ببوابة (gate) PrepareSlaterDeterminant واحدة، بمعطى التدوير أحادي الجسيم اللي تنفّذه الشبكة.

بما أن كل XXPlusYYGate تعمل كتدوير غيفنز على زوج من الأنماط، يمكن بناء هذا التدوير مباشرةً في تصوّر الجسيم الواحد (مصفوفة غيفنز واحدة لكل بوابة) بدل إصدار البوابات ثنائية الكيوبت (qubit) يدويًا. تشكّل البوابات نسيجًا طوبيًا من تدويرات الجيران الأقرب ينتشر من مستوى فيرمي في طبقات متوسعة، فيوزّع كل إلكترون قريب من مستوى فيرمي على الأنماط الفارغة فوقه مباشرةً:

>>> def fermi_level_rotation(norb, nocc, n_excited=3):
... """Orbital rotation exciting the near-Fermi electrons into the nearest empty modes."""
... cos, sin = np.cos(np.pi / 4), np.sin(np.pi / 4)
... fermi = nocc - 1 # highest occupied mode; the Fermi level sits between it and mode `nocc`
... # Nearest-neighbor Givens rotations fanning out from the Fermi level in expanding layers:
... # layer `depth` couples the mode `depth` steps below the level to the one above it, mixing
... # the `n_excited` highest-occupied modes with the empty modes just above them. A final
... # rotation carries amplitude up into the topmost empty mode of the window.
... network = [
... (lower, lower + 1)
... for depth in range(n_excited)
... for lower in range(fermi - depth, fermi + depth + 1, 2)
... ]
... network.append((fermi + n_excited, fermi + n_excited + 1))
... unitary = np.eye(norb, dtype=complex)
... for i, j in network:
... givens = np.eye(norb, dtype=complex)
... givens[i, i] = givens[j, j] = cos
... givens[i, j], givens[j, i] = sin, -sin
... unitary = givens @ unitary
... return unitary
...
>>> reference_rotation = fermi_level_rotation(norb, nelec[0])
ملاحظة

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

يتم تحضير محدد سلاتر لكل قطاع سبين على حدة، لأن لكل قطاع شكله الخاص (إلكترونات، مدارات). بوابة (gate) PrepareSlaterDeterminant واحدة (إشغال، أي ملء أدنى nocc أنماط، مع التدوير أحادي الجسيم) تحضّر قطاعًا واحدًا؛ وتُطبَّق نفس البوابة مرة على أنماط ألفا 0..norb ومرة على أنماط بيتا norb..2*norb، لأن قطاعي السبين يتشاركان هنا نفس الإشغال المرجعي والتدوير:

>>> from qiskit_fermions.circuit.library import PrepareSlaterDeterminant
>>>
>>> occupation = [i < nelec[0] for i in range(norb)]
>>> reference_preparation = PrepareSlaterDeterminant(occupation, reference_rotation)

هالبوابة (gate) التصريحية تحل محل شبكة XXPlusYYGate المكتوبة يدويًا؛ والـ Transpiler هو اللي يولّد تركيب تدويرات غيفنز.

4. تجميع دوائر كريلوف​

كل دائرة (circuit) كريلوف هي تحضير الحالة المرجعية متبوعًا بالتطور تحت e−ik Δt He^{-i k \, \Delta t \, H}. قسّم الهاملتونيان إلى جزأيه أحادي الجسم وثنائي الجسم، H=H1+H2H = H_1 + H_2، وطبّق تقسيم Trotter للتطور على هالانقسام. هذا هو الاختيار الحاسم لعمق الدائرة. التطور أحادي الجسم e−itH1e^{-i t H_1} هو عملية فرميونات حرة (فرميونية غاوسية)، فهو تدوير أساس أحادي الجسيم (بوابة OrbitalRotation بالمصفوفة الوحدوية e−it h1e^{-i t \, \mathbf{h}_1}) بدل أن يُطبَّق عليه تقسيم Trotter بباولي حدًا حدًا. أما التطور ثنائي الجسم e−itH2e^{-i t H_2} فيؤثر فقط على حد التفاعل الموقعي الوحيد. بناء الخطوة بهالطريقة يخلي الـ Transpiler يُصدر نسيجًا طوبيًا ضحلًا من تدويرات غيفنز مع بوابة تفاعل واحدة ثنائية الكيوبت، وهي البنية الضحلة اللي يبنيها منشور SKQD يدويًا.

عندنا الجزء ثنائي الجسم أصلًا. هو المؤثر two_body_momentum المبني في الخطوة 2 (الحد الموقعي الوحيد). التطور أحادي الجسم e−itH1e^{-i t H_1} لا يعتمد على السبين، فهو بوابة OrbitalRotation واحدة لكل قطاع بالمصفوفة الوحدوية e−it h1e^{-i t \, \mathbf{h}_1} تُطبَّق مرة على كل قطاع سبين، بنفس البنية المتوازية لتحضير الحالة المرجعية.

كل دائرة (circuit) كريلوف تتطور لزمن كلي k Δtk \, \Delta t. ويتحقق ذلك بجداء Trotter من الدرجة الثانية من kk خطوة بحجم Δt\Delta t، فيبقى خطأ كل خطوة ثابتًا مع نمو بُعد كريلوف. كل خطوة تضع دوران أحادي الجسم كاملًا (واحدًا لكل قطاع سبين) بين نصفي خطوة من التطور ثنائي الجسم (الرخيص، ذي الحد الواحد). بُعد كريلوف DD هو عدد هالدوائر، مع dim يمتد من 0 (الحالة المرجعية فقط) إلى D−1D - 1.

>>> import scipy.linalg
>>>
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution, OrbitalRotation
>>>
>>> num_modes = 2 * norb
>>> time_step = 0.2
>>>
>>> half_exp_h2 = Evolution(num_modes, two_body_momentum, time_step / 2)
>>> full_exp_h1 = OrbitalRotation(scipy.linalg.expm(-1j * time_step * h1e_momentum))
>>>
>>> def krylov_circuit(dim):
... # closes over the fermionic gates defined in the outer scope:
... # `reference_preparation`, `half_exp_h2` and `full_exp_h1`
... circuit = FermionicCircuit(num_modes)
... circuit.append(reference_preparation, range(norb))
... circuit.append(reference_preparation, range(norb, num_modes))
... for _ in range(dim): # second-order Trotter product of `dim` steps
... circuit.append(half_exp_h2, circuit.modes)
... circuit.append(full_exp_h1, range(norb))
... circuit.append(full_exp_h1, range(norb, num_modes))
... circuit.append(half_exp_h2, circuit.modes)
... return circuit
ملاحظة

الدوائر (circuits) المبنية هنا ما فيها قياسات بعد، لأن نفس الدوائر الفرميونية تقود كمان المحاكاة الدقيقة في الخطوة 6، والقياس ليس عملية وحدوية. الخطوة 5 تضيفها بـ FermionicCircuit.measure_all()، اللي تقرأ النمط i في البت الكلاسيكي i.

فضّل قياس الدائرة الفرميونية على الدائرة المحوَّلة بالـ Transpiler. القياس الفرميوني يسمّي النمط اللي يقرأه، فيحمله التخطيط والتوجيه مع الكيوبت (qubit) اللي انتهى عنده ذلك النمط، وتكون السلسلة الثنائية المأخوذة مفهرسة بالنمط. أما قياس الدائرة المحوَّلة بدلًا من ذلك فيتركك تتولى بنفسك التراجع عن final_index_layout()، ويعيد نتائج مبدّلة الترتيب بصمت إذا نسيت. وهذا يعتمد على ترميز جوردان-فيغنر بأساس الإشغال اللي يستخدمه الإعداد المسبق، حيث يحمل كل نمط كيوبت (qubit) واحد.

5. التحويل بالـ Transpiler إلى دوائر كيوبتات​

تحويل FermionicCircuit بالـ Transpiler يربط بواباتها (gates) الفرميونية ببوابات الكيوبتات (qubits). هذا يستخدم ربط جوردان-فيغنر jordan_wigner(). بوابات OrbitalRotation تُركَّب إلى نسيج طوبي من تدويرات غيفنز، وبوابة Evolution ذات الحد الواحد تُخفَّض إلى بوابة تفاعل واحدة ثنائية الكيوبت.

بما أن لكل بوابة فرميونية هنا تركيبًا افتراضيًا، تقدر تستخدم مباشرةً generate_preset_jw_pass_manager() الجاهز بدل تجميع المراحل يدويًا. أي وسائط بكلمات مفتاحية تُمرَّر إلى مرحلة الكيوبت؛ مرر optimization_level=0 للحصول على صورة أمينة وغير محسّنة للعمق المركَّب. توليد العائلة الكاملة من دوائر كريلوف هو حينها حلقة على بُعد كريلوف. احتفظ بالدوائر FermionicCircuit غير المحوَّلة جنب دوائر الكيوبتات (qubits) المحوَّلة. الفرميونية منها تقود المحاكاة الدقيقة (بمتجه الحالة) في الخطوة 6، فلازم تبقى بلا قياسات؛ وmeasure_all(inplace=False) تعيد نسخة مقيسة لتحويلها وتترك الأصل من غير تغيير، وهذه النسخة هي اللي ينفّذها الـ Backend.

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> pass_manager = generate_preset_jw_pass_manager(optimization_level=0)
>>>
>>> def krylov_circuits(krylov_dim):
... fermionic_circuits, circuits = [], []
... for dim in range(krylov_dim):
... fermionic_circuit = krylov_circuit(dim)
... fermionic_circuits.append(fermionic_circuit)
... measured = fermionic_circuit.measure_all(inplace=False)
... circuits.append(pass_manager.run(measured))
... return fermionic_circuits, circuits
...
>>> krylov_dim = 5
>>> fermionic_circuits, circuits = krylov_circuits(krylov_dim)

يظهر بُعد كريلوف مباشرةً في عمق الدوائر ثنائي الكيوبت. تحضير الحالة المرجعية كلفة ثابتة، وكل قوة إضافية لكريلوف تضيف خطوة Trotter أخرى من الدرجة الثانية لمؤثر التطور الزمني.

>>> two_qubit = lambda instr: instr.operation.num_qubits == 2
>>> [circuit.depth(two_qubit) for circuit in circuits]
[4, 12, 21, 30, 39]
ملاحظة

تقسيم الهاملتونيان يبقي هالأعماق متواضعة. التطور أحادي الجسم هو تدوير مداري حقيقي، فيُركَّب إلى نسيج طوبي من XXPlusYYGate على كيوبتات (qubits) متجاورة. كل نصف خطوة من التفاعل الموقعي هو RZZGate واحدة تقرن نمطي سبين الشائبة؛ وهو الحد الرخيص اللي يضعه الجداء من الدرجة الثانية حول التدوير. أما تسليم الهاملتونيان كاملًا لبوابة Evolution واحدة فكان راح يطبّق تقسيم Trotter بباولي على الجزء أحادي الجسم حدًا حدًا، وينشر سلاسل Z طويلة من جوردان-فيغنر عبر اقترانات الشائبة-الحمّام (بعيدة المدى)، ويضخّم العمق بمرتبة من المقدار.

الدائرة الأولى تحضّر المحدد المرجعي فقط، بينما الأخيرة تحمل أكبر عدد من خطوات Trotter (أربع في هالحالة):

>>> circuits[0].draw("mpl", fold=-1, measure_arrows=False)
<Figure size ... with 1 Axes>

The reference-state preparation circuit (Krylov dimension 0).

>>> circuits[-1].draw("mpl", fold=-1, measure_arrows=False)
<Figure size ... with 1 Axes>

A second-order Trotter Krylov circuit with four evolution steps.

6. أخذ عيّنات من السلاسل الثنائية من دوائر كريلوف​

على العتاد، كنت راح تشغّل الدوائر (circuits) المحوَّلة وتقيس. هذه هي النسخة الخالية من الضوضاء (بمتجه الحالة) من تلك الخطوة، وتبيّن السلاسل الثنائية اللي كان SKQD راح يجمعها. حاكِ كل FermionicCircuit غير محوَّلة بـ ffsim.apply_unitary() وخذ عيّنات من متجه الحالة الناتج بـ ffsim.sample_state_vector().

بوابة (gate) PrepareSlaterDeterminant تتحقق ثم تدوّر تحت المحاكاة (تفحص أن الحالة الداخلة تشغل محددها المرجعي، ثم تطبّق التدوير). لذلك المدخل هو محدد الإشغال البسيط: أدنى nocc أنماط مملوءة لكل سبين، من غير تدوير. البوابة تطبّق reference_rotation بنفسها.

>>> from collections import Counter
>>>
>>> reference = ffsim.slater_determinant(
... norb, (list(range(nelec[0])), list(range(nelec[1])))
... )
>>>
>>> shots = 1000
>>> counts = Counter()
>>> for dim, fermionic_circuit in enumerate(fermionic_circuits):
... statevector = ffsim.apply_unitary(reference, fermionic_circuit, norb=norb, nelec=nelec)
... samples = ffsim.sample_state_vector(
... statevector, norb=norb, nelec=nelec, shots=shots, seed=dim
... )
... counts += Counter(samples)

عند تجميع العيّنات من كل دوائر كريلوف الخمس، الدعم صغير جدًا: بضع مئات من السلاسل الثنائية المتمايزة من أصل (84)2=4900\binom{8}{4}^2 = 4900 محددًا في القطاع (4, 4). هذا التركّز، وهو نتيجة مباشرة لتناثر أساس الزخم المثبت في الخطوة 2، يجعل التقطير القطري الكلاسيكي اللاحق قابلًا للتنفيذ.

رسم العدّادات باستخدام qiskit.visualization.plot_histogram() يجعل التناثر (sparsity) واضحًا. عدد قليل من التكوينات يهيمن على النتائج، وعلى رأسها المحدِّد المرجعي.

>>> from qiskit.visualization import plot_histogram
>>>
>>> plot_histogram(dict(counts), number_to_keep=25)
<Figure size ... with 1 Axes>

Histogram of the bitstrings sampled from the Krylov circuits, showing a small support.

7. التشخيص القطري في الفضاء الجزئي المُعاين​

السلاسل الثنائية المُعاينة هي مدخلات النصف الكلاسيكي من SKQD. يُسقَط الهاميلتوني على الفضاء الجزئي الذي تمتدّ عليه التكوينات المُعاينة ثم يُشخَّص قطريًا فيه. يتم تمرير ذلك إلى حزمة qiskit-addon-sqd، التي تشغّل دالتها diagonalize_fermionic_hamiltonian() حلقة التشخيص القطري الكمومي المعتمد على العيّنات بالكامل. تبني الدالة فضاءً جزئيًا من دفعات من التكوينات المُعاينة، وتشخّص الهاميلتوني قطريًا فيه، ثم تحسّن الفضاء الجزئي تكراريًا باستخدام استرداد التكوين: قلب الإشغالات لإصلاح التكوينات التي تنتهك تناظر عدد الجسيمات المعروف. صُمّم استرداد التكوين لإصلاح ضرر ضجيج العتاد الذي يُفسد السلاسل الثنائية المُعاينة فيدفعها إلى قطاع خاطئ من عدد الجسيمات؛ والمعاينة هنا خالية من الضجيج، فلا يوجد ما يحتاج إلى إصلاح من هذه الناحية.

يستهلك الحلّال العدّادات المُعاينة على شكل BitArray، مبنيّ مباشرة من counts المجمّعة في الخطوة 6. مرّر num_bits صراحةً: فبدونه يُستنتج العرض من أعرض سلسلة ثنائية مُعاينة، وهو أضيق من 2 * norb كلما لم تشغل أي عيّنة آخر نمط، وعندها يرفض الحلّال المصفوفة. يشخّص الحلّال قطريًا في أساس الزخم نفسه الذي يستخدمه باقي البناء، لذا فهو يأخذ التكاملات h1e_momentum وh2e_momentum من الخطوة 2 مباشرة، دون إعادة تعبئة. تطابق الطاقة المستردّة المرجع الدقيق بفارق أقل بكثير من ميلي هارتري، من بضع مئات فقط من التكوينات المُعاينة. ولأن طاقة SQD حدّ أعلى تغايري للحالة الأرضية الحقيقية، فهي دائمًا تساوي reference_energy أو أعلى منها (لا تزال في النطاق من الخطوة 2):

>>> from qiskit.primitives import BitArray
>>> from qiskit_addon_sqd.fermion import diagonalize_fermionic_hamiltonian
>>>
>>> bit_array = BitArray.from_counts(dict(counts), num_bits=2 * norb)
>>>
>>> # record each iteration's batch results so we can watch the energy converge
>>> result_history = []
>>> result = diagonalize_fermionic_hamiltonian(
... h1e_momentum,
... h2e_momentum,
... bit_array,
... samples_per_batch=100,
... norb=norb,
... nelec=nelec,
... num_batches=3,
... max_iterations=3,
... symmetrize_spin=True,
... callback=result_history.append,
... seed=np.random.default_rng(24),
... )
>>> print(f"SQD estimate: {result.energy:.4f} (exact: {reference_energy:.4f})")
SQD estimate: -13.4224 (exact: -13.4225)

صوّر التشغيل: تقارب الطاقة عبر التكرارات، ومتوسط الإشغال لكل مدار فراغي في الحالة الأرضية المستردّة. الطاقة لكل تكرار هي الأدنى بين دفعات ذلك التكرار، ويجمع الإشغال قيم orbital_occupancies عبر قطاعَي السبين. حتى بدون ضجيج لتصحيحه، تنخفض الطاقة من تكرار إلى آخر. يشخّص كل تكرار قطريًا ضمن num_batches من الدفعات، كل منها من samples_per_batch فقط من التكوينات المأخوذة عيّنةً من المجمّع، ويحسّن التنقيح التكراري الناتج عن ترحيل السلاسل الثنائية باستمرار أيّ التكوينات تقع في تلك الدفعات، فتتحسّن الطاقة المستردّة:

>>> import matplotlib.pyplot as plt
>>> from matplotlib.ticker import MaxNLocator
>>>
>>> min_energies = [min(batch, key=lambda r: r.energy).energy for batch in result_history]
>>> occupancies = np.sum(result.orbital_occupancies, axis=0)
>>>
>>> fig, axs = plt.subplots(1, 2, figsize=(12, 5))
>>> _ = axs[0].plot(range(len(min_energies)), min_energies, marker="o", label="SQD energy")
>>> _ = axs[0].axhline(reference_energy, color="gray", linestyle="--", label="exact energy")
>>> _ = axs[0].set_xlabel("iteration")
>>> _ = axs[0].set_ylabel("energy")
>>> _ = axs[0].set_title("Energy vs SQD iteration")
>>> _ = axs[0].xaxis.set_major_locator(MaxNLocator(integer=True))
>>> _ = axs[0].legend()
>>> _ = axs[1].bar(range(norb), occupancies)
>>> _ = axs[1].set_xlabel("spatial orbital")
>>> _ = axs[1].set_ylabel("average occupancy")
>>> _ = axs[1].set_title("Occupancy per spatial orbital")
>>> fig
<Figure size ... with 2 Axes>

SQD energy convergence and the average occupancy per spatial orbital.

بهذا تكتمل حلقة SKQD. تنتج دوائر أساس الزخم في الخطوات 1–5 عيّنة متناثرة (الخطوة 6)، ويحوّل التشخيص القطري المعتمد على العيّنات أعلاه تلك العيّنة إلى طاقة الحالة الأرضية.

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

شغّل هذا الدليل خط SKQD بأكمله على محاكي متجه الحالة الخالي من الضجيج. على العتاد، يتم تنفيذ وقياس circuits المحوَّلة بدلًا من معاينة متجه الحالة في الخطوة 6، وتُغذّى العدّادات الناتجة إلى تشخيص الخطوة 7 القطري دون تغيير. راجع توثيق Qiskit للمساعدة في تشغيل الدوائر، ودروس إضافة SQD لمعرفة المزيد عن المعالجة اللاحقة للتشخيص القطري في الفضاء الجزئي، بما في ذلك سلوكها على العيّنات الضجيجية، حيث يبذل استرداد التكوين أكبر جهد.

اقرأ دليل علاقة ffsim لتفهم لماذا تعمل ffsim.apply_unitary() وffsim.sample_state_vector() بشكل أصيل على الدوائر الفرميونية لهذه الحزمة، ولماذا يجعل قطاع عدد الجسيمات الثابت المستخدم في الخطوة 6 تناثر أساس الزخم المُثبَت في الخطوة 2 كفؤًا في المعاينة.