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

نموذج إيزينغ بالحقل المستعرض مع إدارة الأداء من Q-CTRL

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

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

يُعدّ نموذج إيزينغ بالحقل المستعرض (TFIM) أداةً مهمة لدراسة المغناطيسية الكمومية والتحولات الطورية. يصف هذا النموذج مجموعةً من العزوم المغزلية المرتبة على شبكة بلورية، حيث يتفاعل كل عزم مغزلي مع جيرانه بينما يتأثر في آنٍ واحد بمجال مغناطيسي خارجي يُحفّز التقلبات الكمومية.

يشيع استخدام تحليل Trotter لمحاكاة هذا النموذج، إذ يُقرّب مؤثر التطور الزمني من خلال بناء دوائر تتناوب بين دورات أحادية الكيوبت وتفاعلات ثنائية الكيوبت متشابكة. غير أن إجراء هذه المحاكاة على عتاد حقيقي يظل تحدياً بسبب الضجيج وفقدان الترابط الكمومي، مما يُفضي إلى انحرافات عن الديناميكيات الحقيقية. للتغلب على ذلك، نستخدم أدوات قمع الأخطاء وإدارة الأداء من Fire Opal التابعة لـ Q-CTRL، المتاحة بوصفها دالة Qiskit (انظر توثيق Fire Opal). تُحسّن Fire Opal تلقائياً تنفيذ الدوائر من خلال تطبيق تقنيات مثل الفصل الديناميكي والتخطيط المتقدم والتوجيه وأساليب أخرى لقمع الأخطاء، وكلها تهدف إلى تقليل الضجيج. بفضل هذه التحسينات، تتوافق نتائج العتاد الحقيقي بصورة أكثر دقة مع المحاكاة الخالية من الضجيج، مما يُمكّننا من دراسة ديناميكيات مغنطة نموذج TFIM بدقة عالية.

في هذا البرنامج التعليمي سنقوم بما يلي:

  • بناء هاميلتوني TFIM على رسم بياني من مثلثات العزوم المغزلية المترابطة
  • محاكاة التطور الزمني باستخدام دوائر Trotter بعمق متفاوت
  • حساب مغنطة الكيوبتات المفردة Zi\langle Z_i \rangle وتمثيلها بيانياً عبر الزمن
  • مقارنة المحاكاة الأساسية بالنتائج المستخرجة من تشغيل العتاد الحقيقي باستخدام إدارة أداء Fire Opal من Q-CTRL

نظرة عامة

نموذج إيزينغ بالحقل المستعرض (TFIM) هو نموذج دوران كمومي يلتقط السمات الجوهرية للتحولات الطورية الكمومية. يُعرَّف الهاميلتوني على النحو الآتي:

H=JiZiZi+1hiXiH = -J \sum_{i} Z_i Z_{i+1} - h \sum_{i} X_i

حيث ZiZ_i وXiX_i هما مؤثرا باولي يعملان على الكيوبت ii، وJJ هي قوة الاقتران بين العزوم المغزلية المجاورة، وhh هي شدة المجال المغناطيسي المستعرض. يمثّل الحد الأول التفاعلات الحديدية المغناطيسية الكلاسيكية، بينما يُدخل الحد الثاني التقلبات الكمومية عبر الحقل المستعرض. لمحاكاة ديناميكيات TFIM، نستخدم تحليل Trotter لمؤثر التطور الوحدوي eiHte^{-iHt}، المُنفَّذ عبر طبقات من بوابات RX وRZZ استناداً إلى رسم بياني مخصص من مثلثات العزوم المغزلية المترابطة. يستكشف هذا التنفيذ كيف تتطور المغنطة Z\langle Z \rangle مع تزايد خطوات Trotter.

يُقيَّم أداء تطبيق TFIM المقترح بمقارنة المحاكاة الخالية من الضجيج مع الخلفيات الضوضائية. تُستخدم ميزات التنفيذ المحسّن وقمع الأخطاء في Fire Opal للتخفيف من أثر الضجيج في العتاد الحقيقي، مما يُنتج تقديرات أكثر موثوقية لمتوسطات المتغيرات الكمومية مثل Zi\langle Z_i \rangle والمترابطات ZiZj\langle Z_i Z_j \rangle.

