انتقل إلى المحتوى الرئيسي

مقدمة إلى البوابات الكسرية

تقدير الاستخدام: أقل من 30 ثانية على معالج Heron r2 (ملاحظة: هذا تقدير فحسب. قد يختلف وقت التشغيل الفعلي.)

الخلفية النظرية

البوابات الكسرية على وحدات المعالجة الكمومية من IBM

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

تدعم وحدات المعالجة الكمومية IBM Quantum® Heron البوابات الكسرية التالية:

  • RZZ(θ)R_{ZZ}(\theta) لقيم 0<θ<π/20 < \theta < \pi / 2
  • RX(θ)R_X(\theta) لأي قيمة حقيقية θ\theta

يمكن لهذه البوابات أن تُقلل بشكل ملحوظ من عمق الدوائر الكمومية ومدتها. وهي مفيدة بصفة خاصة في التطبيقات التي تعتمد بشكل كبير على RZZR_{ZZ} و RXR_X، مثل محاكاة هاميلتوني، وخوارزمية التحسين الكمومي التقريبي (QAOA)، وطرق النواة الكمومية. في هذا البرنامج التعليمي، نركز على النواة الكمومية كمثال تطبيقي عملي.

القيود

البوابات الكسرية ميزة تجريبية في الوقت الراهن وتأتي مع بعض القيود:

تتطلب البوابات الكسرية سير عمل مختلفاً مقارنةً بالنهج المعياري. يشرح هذا البرنامج التعليمي كيفية التعامل مع البوابات الكسرية من خلال تطبيق عملي.

راجع ما يلي للمزيد من التفاصيل حول البوابات الكسرية.

نظرة عامة

يتبع سير العمل الخاص باستخدام البوابات الكسرية بشكل عام نمط أنماط Qiskit. الفارق الرئيسي هو أن جميع زوايا RZZ يجب أن تستوفي الشرط 0<θπ/20 < \theta \leq \pi/2. ثمة نهجان لضمان استيفاء هذا الشرط. يُركز هذا البرنامج التعليمي على النهج الثاني ويوصي به.

# Added by doQumentation — installs packages not in the Binder environment
%pip install -q qiskit-basis-constructor

1. توليد قيم المعاملات التي تستوفي قيد زاوية RZZ

إذا كنت متأكداً من أن جميع زوايا RZZ تقع ضمن النطاق الصحيح، فيمكنك اتباع سير عمل أنماط Qiskit المعياري. في هذه الحالة، تُمرر قيم المعاملات ببساطة كجزء من PUB. يسير سير العمل على النحو التالي.

pm = generate_preset_pass_manager(backend=backend, ...)
t_circuit = pm.run(circuit)
t_observable = observable.apply_layout(t_circuit.layout)
sampler.run([(t_circuit, parameter_values)])
estimator.run([(t_circuit, t_observable, parameter_values)])

إذا حاولت إرسال PUB يتضمن بوابة RZZ بزاوية خارج النطاق الصحيح، فستواجه رسالة خطأ من هذا القبيل:

'The instruction rzz is supported only for angles in the range [0, pi/2], but an angle (20.0) outside of this range has been requested; via parameter value(s) γ[0]=10.0, substituted in parameter expression 2.0*γ[0].'

لتجنب هذا الخطأ، ينبغي النظر في النهج الثاني الموضح أدناه.

2. تعيين قيم المعاملات للدوائر قبل عملية التحويل

توفر حزمة qiskit-ibm-runtime مرحلة تحويل متخصصة تُسمى FoldRzzAngle. تقوم هذه المرحلة بتحويل الدوائر الكمومية بحيث تستوفي جميع زوايا RZZ قيد زاوية RZZ. إذا زوّدت الـ backend إلى generate_preset_pass_manager أو transpile، يُطبّق Qiskit تلقائياً FoldRzzAngle على الدوائر الكمومية. يستلزم ذلك تعيين قيم المعاملات للدوائر الكمومية قبل عملية التحويل. يسير سير العمل على النحو التالي.