المتطلبات

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

  • Qiskit SDK الإصدار 1.4 أو أحدث، مع دعم التصور
  • Qiskit Runtime الإصدار 0.40 أو أحدث (pip install qiskit-ibm-runtime)
  • Qiskit Functions Catalog الإصدار 0.9.0 (pip install qiskit-ibm-catalog)
  • Fire Opal SDK الإصدار 9.0.2 أو أحدث (pip install fire-opal)
  • Q-CTRL Visualizer الإصدار 8.0.2 أو أحدث (pip install qctrl-visualizer)

الإعداد

أولاً، قم بالمصادقة باستخدام مفتاح IBM Quantum API. ثم اختر دالة Qiskit كما يلي. (يفترض هذا الكود أنك قد حفظت حسابك مسبقاً في بيئتك المحلية.)

# Added by doQumentation — installs packages not in the Binder environment
%pip install -q networkx qctrlvisualizer
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit import QuantumCircuit
from qiskit_ibm_catalog import QiskitFunctionsCatalog
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit.quantum_info import SparsePauliOp
from qiskit_aer import AerSimulator

import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
import qctrlvisualizer as qv
catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")

# Access Function
perf_mgmt = catalog.load("q-ctrl/performance-management")

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

توليد الرسم البياني لـ TFIM

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

تبني الدالة المساعدة connected_triangles_adj_matrix مصفوفة التجاور لهذا التركيب. بالنسبة لسلسلة من nn مثلثاً، يحتوي الرسم البياني الناتج على 2n+12n+1 عقدة.

def connected_triangles_adj_matrix(n):
"""
Generate the adjacency matrix for 'n' connected triangles in a chain.
"""
num_nodes = 2 * n + 1
adj_matrix = np.zeros((num_nodes, num_nodes), dtype=int)

for i in range(n):
a, b, c = i * 2, i * 2 + 1, i * 2 + 2 # Nodes of the current triangle

# Connect the three nodes in a triangle
adj_matrix[a, b] = adj_matrix[b, a] = 1
adj_matrix[b, c] = adj_matrix[c, b] = 1
adj_matrix[a, c] = adj_matrix[c, a] = 1

# If not the first triangle, connect to the previous triangle
if i > 0:
adj_matrix[a, a - 1] = adj_matrix[a - 1, a] = 1

return adj_matrix

لتصوير الشبكة التي عرّفناها للتو، يمكننا رسم سلسلة المثلثات المترابطة ووضع تسمية لكل عقدة. تبني الدالة أدناه الرسم البياني لعدد محدد من المثلثات وتعرضه.

def plot_triangle_chain(n, side=1.0):
"""
Plot a horizontal chain of n equilateral triangles.
Baseline: even nodes (0,2,4,...,2n) on y=0
Apexes: odd nodes (1,3,5,...,2n-1) above the midpoint.
"""
# Build graph
A = connected_triangles_adj_matrix(n)
G = nx.from_numpy_array(A)

h = np.sqrt(3) / 2 * side
pos = {}

# Place baseline nodes
for k in range(n + 1):
pos[2 * k] = (k * side, 0.0)

# Place apex nodes
for k in range(n):
x_left = pos[2 * k][0]
x_right = pos[2 * k + 2][0]
pos[2 * k + 1] = ((x_left + x_right) / 2, h)

# Draw
fig, ax = plt.subplots(figsize=(1.5 * n, 2.5))
nx.draw(
G,
pos,
ax=ax,
with_labels=True,
font_size=10,
font_color="white",
node_size=600,
node_color=qv.QCTRL_STYLE_COLORS[0],
edge_color="black",
width=2,
)
ax.set_aspect("equal")
ax.margins(0.2)
plt.show()

return G, pos

في هذا البرنامج التعليمي سنستخدم سلسلة مكونة من 20 مثلثاً.

n_triangles = 20
n_qubits = 2 * n_triangles + 1
plot_triangle_chain(n_triangles, side=1.0)
plt.show()

Output of the previous code cell

تلوين حواف الرسم البياني

لتطبيق الاقتران بين العزوم المغزلية، من المفيد تجميع الحواف غير المتداخلة. يُتيح ذلك تطبيق بوابات ثنائية الكيوبت بالتوازي. يمكن تحقيق ذلك بإجراء بسيط لتلوين الحواف [1]، الذي يُعيّن لوناً لكل حافة بحيث توضع الحواف المتلاقية عند العقدة ذاتها في مجموعات مختلفة.

def edge_coloring(graph):
"""
Takes a NetworkX graph and returns a list of lists where each inner list contains
the edges assigned the same color.
"""
line_graph = nx.line_graph(graph)
edge_colors = nx.coloring.greedy_color(line_graph)

color_groups = {}
for edge, color in edge_colors.items():
if color not in color_groups:
color_groups[color] = []
color_groups[color].append(edge)

return list(color_groups.values())

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

توليد دوائر Trotter على الرسوم البيانية للعزوم المغزلية

لمحاكاة ديناميكيات TFIM، نبني دوائر تُقرّب مؤثر التطور الزمني.

U(t)=eiHt,whereH=Ji,jZiZjhiXi.U(t) = e^{-i H t}, \quad \text{where} \quad H = -J \sum_{\langle i,j \rangle} Z_i Z_j - h \sum_i X_i .

نستخدم تحليل Trotter من الرتبة الثانية:

eiHΔteiHXΔt/2eiHZΔteiHXΔt/2,e^{-i H \Delta t} \approx e^{-i H_X \Delta t / 2}\, e^{-i H_Z \Delta t}\, e^{-i H_X \Delta t / 2},

حيث HX=hiXiH_X = -h \sum_i X_i وHZ=Ji,jZiZjH_Z = -J \sum_{\langle i,j \rangle} Z_i Z_j.

  • يُنفَّذ حد HXH_X بطبقات من دورات RX.
  • يُنفَّذ حد HZH_Z بطبقات من بوابات RZZ على طول حواف الرسم البياني للتفاعل.

تتحدد زوايا هذه البوابات بناءً على الحقل المستعرض hh، وثابت الاقتران JJ، والخطوة الزمنية Δt\Delta t. بتراكم خطوات Trotter المتعددة، نُولّد دوائر بعمق متزايد تُقرّب ديناميكيات النظام. تبني الدالتان generate_tfim_circ_custom_graph وtrotter_circuits دائرة كمومية مُرتّبة بتحليل Trotter انطلاقاً من رسم بياني اعتباطي لتفاعل العزوم المغزلية.

def generate_tfim_circ_custom_graph(
steps, h, J, dt, psi0, graph: nx.graph.Graph, meas_basis="Z", mirror=False
):
"""
Generate a second order trotter of the form e^(a+b) ~ e^(b/2) e^a e^(b/2) for simulating a transverse field ising model:
e^{-i H t} where the Hamiltonian H = -J \\sum_i Z_i Z_{i+1} + h \\sum_i X_i.

steps: Number of trotter steps
theta_x: Angle for layer of X rotations
theta_zz: Angle for layer of ZZ rotations
theta_x: Angle for second layer of X rotations
J: Coupling between nearest neighbor spins
h: The transverse magnetic field strength
dt: t/total_steps
psi0: initial state (assumed to be prepared in the computational basis).
meas_basis: basis to measure all correlators in

This is a second order trotter of the form e^(a+b) ~ e^(b/2) e^a e^(b/2)
"""
theta_x = h * dt
theta_zz = -2 * J * dt
nq = graph.number_of_nodes()
color_edges = edge_coloring(graph)
circ = QuantumCircuit(nq, nq)
# Initial state, for typical cases in the computational basis
for i, b in enumerate(psi0):
if b == "1":
circ.x(i)
# Trotter steps
for step in range(steps):
for i in range(nq):
circ.rx(theta_x, i)
if mirror:
color_edges = [sublist[::-1] for sublist in color_edges[::-1]]
for edge_list in color_edges:
for edge in edge_list:
circ.rzz(theta_zz, edge[0], edge[1])
for i in range(nq):
circ.rx(theta_x, i)