pm = generate_preset_pass_manager(backend=backend, ...)
b_circuit = circuit.assign_parameters(parameter_values)
t_circuit = pm.run(b_circuit)
t_observable = observable.apply_layout(t_circuit.layout)
sampler.run([(t_circuit,)])
estimator.run([(t_circuit, t_observable)])

لاحظ أن سير العمل هذا يُكبّد تكلفة حسابية أعلى مقارنةً بالنهج الأول، إذ ينطوي على تعيين قيم المعاملات للدوائر الكمومية وتخزين الدوائر المرتبطة بالمعاملات محلياً. علاوة على ذلك، ثمة مشكلة معروفة في Qiskit حيث قد تفشل عملية تحويل بوابات RZZ في سيناريوهات معينة. للاطلاع على حل بديل، يُرجى الرجوع إلى قسم استكشاف الأخطاء وإصلاحها. يوضح هذا البرنامج التعليمي كيفية استخدام البوابات الكسرية عبر النهج الثاني من خلال مثال مستوحى من طريقة النواة الكمومية. لفهم أعمق لحالات الاستخدام الأمثل للنوى الكمومية، نوصي بقراءة Liu, Arunachalam & Temme (2021).

يمكنك أيضاً العمل على البرنامج التعليمي تدريب النواة الكمومية ودرس النوى الكمومية في دورة تعلم الآلة الكمومية على IBM Quantum Learning.

المتطلبات

قبل البدء في هذا البرنامج التعليمي، تأكد من تثبيت ما يلي:

  • Qiskit SDK الإصدار v2.0 أو أحدث، مع دعم التصور البصري
  • Qiskit Runtime الإصدار v0.37 أو أحدث (pip install qiskit-ibm-runtime)
  • Qiskit Basis Constructor (pip install qiskit_basis_constructor)

الإعداد

import matplotlib.pyplot as plt
import numpy as np
from qiskit import QuantumCircuit, generate_preset_pass_manager
from qiskit.circuit import ParameterVector
from qiskit.circuit.library import unitary_overlap
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2

تفعيل البوابات الكسرية والتحقق من البوابات الأساسية

لاستخدام البوابات الكسرية، يمكنك الحصول على backend يدعمها عبر ضبط الخيار use_fractional_gates=True. إذا كان الـ backend يدعم البوابات الكسرية، فستجد rzz و rx مُدرجتين ضمن بواباته الأساسية.

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
) # backend should be a heron device or later
backend_name = backend.name
backend_c = service.backend(backend_name) # w/o fractional gates
backend_f = service.backend(
backend_name, use_fractional_gates=True
) # w/ fractional gates
print(f"Backend: {backend_name}")
print(f"No fractional gates: {backend_c.basis_gates}")
print(f"With fractional gates: {backend_f.basis_gates}")
if "rzz" not in backend_f.basis_gates:
print(f"Backend {backend_name} does not support fractional gates")
Backend: ibm_fez
No fractional gates: ['cz', 'id', 'rz', 'sx', 'x']
With fractional gates: ['cz', 'id', 'rx', 'rz', 'rzz', 'sx', 'x']

سير العمل مع البوابات الكسرية

الخطوة 1: تعيين المدخلات الكلاسيكية إلى مسألة كمومية

دائرة النواة الكمومية

في هذا القسم، نستكشف دائرة النواة الكمومية باستخدام بوابات RZZ لتقديم سير العمل الخاص بالبوابات الكسرية.

نبدأ ببناء دائرة كمومية لحساب عناصر مصفوفة النواة بصورة فردية. يتم ذلك من خلال دمج دوائر ZZ feature map مع تداخل وحدوي (unitary overlap). تأخذ دالة النواة متجهات في فضاء الخريطة المميزة وتُعيد حاصل ضربها الداخلي كعنصر في مصفوفة النواة: K(x,y)=Φ(x)Φ(y),K(x, y) = \langle \Phi(x) | \Phi(y) \rangle, حيث يمثل Φ(x)|\Phi(x)\rangle الحالة الكمومية المعيّنة بالخريطة المميزة.

نبني يدوياً دائرة ZZ feature map باستخدام بوابات RZZ. على الرغم من أن Qiskit يوفر zz_feature_map مدمجة، إلا أنها لا تدعم حالياً بوابات RZZ اعتباراً من Qiskit v2.0.2 (انظر المشكلة).

بعد ذلك، نحسب دالة النواة لمدخلات متطابقة - على سبيل المثال، K(x,x)=1K(x, x) = 1. على أجهزة الحوسبة الكمومية المشوشة، قد تكون هذه القيمة أقل من 1 بسبب الضوضاء. نتيجة أقرب إلى 1 تُشير إلى ضوضاء أقل في التنفيذ. في هذا البرنامج التعليمي، نُشير إلى هذه القيمة بـ الإخلاص (fidelity)، المُعرَّف بأنه fidelity=K(x,x).\text{fidelity} = K(x, x).

optimization_level = 2
shots = 2000
reps = 3
rng = np.random.default_rng(seed=123)
def my_zz_feature_map(num_qubits: int, reps: int = 1) -> QuantumCircuit:
x = ParameterVector("x", num_qubits * reps)
qc = QuantumCircuit(num_qubits)
qc.h(range(num_qubits))
for k in range(reps):
K = k * num_qubits
for i in range(num_qubits):
qc.rz(x[i + K], i)
pairs = [(i, i + 1) for i in range(num_qubits - 1)]
for i, j in pairs[0::2] + pairs[1::2]:
qc.rzz((np.pi - x[i + K]) * (np.pi - x[j + K]), i, j)
return qc

def quantum_kernel(num_qubits: int, reps: int = 1) -> QuantumCircuit:
qc = my_zz_feature_map(num_qubits, reps=reps)
inner_product = unitary_overlap(qc, qc, "x", "y", insert_barrier=True)
inner_product.measure_all()
return inner_product