# some typically used basis rotations
if meas_basis == "X":
for b in range(nq):
circ.h(b)
elif meas_basis == "Y":
for b in range(nq):
circ.sdg(b)
circ.h(b)

for i in range(nq):
circ.measure(i, i)

return circ

def trotter_circuits(G, d_ind_tot, J, h, dt, meas_basis, mirror=True):
"""
Generates a sequence of Trotterized circuits, each with increasing depth.
Given a spin interaction graph and Hamiltonian parameters, it constructs
a list of circuits with 1 to d_ind_tot Trotter steps

G: Graph defining spin interactions (edges = ZZ couplings)
d_ind_tot: Number of Trotter steps (maximum depth)
J: Coupling between nearest neighboring spins
h: Transverse magnetic field strength
dt: (t / total_steps
meas_basis: Basis to measure all correlators in
mirror: If True, mirror the Trotter layers
"""
qubit_count = len(G)
circuits = []
psi0 = "0" * qubit_count

for steps in range(1, d_ind_tot + 1):
circuits.append(
generate_tfim_circ_custom_graph(
steps, h, J, dt, psi0, G, meas_basis, mirror
)
)
return circuits

تقدير مغنطة الكيوبتات المفردة Zi\langle Z_i \rangle

لدراسة ديناميكيات النموذج، نريد قياس مغنطة كل كيوبت، المُعرَّفة بقيمة التوقع Zi=ψZiψ\langle Z_i \rangle = \langle \psi | Z_i | \psi \rangle.

في المحاكاة، يمكن حساب هذه القيمة مباشرةً من نتائج القياس. تعالج الدالة z_expectation عدد سلاسل البتات وتُعيد قيمة Zi\langle Z_i \rangle لمؤشر كيوبت محدد. على العتاد الحقيقي، نُقيّم الكمية ذاتها بتحديد مؤثر باولي باستخدام الدالة generate_z_observables، ثم تحسب الخلفية قيمة التوقع.

def z_expectation(counts, index):
"""
counts: Dict of mitigated bitstrings.
index: Index i in the single operator expectation value < II...Z_i...I > to be calculated.
return: < Z_i >
"""
z_exp = 0
tot = 0
for bitstring, value in counts.items():
bit = int(bitstring[index])
sign = 1
if bit % 2 == 1:
sign = -1
z_exp += sign * value
tot += value

return z_exp / tot
def generate_z_observables(nq):
observables = []
for i in range(nq):
pauli_string = "".join(["Z" if j == i else "I" for j in range(nq)])
observables.append(SparsePauliOp(pauli_string))
return observables
observables = generate_z_observables(n_qubits)

نُعرّف الآن معاملات توليد دوائر Trotter. في هذا البرنامج التعليمي، الشبكة عبارة عن سلسلة من 20 مثلثاً متصلاً، وهو ما يُقابل نظاماً من 41 كيوبت.

all_circs_mirror = []
for num_triangles in [n_triangles]:
for meas_basis in ["Z"]:
A = connected_triangles_adj_matrix(num_triangles)
G = nx.from_numpy_array(A)
nq = len(G)
d_ind_tot = 22
dt = 2 * np.pi * 1 / 30 * 0.25
J = 1
h = -7
all_circs_mirror.extend(
trotter_circuits(G, d_ind_tot, J, h, dt, meas_basis, True)
)
circs = all_circs_mirror

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

تشغيل محاكاة MPS

يُنفَّذ قائمة دوائر Trotter باستخدام محاكي matrix_product_state مع اختيار اعتباطي يبلغ 40964096 لقطة. تُوفر طريقة MPS تقريباً فعّالاً لديناميكيات الدائرة، تتحدد دقته بأبعاد الرابط المختارة. بالنسبة لأحجام الأنظمة المدروسة هنا، يكفي البُعد الافتراضي للرابط لالتقاط ديناميكيات المغنطة بدقة عالية. تُوحَّد الأعداد الخام، ومن هذه الأعداد نحسب قيم التوقع أحادية الكيوبت Zi\langle Z_i \rangle عند كل خطوة Trotter. وأخيراً، نحسب المتوسط عبر جميع الكيوبتات للحصول على منحنى واحد يُظهر كيف تتغير المغنطة عبر الزمن.

backend_sim = AerSimulator(method="matrix_product_state")

def normalize_counts(counts_list, shots):
new_counts_list = []
for counts in counts_list:
a = {k: v / shots for k, v in counts.items()}
new_counts_list.append(a)
return new_counts_list

def run_sim(circ_list):
shots = 4096
res = backend_sim.run(circ_list, shots=shots)
normed = normalize_counts(res.result().get_counts(), shots)
return normed

sim_counts = run_sim(circs)

التشغيل على العتاد الحقيقي

service = QiskitRuntimeService()
backend = service.backend("ibm_marrakesh")

def run_qiskit(circ_list):
shots = 4096
pm = generate_preset_pass_manager(backend=backend)
isa_circuits = [pm.run(qc) for qc in circ_list]
sampler = Sampler(mode=backend)
res = sampler.run(isa_circuits, shots=shots)
res = [r.data.c.get_counts() for r in res.result()]
normed = normalize_counts(res, shots)
return normed

qiskit_counts = run_qiskit(circs)

التشغيل على العتاد الحقيقي مع Fire Opal

نُقيّم ديناميكيات المغنطة على عتاد كمومي حقيقي. تُوفّر Fire Opal دالة Qiskit تُوسّع primitive المقدِّر القياسي في Qiskit Runtime بقمع الأخطاء الآلي وإدارة الأداء. نُرسل دوائر Trotter مباشرةً إلى خلفية IBM® بينما تتولى Fire Opal التنفيذ مع الوعي بالضجيج.

نُعدّ قائمة من pubs، حيث يحتوي كل عنصر على دائرة والمتغيرات الكمومية المقابلة لها من نوع باولي-Z. تُمرَّر هذه العناصر إلى دالة المقدِّر في Fire Opal، التي تُعيد قيم التوقع Zi\langle Z_i \rangle لكل كيوبت عند كل خطوة Trotter. يمكن بعد ذلك حساب متوسط النتائج عبر الكيوبتات للحصول على منحنى المغنطة من العتاد الحقيقي.

backend_name = "ibm_marrakesh"
estimator_pubs = [(qc, observables) for qc in all_circs_mirror[:]]

# Run the circuit using the estimator
qctrl_estimator_job = perf_mgmt.run(
primitive="estimator",
pubs=estimator_pubs,
backend_name=backend_name,
options={"default_shots": 4096},
)

result_qctrl = qctrl_estimator_job.result()

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

أخيراً، نقارن منحنى المغنطة من المحاكي بالنتائج المستخرجة من العتاد الحقيقي. يُظهر رسم كليهما جنباً إلى جنب مدى توافق تنفيذ العتاد مع Fire Opal مع الأساس الخالي من الضجيج عبر خطوات Trotter.

def make_correlators(test_counts, nq, d_ind_tot):
mz = np.empty((nq, d_ind_tot))
for d_ind in range(d_ind_tot):
counts = test_counts[d_ind]
for i in range(nq):
mz[i, d_ind] = z_expectation(counts, i)
average_z = np.mean(mz, axis=0)
return np.concatenate((np.array([1]), average_z), axis=0)

sim_exp = make_correlators(sim_counts[0:22], nq=nq, d_ind_tot=22)
qiskit_exp = make_correlators(qiskit_counts[0:22], nq=nq, d_ind_tot=22)
qctrl_exp = [ev.data.evs for ev in result_qctrl[:]]
qctrl_exp_mean = np.concatenate(
(np.array([1]), np.mean(qctrl_exp, axis=1)), axis=0
)
def make_expectations_plot(
sim_z,
depths,
exp_qctrl=None,
exp_qctrl_error=None,
exp_qiskit=None,
exp_qiskit_error=None,
plot_from=0,
plot_upto=23,
):
import numpy as np
import matplotlib.pyplot as plt