def random_parameters(inner_product: QuantumCircuit) -> np.ndarray:
return np.tile(rng.random(inner_product.num_parameters // 2), 2)

def fidelity(result) -> float:
ba = result.data.meas
return ba.get_int_counts().get(0, 0) / ba.num_shots

تُولَّد دوائر النواة الكمومية وقيم معاملاتها المقابلة لأنظمة تتراوح من 4 إلى 40 بتة كمومية، ثم يُقيَّم إخلاصها لاحقاً.

qubits = list(range(4, 44, 4))
circuits = [quantum_kernel(i, reps=reps) for i in qubits]
params = [random_parameters(circ) for circ in circuits]

تُعرض دائرة البتات الكمومية الأربع في الشكل أدناه.

circuits[0].draw("mpl", fold=-1)

Output of the previous code cell

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

b_qc = [
circ.assign_parameters(param) for circ, param in zip(circuits, params)
]
b_qc[0].draw("mpl", fold=-1)

Output of the previous code cell

الخطوة 2: تحسين المسألة لتنفيذها على الأجهزة الكمومية

نُحوّل بعد ذلك الدائرة باستخدام مدير المراحل وفق نمط Qiskit المعياري. من خلال تزويد generate_preset_pass_manager بـ backend يدعم البوابات الكسرية، تُضاف تلقائياً مرحلة متخصصة تُسمى FoldRzzAngle. تُعدّل هذه المرحلة الدائرة لتستوفي قيود زاوية RZZ. ونتيجةً لذلك، تتحول بوابات RZZ ذات القيم السالبة في الشكل السابق إلى قيم موجبة، مع إضافة بعض بوابات X.

backend_f = service.backend(name=backend_name, use_fractional_gates=True)
# pm_f includes `FoldRzzAngle` pass
pm_f = generate_preset_pass_manager(
optimization_level=optimization_level, backend=backend_f
)
t_qc_f = pm_f.run(b_qc)
print(t_qc_f[0].count_ops())
t_qc_f[0].draw("mpl", fold=-1)
OrderedDict([('rz', 35), ('rzz', 18), ('x', 13), ('rx', 9), ('measure', 4), ('barrier', 2)])

Output of the previous code cell

لتقييم تأثير البوابات الكسرية، نقيس عدد البوابات غير المحلية (CZ و RZZ لهذا الـ backend)، إلى جانب عمق الدوائر ومدتها، ثم نقارن هذه المقاييس بتلك الخاصة بسير العمل المعياري لاحقاً.

nnl_f = [qc.num_nonlocal_gates() for qc in t_qc_f]
depth_f = [qc.depth() for qc in t_qc_f]
duration_f = [
qc.estimate_duration(backend_f.target, unit="u") for qc in t_qc_f
]

الخطوة 3: التنفيذ باستخدام Qiskit primitives

نُشغّل الدائرة المحوَّلة مع الـ backend الذي يدعم البوابات الكسرية.

sampler_f = SamplerV2(mode=backend_f)
sampler_f.options.dynamical_decoupling.enable = True
sampler_f.options.dynamical_decoupling.sequence_type = "XY4"
sampler_f.options.dynamical_decoupling.skip_reset_qubits = True
job = sampler_f.run(t_qc_f, shots=shots)
print(job.job_id())
d4bninsi51bc738j97eg

الخطوة 4: المعالجة اللاحقة وإعادة النتيجة بالصيغة الكلاسيكية المطلوبة

يمكنك الحصول على قيمة دالة النواة K(x,x)K(x, x) بقياس احتمال سلسلة البتات الصفرية الكاملة 00...00 في المخرجات.

# job = service.job("d1obougt0npc73flhiag")
result = job.result()
fidelity_f = [fidelity(result=res) for res in result]
print(fidelity_f)
usage_f = job.usage()
[0.9005, 0.647, 0.3345, 0.355, 0.3315, 0.174, 0.1875, 0.149, 0.1175, 0.085]

مقارنة سير العمل والدائرة بدون البوابات الكسرية

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

# step 1: map classical inputs to quantum problem
# `circuits` and `params` from the previous section are reused here
# step 2: optimize circuits
backend_c = service.backend(backend_name) # w/o fractional gates
pm_c = generate_preset_pass_manager(
optimization_level=optimization_level, backend=backend_c
)
t_qc_c = pm_c.run(circuits)
print(t_qc_c[0].count_ops())
t_qc_c[0].draw("mpl", fold=-1)
OrderedDict([('rz', 130), ('sx', 80), ('cz', 36), ('measure', 4), ('barrier', 2)])

Output of the previous code cell

nnl_c = [qc.num_nonlocal_gates() for qc in t_qc_c]
depth_c = [qc.depth() for qc in t_qc_c]
duration_c = [
qc.estimate_duration(backend_c.target, unit="u") for qc in t_qc_c
]
# step 3: execute
sampler_c = SamplerV2(backend_c)
sampler_c.options.dynamical_decoupling.enable = True
sampler_c.options.dynamical_decoupling.sequence_type = "XY4"
sampler_c.options.dynamical_decoupling.skip_reset_qubits = True
job = sampler_c.run(pubs=zip(t_qc_c, params), shots=shots)
print(job.job_id())
d4bnirvnmdfs73ae3a2g
# step 4: post-processing
# job = service.job("d1obp8j3rr0s73bg4810")
result = job.result()
fidelity_c = [fidelity(res) for res in result]
print(fidelity_c)
usage_c = job.usage()
[0.6675, 0.5725, 0.098, 0.102, 0.065, 0.0235, 0.006, 0.0015, 0.0015, 0.002]

مقارنة الأعماق والإخلاص

في هذا القسم، نقارن عدد البوابات غير المحلية والإخلاص بين الدوائر مع البوابات الكسرية ودونها. يُبرز هذا الفوائد المحتملة لاستخدام البوابات الكسرية من حيث كفاءة التنفيذ وجودته.

plt.plot(qubits, depth_c, "-o", label="no fractional gates")
plt.plot(qubits, depth_f, "-o", label="with fractional gates")
plt.xlabel("number of qubits")
plt.ylabel("depth")
plt.title("Comparison of depths")
plt.grid()
plt.legend()
<matplotlib.legend.Legend at 0x12bcaac50>

Output of the previous code cell

plt.plot(qubits, duration_c, "-o", label="no fractional gates")
plt.plot(qubits, duration_f, "-o", label="with fractional gates")
plt.xlabel("number of qubits")
plt.ylabel("duration (µs)")
plt.title("Comparison of durations")
plt.grid()
plt.legend()
<matplotlib.legend.Legend at 0x12bdef310>

Output of the previous code cell

plt.plot(qubits, nnl_c, "-o", label="no fractional gates")
plt.plot(qubits, nnl_f, "-o", label="with fractional gates")
plt.xlabel("number of qubits")
plt.ylabel("number of non-local gates")
plt.title("Comparison of numbers of non-local gates")
plt.grid()
plt.legend()
<matplotlib.legend.Legend at 0x12be8ac90>

Output of the previous code cell

plt.plot(qubits, fidelity_c, "-o", label="no fractional gates")
plt.plot(qubits, fidelity_f, "-o", label="with fractional gates")
plt.xlabel("number of qubits")
plt.ylabel("fidelity")
plt.title("Comparison of fidelities")
plt.grid()
plt.legend()
<matplotlib.legend.Legend at 0x12bea8290>

Output of the previous code cell

نقارن وقت استخدام وحدة المعالجة الكمومية مع البوابات الكسرية ودونها. تُظهر نتائج الخلية التالية أن أوقات استخدام وحدة المعالجة الكمومية متقاربة تقريباً.

print(f"no fractional gates: {usage_c} seconds")
print(f"fractional gates: {usage_f} seconds")
no fractional gates: 7 seconds
fractional gates: 7 seconds

موضوع متقدم: استخدام بوابات RX الكسرية فحسب

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

لدعم هذا الغرض، نوفر دالة مساعدة تُتيح لك تعطيل بوابة أساسية محددة في كائن Target. نستخدمها هنا لتعطيل بوابات RZZ.

from qiskit.circuit.library import n_local
from qiskit.transpiler import Target
def remove_instruction_from_target(target: Target, gate_name: str) -> Target:
new_target = Target(
description=target.description,
num_qubits=target.num_qubits,
dt=target.dt,
granularity=target.granularity,
min_length=target.min_length,
pulse_alignment=target.pulse_alignment,
acquire_alignment=target.acquire_alignment,
qubit_properties=target.qubit_properties,
concurrent_measurements=target.concurrent_measurements,
)

for name, qarg_map in target.items():
if name == gate_name:
continue
instruction = target.operation_from_name(name)
if qarg_map == {None: None}:
qarg_map = None
new_target.add_instruction(instruction, qarg_map, name=name)
return new_target

نستخدم دائرة تتكون من بوابات U و CZ و RZZ كمثال.

qc = n_local(3, "u", "cz", "linear", reps=1)
qc.rzz(1.1, 0, 1)
qc.draw("mpl")

Output of the previous code cell

نُحوّل أولاً الدائرة لـ backend لا يدعم البوابات الكسرية.

pm_c = generate_preset_pass_manager(
optimization_level=optimization_level, backend=backend_c
)
t_qc = pm_c.run(qc)
print(t_qc.count_ops())
t_qc.draw("mpl")
OrderedDict([('rz', 23), ('sx', 16), ('cz', 4)])

Output of the previous code cell

ثم نُحوّل الدائرة ذاتها باستخدام بوابات RX الكسرية، مع استثناء بوابات RZZ. يُفضي ذلك إلى تخفيض طفيف في إجمالي عدد البوابات، بفضل التطبيق الأكثر كفاءة لبوابات RX.

backend_f = service.backend(backend_name, use_fractional_gates=True)
target = remove_instruction_from_target(backend_f.target, "rzz")
pm_f = generate_preset_pass_manager(
optimization_level=optimization_level,
target=target,
)
t_qc = pm_f.run(qc)
print(t_qc.count_ops())
t_qc.draw("mpl")
OrderedDict([('rz', 22), ('sx', 14), ('cz', 4), ('rx', 1)])

Output of the previous code cell

تحسين بوابات U باستخدام بوابات RX الكسرية

في هذا القسم، نوضح كيفية تحسين بوابات U باستخدام بوابات RX الكسرية، بالاستناد إلى الدائرة ذاتها المُقدَّمة في القسم السابق.

ستحتاج إلى تثبيت حزمة qiskit-basis-constructor package لهذا القسم. هذه نسخة تجريبية من مكوّن تحويل جديد لـ Qiskit، قد يُدمج في Qiskit مستقبلاً.

# %pip install qiskit-basis-constructor
from qiskit.circuit.library import UGate
from qiskit_basis_constructor import DEFAULT_EQUIVALENCE_LIBRARY

نُحوّل الدائرة باستخدام بوابات RX الكسرية فحسب، مع استثناء بوابات RZZ. من خلال إدخال قاعدة تحليل مخصصة، كما هو موضح فيما يلي، يمكن تقليل عدد البوابات أحادية البت الكمومي اللازمة لتطبيق بوابة U.

هذه الميزة قيد النقاش حالياً في هذه مشكلة على GitHub.

# special decomposition rule for UGate
x = ParameterVector("x", 3)
zxz = QuantumCircuit(1)
zxz.rz(x[2] - np.pi / 2, 0)
zxz.rx(x[0], 0)
zxz.rz(x[1] + np.pi / 2, 0)
DEFAULT_EQUIVALENCE_LIBRARY.add_equivalence(UGate(x[0], x[1], x[2]), zxz)

بعد ذلك، نطبق المُحوّل باستخدام ترجمة constructor-beta المُوفَّرة من حزمة qiskit-basis-constructor. ونتيجةً لذلك، يتقلص إجمالي عدد البوابات مقارنةً بعملية التحويل السابقة.

pm_f = generate_preset_pass_manager(
optimization_level=optimization_level,
target=target,
translation_method="constructor-beta",
)
t_qc = pm_f.run(qc)
print(t_qc.count_ops())
t_qc.draw("mpl")
OrderedDict([('rz', 16), ('rx', 9), ('cz', 4)])

Output of the previous code cell

استكشاف الأخطاء وإصلاحها

المشكلة: قد تبقى زوايا RZZ غير صالحة بعد التحويل

اعتباراً من Qiskit v2.0.3، ثمة مشكلات معروفة قد تظل فيها بوابات RZZ ذات الزوايا غير الصالحة في الدوائر حتى بعد التحويل. تنشأ المشكلة عادةً في الحالات التالية.

الفشل عند استخدام خيار target مع generate_preset_pass_manager أو transpiler

عند استخدام خيار target مع generate_preset_pass_manager أو transpiler، لا تُستدعى مرحلة التحويل المتخصصة FoldRzzAngle. لضمان المعالجة الصحيحة لزوايا RZZ في البوابات الكسرية، نوصي دائماً باستخدام خيار backend عوضاً عن ذلك. راجع هذه المشكلة للمزيد من التفاصيل.

الفشل عندما تحتوي الدوائر على بوابات معينة

إذا احتوت دائرتك على بوابات معينة مثل XXPlusYYGate، فقد يُولّد مُحوّل Qiskit بوابات RZZ بزوايا غير صالحة. إذا واجهت هذه المشكلة، راجع هذه مشكلة GitHub للاطلاع على حل بديل.

استطلاع البرنامج التعليمي

يُرجى إجراء هذا الاستطلاع القصير لتقديم ملاحظاتك حول هذا البرنامج التعليمي. ستُساعدنا آراؤك في تحسين محتوانا وتجربة المستخدم.

رابط الاستطلاع