depth_ticks = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]

d = np.asarray(depths)[plot_from:plot_upto]
sim = np.asarray(sim_z)[plot_from:plot_upto]

qk = (
None
if exp_qiskit is None
else np.asarray(exp_qiskit)[plot_from:plot_upto]
)
qc = (
None
if exp_qctrl is None
else np.asarray(exp_qctrl)[plot_from:plot_upto]
)

qk_err = (
None
if exp_qiskit_error is None
else np.asarray(exp_qiskit_error)[plot_from:plot_upto]
)
qc_err = (
None
if exp_qctrl_error is None
else np.asarray(exp_qctrl_error)[plot_from:plot_upto]
)

# ---- helper(s) ----
def rmse(a, b):
if a is None or b is None:
return None
a = np.asarray(a, dtype=float)
b = np.asarray(b, dtype=float)
mask = np.isfinite(a) & np.isfinite(b)
if not np.any(mask):
return None
diff = a[mask] - b[mask]
return float(np.sqrt(np.mean(diff**2)))

def plot_panel(ax, method_y, method_err, color, label, band_color=None):
# Noiseless reference
ax.plot(d, sim, color="grey", label="Noiseless simulation")

# Method line + band
if method_y is not None:
ax.plot(d, method_y, color=color, label=label)
if method_err is not None:
lo = np.clip(method_y - method_err, -1.05, 1.05)
hi = np.clip(method_y + method_err, -1.05, 1.05)
ax.fill_between(
d,
lo,
hi,
alpha=0.18,
color=band_color if band_color else color,
label=f"{label} ± error",
)
else:
ax.text(
0.5,
0.5,
"No data",
transform=ax.transAxes,
ha="center",
va="center",
fontsize=10,
color="0.4",
)

# RMSE box (vs sim)
r = rmse(method_y, sim)
if r is not None:
ax.text(
0.98,
0.02,
f"RMSE: {r:.4f}",
transform=ax.transAxes,
va="bottom",
ha="right",
fontsize=8,
bbox=dict(
boxstyle="round,pad=0.35", fc="white", ec="0.7", alpha=0.9
),
)
# Axes
ax.set_xticks(depth_ticks)
ax.set_ylim(-1.05, 1.05)
ax.grid(True, which="both", linewidth=0.4, alpha=0.4)
ax.set_axisbelow(True)
ax.legend(prop={"size": 8}, loc="best")

fig, axes = plt.subplots(1, 2, figsize=(10, 4), dpi=300, sharey=True)

axes[0].set_title("Fire Opal (Q-CTRL)", fontsize=10)
plot_panel(
axes[0],
qc,
qc_err,
color="#680CE9",
label="Fire Opal",
band_color="#680CE9",
)
axes[0].set_xlabel("Trotter step")
axes[0].set_ylabel(r"$\langle Z \rangle$")
axes[1].set_title("Qiskit", fontsize=10)
plot_panel(
axes[1], qk, qk_err, color="blue", label="Qiskit", band_color="blue"
)
axes[1].set_xlabel("Trotter step")

plt.tight_layout()
plt.show()
depths = list(range(d_ind_tot + 1))
errors = np.abs(np.array(qctrl_exp_mean) - np.array(sim_exp))

errors_qiskit = np.abs(np.array(qiskit_exp) - np.array(sim_exp))
make_expectations_plot(
sim_exp,
depths,
exp_qctrl=qctrl_exp_mean,
exp_qctrl_error=errors,
exp_qiskit=qiskit_exp,
exp_qiskit_error=errors_qiskit,
)

Output of the previous code cell

المراجع

[1] Graph coloring. Wikipedia. Retrieved September 15, 2025, from https://en.wikipedia.org/wiki/Graph_coloring

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

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

رابط الاستبيان