التعليم والبحث العلمي الأدلة التقنية

الجدول الدراسي Timetable دون تعارضات مع FET

كيف تصف مدرستك لبرنامج FET بالمعلمين والفصول والقاعات والقيود، فيخرج لك جدولاً كاملاً دون أي تعارض في ثوان.

الجدول الدراسي Timetable دون تعارضات مع FET

في كل مدرسة يأتي أسبوع قبل بداية الفصل الدراسي يجلس فيه وكيل الشؤون التعليمية أمام جدول كبير في Excel، ويحاول أن يضع 198 حصة لستة فصول واثني عشر معلماً في 35 فترة، فإذا نقل حصة العلوم ليفرغ المعمل وجد أن معلم الإنجليزية أصبح عنده حصتان في الوقت نفسه، وإذا أصلح ذلك ظهرت فجوة في يوم أحد الفصول. وهذا العمل يأخذ أياماً، ثم يتغير كل شيء إذا اعتذر معلم عن يوم واحد. في مقال الأنظمة التي تحتاجها الجامعة ذكرنا أن الجدولة Scheduling جزء من نظام معلومات الطلاب SIS، وفي هذا المقال سوف نأخذها وحدها مع برنامج مفتوح المصدر متخصص فيها هو FET، وسوف نتناول:

  • لماذا لا يصلح الحل اليدوي ولا الحل البسيط بالترتيب، وكيف يبحث FET عن الحل.
  • بناء FET 7.12.0 في Docker Image وتشغيله من سطر الأوامر باللغة العربية.
  • وصف مدرسة متوسطة كاملة: الأيام والحصص والمواد والمعلمون والفصول والقاعات والقيود.
  • تشغيل حقيقي وجداول ناتجة لكل فصل ومعلم وقاعة، ثم فحص مستقل للتأكد من عدم وجود تعارض.
  • ماذا يحدث إذا كانت البيانات مستحيلة، وكيف تقرأ رسالة FET.

لماذا يصعب الجدول الدراسي Timetable؟

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

والصعوبة الحقيقية ليست في منع التعارض فقط، وإنما في القيود Constraints التي تضيفها المدرسة فوقه، وهي نوعان:

  • القيد الإلزامي Hard Constraint: لا يقبل الجدول إذا خالفه، مثل ألا يدرس المعلم فصلين في الوقت نفسه، وألا يوضع معلم في يوم لا يأتي فيه.
  • القيد المرن Soft Constraint: رغبة تحاول أن تحققها قدر الإمكان، مثل توزيع حصص المادة على أيام مختلفة، ويعطيه FET وزناً Weight بنسبة مئوية، فالوزن 100% يجعله إلزامياً، والوزن 95% يعني أن البرنامج يحاول تحقيقه ويقبل مخالفته إذا لم يجد غير ذلك.

ما هو FET؟

FET برنامج مفتوح المصدر لتوليد الجداول الدراسية للمدارس والجامعات تلقائياً، يطوره Liviu Lalescu وVolker Dirr منذ 2002، وهو مرخص بترخيص GNU Affero General Public License الإصدار 3، وصفحة التنزيل تذكر أن آخر إصدار هو 7.12.0 الصادر في 14 أكتوبر 2026. والبرنامج نسختان من الكود نفسه:

  • الواجهة الرسومية fet: برنامج سطح مكتب لويندوز وLinux وmacOS، يدخل فيه المسؤول البيانات ويولد الجدول ويعدله، وهي التي يستخدمها أغلب الناس.
  • سطر الأوامر fet-cl: يأخذ ملف البيانات ويخرج الجداول دون أي واجهة، وهو المناسب لسيرفر أو لربط FET بنظام آخر، وهو الذي سوف نستخدمه في التجربة لأنه يعمل داخل Container.

وكل بيانات المدرسة تحفظ في ملف واحد بامتداد .fet، وهو ملف XML، والصورة التالية تبين ما يدخل فيه وما يخرج من البرنامج:

مخطط المدخلات والمخرجات في FET: على اليمين ملف school.fet وفيه الأيام والحصص 5 أيام في 7 حصص، والمواد والمعلمون 9 مواد و12 معلماً، والصفوف والفصول 3 صفوف و6 فصول، والقاعات 6 فصول ومعملان وصالة، والأنشطة من يدرس ماذا لمن وكم حصة، والقيود الإلزامية والمرنة وقيود القاعات، وفي الوسط FET يبحث عن مكان لكل نشاط ويبدل الأنشطة عند التعارض، وعلى اليسار المخرجات جدول كل فصل وجدول كل معلم وجدول كل قاعة وملف XML للأنظمة الأخرى
كل ما تعرفه المدرسة يدخل في ملف واحد، والجداول كلها تخرج منه

والمفهوم الأهم في FET هو النشاط Activity، وهو حصة أو مجموعة حصص يدرسها معلم معين لفصل معين في مادة معينة، فحصص العلوم الأربع للأول أ نشاط واحد مقسم إلى ثلاثة أجزاء: حصة مزدوجة Double Period وحصتان مفردتان، والبرنامج يضع كل جزء في مكانه.

كيف يبحث FET عن الحل؟

يشرح مؤلف البرنامج الخوارزمية في الملف doc/algorithm/algorithm-description.txt داخل الكود، ويصفها بأنها خوارزمية تقريبية Heuristic سماها التبديل المتكرر Recursive Swapping، وفكرتها كما يلي: يرتب الأنشطة من الأصعب إلى الأسهل، ثم يضع كل نشاط في فترة مسموحة، وإذا لم يجد فترة فارغة يحسب لكل فترة عدد الأنشطة التي يجب أن تخرج منها لو وضع النشاط فيها، فيختار الفترة الأقل ويخرج تلك الأنشطة ويحاول أن يضعها من جديد بالطريقة نفسها، بعمق محدود حتى لا يدور بلا نهاية. والصورة التالية تبين ذلك بمثال من مدرستنا:

ثلاث خطوات للتبديل المتكرر في FET: الأولى حصة رياضيات للأول أ لا تجد فترة فارغة، فيحسب لكل فترة عدد التعارضات، عربية 3 وإنجليزية 2 والعلوم واحد وفترة فيها المعلم غير متاح، ويختار فترة العلوم، والثانية يضع الرياضيات في فترة العلوم ويخرج حصة العلوم فيخرج نشاط واحد بدلاً من ثلاثة، والثالثة يكرر الخطوة لحصة العلوم فتجد فترة أخرى، بعمق محدود للتكرار، حتى توضع كل الأنشطة أو ينتهي الوقت
بدلاً من أن يتوقف عند أول حصة بلا مكان، يخرج أقل عدد من الحصص ويعيد وضعها

ولاحظ أن الترتيب من الأصعب إلى الأسهل ليس تفصيلاً، فالملف نفسه يذكر أنه قد يسرع البحث عشر مرات أو أكثر، والسبب أن المعلم الذي يأتي ثلاثة أيام فقط والمعمل الذي تشترك فيه ستة فصول لهما خيارات قليلة، فإذا تركتهما للنهاية لن تجد لهما مكاناً. والصفحة الرئيسية للبرنامج تذكر أنه يحل الجدول المعقد عادة في 5 إلى 20 دقيقة، والصعب جداً قد يأخذ ساعات.

المتطلبات

  • للواجهة الرسومية: جهاز ويندوز أو Linux أو macOS، والنسخ الجاهزة على صفحة التنزيل، ولا تحتاج تثبيت Qt بنفسك.
  • لسطر الأوامر على سيرفر: Docker، وحوالي 1GB مساحة مؤقتة أثناء البناء، والبرنامج نفسه بعد البناء صغير، والمعالج هو الذي يعمل أثناء البحث، والذاكرة التي يحتاجها جدول مدرسة واحدة قليلة، وقد قيدنا ال Container ب 512MB ومعالجين وعمل دون مشكلة.
  • لا توجد واجهة ويب ولا منفذ Port: FET لا يعمل كخدمة، فلا يوجد ما ينشر على الشبكة، والمخرجات صفحات HTML ثابتة تنشرها إن أردت على أي سيرفر ويب.

بناء fet-cl في Docker Image

لا يوجد Docker Image رسمي لبرنامج FET، والمشروع ينشر الكود مع ملف README يشرح البناء، وصفحة التنزيل تعطي حجم الملف وبصمته SHA256، فنبدأ بتنزيله والتأكد منه:

mkdir fet-lab && cd fet-lab
curl -sL -o fet-7.12.0.tar.xz https://lalescu.ro/liviu/fet/download/fet-7.12.0.tar.xz
sha256sum fet-7.12.0.tar.xz
ff3d1e0be7708a648f6c4a5078f49d0b0926283c065791105229753c7b697cd4 *fet-7.12.0.tar.xz

والبصمة مطابقة لما على صفحة التنزيل. وملف README يذكر طريقتين للبناء: CMake، وهي الموصى بها وتحتاج Qt 6.10 على الأقل، وQMake، وهي الطريقة القديمة وتكفيها Qt 5.15 أو أحدث. وDebian 13 فيه Qt 6.8.2، لذلك استخدمنا QMake وبنينا نسخة سطر الأوامر فقط من الملف src/src-cl.pro، ثم حولنا ملف الترجمة العربية fet_ar.ts إلى الصيغة التي يقرؤها البرنامج .qm بالأداة lrelease. أنشئ الملف Dockerfile التالي:

FROM debian:trixie-20261005-slim AS build
RUN apt-get update && apt-get install -y --no-install-recommends \
      g++ make qmake6 qt6-base-dev qt6-l10n-tools xz-utils ca-certificates && rm -rf /var/lib/apt/lists/*
COPY fet-7.12.0.tar.xz /tmp/
RUN cd /tmp && tar xf fet-7.12.0.tar.xz && cd fet-7.12.0/src \
 && qmake6 src-cl.pro && make -j3 \
 && for f in /tmp/fet-7.12.0/translations/fet_ar.ts /tmp/fet-7.12.0/translations/fet_en_GB.ts; do /usr/lib/qt6/bin/lrelease $f; done && ldd /tmp/fet-7.12.0/fet-cl

FROM debian:trixie-20261005-slim
RUN apt-get update && apt-get install -y --no-install-recommends libqt6core6t64 libqt6network6 && rm -rf /var/lib/apt/lists/*
COPY --from=build /tmp/fet-7.12.0/fet-cl /usr/local/bin/fet-cl
COPY --from=build /tmp/fet-7.12.0/translations/ /usr/local/share/fet/translations/
WORKDIR /work
ENTRYPOINT ["fet-cl"]

ثم ابنه وتأكد من الإصدار:

docker build -t fet:7.12.0 .
docker run --rm fet:7.12.0 --version
FET version 7.12.0
Free timetabling software, licensed under the GNU Affero General Public License version 3
Copyright (C) 2002-2026 Liviu Lalescu, Volker Dirr
Homepage: https://lalescu.ro/liviu/fet/
This program uses Qt version 6.8.2, Copyright (C) The Qt Company Ltd and other contributors.

في الإعداد أعلاه لاحظ التالي:

  • البناء على مرحلتين Multi-stage Build: المترجم g++ وملفات Qt للتطوير في المرحلة الأولى فقط، والمرحلة الثانية فيها البرنامج ومكتبة Qt Core التي يحتاجها، وأمر ldd في آخر البناء يطبع المكتبات التي يرتبط بها البرنامج، فلا توجد فيها مكتبة واجهة رسومية، لأن الملف src-cl.pro يحذف وحدة gui.
  • الترجمة العربية: الأداة lrelease طبعت Generated 13770 translation(s) (13770 finished and 0 unfinished) لملف العربية، أي أن كل نصوص البرنامج مترجمة.
  • مكان ملفات الترجمة: البرنامج يبحث عن fet_ar.qm بجانب الملف التنفيذي، ثم في المجلد translations بجانبه، ثم في /usr/local/share/fet/translations، لذلك نسخناها إلى المكان الأخير، وسوف ترى في قسم المشكلات ما يحدث إذا وضعتها في مكان آخر.
  • الوقت: البناء يحتاج دقائق لأن كود البرنامج كبير، ولكنك تبنيه مرة واحدة لكل إصدار.

وصف المدرسة في ملف school.fet

لنفرض أن لديك مدرسة متوسطة فيها ثلاثة صفوف، ولكل صف فصلان، والأسبوع خمسة أيام من الأحد إلى الخميس، وفي اليوم سبع حصص. والجدول التالي يلخص ما سوف نعطيه للبرنامج:

العنصرالقيمة في المثال
الأيام والحصص Days / Hours5 أيام × 7 حصص = 35 فترة، تبدأ 07:00 وآخرها 12:20
الفصول Studentsالأول أ وب، والثاني أ وب، والثالث أ وب، في كل فصل 30 طالباً
المواد Subjects وحصصها في الأسبوعالإسلامية 5، والعربية 6، والرياضيات 6، والعلوم 4 (حصة مزدوجة وحصتان)، والإنجليزية 4، والاجتماعيات 3، والحاسب 2 (حصة مزدوجة)، والبدنية 2، والفنية 1، والمجموع 33 حصة لكل فصل
المعلمون Teachers12 معلماً، لكل مادة رئيسية معلمان يقسمان الفصول، ومعلم واحد للإنجليزية يدرس الفصول الستة (24 حصة)
القاعات Rooms6 فصول ثابتة، ومعمل العلوم، ومعمل الحاسب، والصالة الرياضية
الأنشطة Activities186 نشاطاً بعد تقسيم المواد، مجموعها 198 حصة

وأدخلنا البيانات بسكربت Python صغير يكتب الملف بصيغة FET، لأن 186 نشاطاً كثيرة على الإدخال اليدوي في مقال، ولكن في الواقع سوف تدخلها من الواجهة الرسومية أو تستوردها من ملفات CSV، فالبرنامج يأتي بأداة استيراد CSV للمعلمين والمواد والفصول والأنشطة. وهذه هي أجزاء الملف الناتج التي تهمك، وأولها النشاط، والمثال التالي حصص العلوم للأول أ:

<Activity>
  <Teacher>أ. عمر</Teacher>
  <Subject>العلوم</Subject>
  <Students>الأول أ</Students>
  <Duration>2</Duration>
  <Total_Duration>4</Total_Duration>
  <Id>103</Id>
  <Activity_Group_Id>103</Activity_Group_Id>
  <Active>true</Active>
  <Comments></Comments>
</Activity>
<Activity>
  <Teacher>أ. عمر</Teacher>
  <Subject>العلوم</Subject>
  <Students>الأول أ</Students>
  <Duration>1</Duration>
  <Total_Duration>4</Total_Duration>
  <Id>104</Id>
  <Activity_Group_Id>103</Activity_Group_Id>
  <Active>true</Active>
  <Comments></Comments>
</Activity>
<Activity>
  <Teacher>أ. عمر</Teacher>
  <Subject>العلوم</Subject>
  <Students>الأول أ</Students>
  <Duration>1</Duration>
  <Total_Duration>4</Total_Duration>
  <Id>105</Id>
  <Activity_Group_Id>103</Activity_Group_Id>
  <Active>true</Active>
  <Comments></Comments>
</Activity>

الأجزاء الثلاثة تحمل رقم المجموعة نفسه Activity_Group_Id، والأول مدته Duration حصتان متتاليتان، ومجموع المدة Total_Duration أربع حصص.

القيود Constraints التي أضفناها

القيود قسمان في الملف: قيود الوقت Time Constraints وقيود المكان Space Constraints، وفي كل قسم قيد أساسي Basic Compulsory لا يحذف، وهو الذي يمنع أن يكون المعلم أو الفصل أو القاعة في مكانين في الوقت نفسه. وأضفنا فوقه ما يلي، والأول معلم الإنجليزية الذي يخرج مبكراً يوم الخميس:

<ConstraintTeacherNotAvailableTimes>
  <Weight_Percentage>100</Weight_Percentage>
  <Teacher>أ. هشام</Teacher>
  <Number_of_Not_Available_Times>3</Number_of_Not_Available_Times>
  <Not_Available_Time>
    <Day>الخميس</Day>
    <Hour>10:40</Hour>
  </Not_Available_Time>
  <Not_Available_Time>
    <Day>الخميس</Day>
    <Hour>11:30</Hour>
  </Not_Available_Time>
  <Not_Available_Time>
    <Day>الخميس</Day>
    <Hour>12:20</Hour>
  </Not_Available_Time>
  <Active>true</Active>
  <Comments></Comments>
</ConstraintTeacherNotAvailableTimes>

ثم توزيع حصص المادة الواحدة على أيام مختلفة، وهو قيد مرن بوزن 95%، وفي الملف 42 قيداً منه، واحد لكل مادة مقسمة في كل فصل:

<ConstraintMinDaysBetweenActivities>
  <Weight_Percentage>95</Weight_Percentage>
  <Consecutive_If_Same_Day>true</Consecutive_If_Same_Day>
  <Number_of_Activities>3</Number_of_Activities>
  <Activity_Id>103</Activity_Id>
  <Activity_Id>104</Activity_Id>
  <Activity_Id>105</Activity_Id>
  <MinDays>1</MinDays>
  <Active>true</Active>
  <Comments></Comments>
</ConstraintMinDaysBetweenActivities>

ثم يوم الطالب دون فجوات Gaps ويبدأ من الحصة الأولى، وألا يكون للمعلم أكثر من فترتين فارغتين في اليوم:

<ConstraintStudentsMaxGapsPerWeek>
  <Weight_Percentage>100</Weight_Percentage>
  <Max_Gaps>0</Max_Gaps>
  <Active>true</Active>
  <Comments></Comments>
</ConstraintStudentsMaxGapsPerWeek>
<ConstraintStudentsEarlyMaxBeginningsAtSecondHour>
  <Weight_Percentage>100</Weight_Percentage>
  <Max_Beginnings_At_Second_Hour>0</Max_Beginnings_At_Second_Hour>
  <Active>true</Active>
  <Comments></Comments>
</ConstraintStudentsEarlyMaxBeginningsAtSecondHour>
<ConstraintTeachersMaxGapsPerDay>
  <Weight_Percentage>100</Weight_Percentage>
  <Max_Gaps>2</Max_Gaps>
  <Active>true</Active>
  <Comments></Comments>
</ConstraintTeachersMaxGapsPerDay>

ثم قيود المكان، فلكل فصل قاعته الثابتة Home Room، والعلوم في المعمل، ومثلها الحاسب في معمله والبدنية في الصالة:

<ConstraintStudentsSetHomeRoom>
  <Weight_Percentage>100</Weight_Percentage>
  <Students>الأول أ</Students>
  <Room>فصل 1</Room>
  <Active>true</Active>
  <Comments></Comments>
</ConstraintStudentsSetHomeRoom>
<ConstraintSubjectPreferredRoom>
  <Weight_Percentage>100</Weight_Percentage>
  <Subject>العلوم</Subject>
  <Room>معمل العلوم</Room>
  <Active>true</Active>
  <Comments></Comments>
</ConstraintSubjectPreferredRoom>

في الإعداد أعلاه لاحظ التالي:

  • القيد Consecutive_If_Same_Day يقول للبرنامج: إذا اضطررت أن تضع حصتين من المادة في يوم واحد فاجعلهما متتاليتين، وهذا ما يحدث حتماً في العربية والرياضيات، لأن ست حصص لا تتوزع على خمسة أيام دون أن يتكرر يوم.
  • حصة العلوم تذهب إلى المعمل رغم أن للفصل قاعته الثابتة، والسبب أن القاعة المفضلة Preferred Room تتقدم على القاعة الثابتة Home Room، وواجهة البرنامج نفسها تنبه إلى ذلك عند إضافة القيد، فاحذر من أن تعطي مادة قاعة مفضلة لا تتسع لكل حصصها.
  • معلم العلوم أ. عمر لا يأتي يوم الأحد في المثال، ومعمل العلوم تحتاجه الفصول الستة 24 حصة من 35، وهذان القيدان معاً هما ما يجعل الجدول صعباً.

توليد الجدول

الآن نشغل البرنامج على الملف، مع تحديد العربية لغة للمخرجات، وحد أقصى للوقت --timelimitseconds حتى لا يبقى يبحث إلى الأبد إذا كانت البيانات صعبة:

docker run --rm -m 512m --cpus 2 -e LANG=C.UTF-8 -v "$PWD":/work fet:7.12.0 \
  --inputfile=school.fet --outputdir=out --language=ar --timelimitseconds=600

والمخرج سوف يكون كما يلي:

Starting timetable generation...
Total searching time (seconds): 3
Generation successful

ثلاث ثوان للبحث في هذه المحاولة، وفي محاولة سابقة على البيانات نفسها كانت 14 ثانية، والسبب أن الخوارزمية تختار عشوائياً بين الفترات المتساوية، فيختلف المسار والوقت بين تشغيل وآخر، والبرنامج يحفظ البذرة العشوائية Random Seed في الملفين school_random_seed_before.txt وschool_random_seed_after.txt إن أردت أن تكرر الحل نفسه. وفي المجلد out/timetables/school ستجد عشرات الملفات، أهمها:

  • school_groups_days_horizontal.html: جدول كل فصل، وهو الذي تطبعه وتعلقه.
  • school_teachers_days_horizontal.html: جدول كل معلم، ومعه school_teachers_free_periods_days_horizontal.html للفترات الفارغة، وهو مفيد عند البحث عن معلم بديل.
  • school_rooms_days_horizontal.html: جدول كل قاعة.
  • school_activities.xml: لكل نشاط اليوم والحصة والقاعة، وهو الملف الذي تقرؤه الأنظمة الأخرى.
  • school_soft_conflicts.txt: القيود المرنة التي لم تتحقق، ولماذا.
  • school_data_and_timetable.fet: البيانات نفسها مع الجدول مثبتاً فيها، فتفتحه في الواجهة الرسومية وتعدل عليه.

والصورة التالية لجدول الأول أ كما خرج من البرنامج، والصفحات تخرج بالعربية ومن اليمين إلى اليسار لأن الترجمة العربية تضع dir="rtl" في الصفحة:

جدول فصل الأول أ من مخرجات FET بالعربية: الأيام من الأحد إلى الخميس في الأعمدة والحصص من 07:00 إلى 12:20 في الصفوف، وفي كل خانة المادة والمعلم والقاعة، مثل الإنجليزية أ. هشام فصل 1، والعلوم أ. عمر معمل العلوم حصة مزدوجة يوم الاثنين في 10:40 و11:30، والحاسب أ. سعد معمل الحاسب حصة مزدوجة يوم الخميس، والفترتان الفارغتان في آخر يومي الاثنين والأربعاء
جدول الأول أ: 33 حصة، والفترتان الفارغتان في آخر اليوم، والحصص المزدوجة في المعمل

لاحظ أن الفترتين اللتين لا توجد فيهما حصص جاءتا في آخر يومي الاثنين والأربعاء، وهذا أثر القيدين: لا فجوات، والبداية من الحصة الأولى. والصورة التالية لجدول معلم الإنجليزية، وفيها العلامة -x- في الفترات التي قلنا إنه غير متاح فيها:

جدول المعلم أ. هشام من مخرجات FET: 24 حصة إنجليزية للفصول الستة موزعة على الأيام الخمسة، وفي خانات يوم الخميس من 10:40 إلى 12:20 العلامة -x- لأنه غير متاح، وفي الخانات الفارغة ثلاث شرطات
جدول معلم الإنجليزية: 24 حصة، والعلامة -x- في الفترات التي لا يأتي فيها

وأخيراً جدول معمل العلوم، وهو أكثر قاعة مزدحمة في المدرسة:

جدول قاعة معمل العلوم من مخرجات FET: 24 حصة للفصول الستة مع المعلمين أ. عمر وأ. طارق، والحصص المزدوجة تظهر في خانة طويلة تمتد حصتين، ويوم الأحد كل حصصه مع أ. طارق لأن أ. عمر لا يأتي يوم الأحد
معمل العلوم: 24 حصة من 35، ويوم الأحد كله لمعلم واحد لأن الآخر لا يأتي فيه

ماذا عن القيود المرنة؟

افتح الملف school_soft_conflicts.txt، وستجد في أوله عدد القيود المرنة التي لم تتحقق، وكان في هذا التشغيل 13 قيداً، ومجموع التعارضات المرنة 12.35، وكل سطر بعدها يذكر الحصتين اللتين وضعتا في يوم واحد. وقد يتساءل البعض: هل هذا خطأ في الجدول؟ والإجابة لا، فالعربية والرياضيات ست حصص في خمسة أيام لكل فصل من الفصول الستة، أي 12 حالة على الأقل لا يمكن فيها توزيع المادة على أيام مختلفة، والبرنامج وضع الحصتين متتاليتين كما طلبنا. وإذا رأيت في هذا الملف مادة لا يتوقع فيها التكرار، مثل الإنجليزية بأربع حصص، فارفع وزن قيدها وأعد التوليد.

التحقق من النجاح

رسالة Generation successful تقول إن كل القيود الإلزامية تحققت، ولكن لا تقم بتسليم جدول لمدرسة بناءً على رسالة البرنامج وحدها، والسبب أن الخطأ الأكثر حدوثاً ليس في البرنامج وإنما في البيانات، مثل معلم مكرر باسمين أو قيد نسيت تفعيله. لذلك كتبنا سكربت Python مستقلاً يقرأ ملف البيانات وملف school_activities.xml معاً، ويعد التعارضات بنفسه:

# Independent check of the FET result: joins the input (who/what/duration) with the output (day/hour/room)
# and counts clashes for teachers, classes and rooms, gaps in the class day, and the load of each special room.
import sys, collections, xml.etree.ElementTree as ET
sys.stdout.reconfigure(encoding='utf-8')
inp = ET.parse(sys.argv[1]).getroot()
out = ET.parse(sys.argv[2]).getroot()
hours = [h.findtext('Name') for h in inp.find('Hours_List').findall('Hour')]
acts = {a.findtext('Id'): a for a in inp.find('Activities_List').findall('Activity')}
busy = collections.defaultdict(list)          # (kind, name, day, hour) -> activity ids
room_load = collections.Counter()
class_slots = collections.defaultdict(set)    # (class, day) -> hour indexes
na = {(c.findtext('Teacher'), t.findtext('Day'), t.findtext('Hour'))
      for c in inp.iter('ConstraintTeacherNotAvailableTimes') for t in c.findall('Not_Available_Time')}
na_hits = 0
for a in out.findall('Activity'):
    src = acts[a.findtext('Id')]
    day, start, room = a.findtext('Day'), hours.index(a.findtext('Hour')), a.findtext('Room')
    for k in range(int(src.findtext('Duration'))):
        h = hours[start + k]
        busy[('teacher', src.findtext('Teacher'), day, h)].append(a.findtext('Id'))
        busy[('class', src.findtext('Students'), day, h)].append(a.findtext('Id'))
        busy[('room', room, day, h)].append(a.findtext('Id'))
        class_slots[(src.findtext('Students'), day)].add(start + k)
        room_load[room] += 1
        na_hits += (src.findtext('Teacher'), day, h) in na
clashes = collections.Counter(k[0] for k, v in busy.items() if len(v) > 1)
gaps = sum(max(s) - min(s) + 1 - len(s) for s in class_slots.values())
late = sum(1 for s in class_slots.values() if min(s) > 0)
print(f"activities placed: {len(out.findall('Activity'))} of {len(acts)}")
print(f"clashes: teacher={clashes['teacher']} class={clashes['class']} room={clashes['room']}")
print(f"teacher not-available slots used: {na_hits}")
print(f"class gaps: {gaps}, class days not starting at period 1: {late}")
for r in ['معمل العلوم', 'معمل الحاسب', 'الصالة الرياضية']:
    print(f"{r}: {room_load[r]} of {len(hours) * len(inp.find('Days_List').findall('Day'))} periods")
docker run --rm -v "$PWD":/work -w /work python:3.13.16-slim \
  python -I check_clashes.py school.fet out/timetables/school/school_activities.xml

والمخرج سوف يكون كما يلي:

activities placed: 186 of 186
clashes: teacher=0 class=0 room=0
teacher not-available slots used: 0
class gaps: 0, class days not starting at period 1: 0
معمل العلوم: 24 of 35 periods
معمل الحاسب: 12 of 35 periods
الصالة الرياضية: 12 of 35 periods

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

ماذا يحدث إذا كانت البيانات مستحيلة؟

لنفرض أن معلم الإنجليزية أخبرك أنه لن يأتي يومي الأربعاء والخميس، ولم تنتبه إلى أن عنده 24 حصة، فغيرنا قيد عدم التوفر ليشمل اليومين كاملين وشغلنا البرنامج مرة أخرى:

docker run --rm -m 512m --cpus 2 -e LANG=C.UTF-8 -v "$PWD":/work fet:7.12.0 \
  --inputfile=school-impossible.fet --outputdir=out-bad --language=ar --timelimitseconds=120
echo "exit=$?"
Cannot precompute - data is wrong - aborting
exit=1

توقف البرنامج في أقل من ثانيتين دون أن يبدأ البحث، والتفاصيل في الملف out-bad/logs/errors.txt:

العنوان:FET تحذير
الرسالة:لا يمكن مقاربة الحل للمدرس أ. هشام، لأن عدد الساعات للمدرس هو 24 ولديك فقط 21 فترة(رات) حرة من قبل القيود مدرس غير متاح و/أو التوقف.

وهذه من أفضل ميزات البرنامج، فهو يحسب قبل البحث ما إذا كانت البيانات ممكنة أصلاً، والرسالة تقول بوضوح: 24 حصة و21 فترة متاحة، والحل إما أن يأتي المعلم يوماً آخر، أو أن تعطي بعض حصصه لمعلم ثان. ولاحظ أن الترجمة العربية فيها بعض العبارات غير الواضحة مثل «لا يمكن مقاربة الحل»، والمقصود أنه لا يوجد حل، والرقمان هما المهم في الرسالة. وأما إذا كانت البيانات ممكنة حسابياً ولكنها صعبة جداً، فلن تظهر هذه الرسالة، وسوف يبحث البرنامج حتى ينتهي الوقت، وعندها افتح الملف logs/max_placed_activities.txt، وملف README يشرح القاعدة: إذا وصل البرنامج إلى n نشاطاً كحد أقصى، فانظر إلى النشاط رقم n+1 في الملف logs/initial_order.txt، فهو أو معلمه أو فصله غالباً سبب المشكلة.

ربط FET بنظام شؤون الطلاب SIS

FET لا يعرف شيئاً عن الطلاب بأسمائهم ولا عن التسجيل، فهو يأخذ الفصول والمعلمين والمواد ويعيد الجدول، لذلك مكانه الطبيعي بجانب نظام شؤون الطلاب، كما في مقال نظام شؤون الطلاب المفتوح المصدر:

  1. صدر من ال SIS المعلمين والمواد والفصول وعدد الحصص بملفات CSV، واستوردها في FET.
  2. أضف القيود في الواجهة الرسومية، وهي معرفة لا توجد في ال SIS، مثل الأيام التي لا يأتي فيها كل معلم.
  3. ولد الجدول وراجعه مع الإدارة، ثم ثبت ما اتفقتم عليه بالقفل Lock في الواجهة، حتى لا يتغير في التوليد التالي.
  4. اقرأ activities.xml بسكربت يدخل الجدول في ال SIS أو في منصة التعلم، أو انشر صفحات HTML كما هي.

النسخ الاحتياطي والاستعادة

كل ما تحتاج أن تحفظه هو ملف .fet، لأن كل المخرجات تعاد من جديد بأمر واحد، وأفضل مكان له مستودع Git، فكل تعديل على البيانات أو القيود يصبح مسجلاً مع تاريخه وسببه، وتستطيع الرجوع إلى جدول الفصل الماضي بالأمر git checkout. واحفظ معه أيضاً ملف school_data_and_timetable.fet الذي يخرج مع كل جدول اعتمدته، لأنه الجدول نفسه، فإذا أعدت التوليد بالبيانات نفسها قد يخرج جدول مختلف بسبب العشوائية.

التحديث إلى إصدار أحدث

FET يصدر إصدارات كثيرة، وسجل التغييرات ChangeLog في الكود يذكر ما تغير في كل إصدار. لتحديث نسخة Docker غير رقم الإصدار في أمر التنزيل وفي ملف Dockerfile، وتأكد من البصمة على صفحة التنزيل، ثم ابن Image جديداً برقم جديد، وشغله على ملفك الحالي قبل أن تحذف القديم، وقارن الرسالة النهائية وعدد القيود المرنة التي لم تتحقق. والبرنامج يقرأ ملفات الإصدارات القديمة ويحفظها بصيغته الجديدة، لذلك احتفظ بنسخة من الملف قبل أن تفتحه في إصدار أحدث.

مشكلات شائعة وحلولها

  • المخرجات بالإنجليزية رغم --language=ar: في أول تشغيل لنا كانت ملفات الترجمة في /usr/share/fet/translations، فكتب البرنامج في logs/warnings.txt أنه بحث عن fet_ar.qm في /usr/local/bin ثم /usr/local/bin/translations ثم /usr/local/share/fet/translations ولم يجده، فرجع إلى الإنجليزية، والحل أن تضع الملفات في أحد هذه الأماكن كما في ملف Dockerfile أعلاه.
  • رسالة Detected locale "C" في أول المخرج: Qt تحتاج إعداد لغة UTF-8، وال Image الصغير debian-slim ليس فيه إعداد، فأضف -e LANG=C.UTF-8 إلى الأمر.
  • البحث لا ينتهي: ضع دائماً --timelimitseconds، ثم اقرأ max_placed_activities.txt كما شرحنا، وخفف قيداً واحداً في كل مرة، وابدأ بالقيود الإلزامية التي لا تحتاجها فعلاً، فكثير من القيود التي تكتب بوزن 100% هي في الحقيقة رغبات.
  • النتيجة تتغير في كل تشغيل: هذا طبيعي بسبب العشوائية، فإذا اعتمدت جدولاً فاحفظ ملف data_and_timetable الخاص به، ولا تعتمد على إعادة التوليد.
  • الأسماء العربية في الملفات: عملت أسماء المعلمين والمواد والقاعات العربية في كل المخرجات دون مشكلة، ولكن أسماء الملفات نفسها تأخذ اسم ملف البيانات، لذلك اجعله بالإنجليزية مثل school.fet حتى يسهل التعامل معه في السكربتات.

تدريب عملي

  • اجعل حصص العلوم ست حصص لكل فصل بدلاً من أربع، أي 36 حصة في معمل واحد فيه 35 فترة، وشغل البرنامج واقرأ رسالة الخطأ.
  • اطلب أن تكون حصة البدنية في آخر اليوم فقط بقيد من نوع الأوقات المفضلة للنشاط، وقارن وقت البحث قبل القيد وبعده.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • الجدول الدراسي مشكلة بحث كبيرة، والحل اليدوي أو بالترتيب يتوقف عند أول حصة بلا مكان، وFET يحلها بخوارزمية التبديل المتكرر.
  • البيانات كلها في ملف .fet واحد: الأيام والحصص والمواد والمعلمون والفصول والقاعات والأنشطة والقيود، وهو الملف الوحيد الذي تحتاج أن تحفظه.
  • القيد بوزن 100% إلزامي، وأقل من ذلك رغبة، وملف soft_conflicts يخبرك بما لم يتحقق ولماذا.
  • مدرسة فيها 6 فصول و12 معلماً و198 حصة احتاجت ثواني، والفحص المستقل أكد عدم وجود أي تعارض أو فجوة.
  • إذا كانت البيانات مستحيلة يتوقف FET قبل البحث ويذكر السبب بالأرقام، فاقرأ logs/errors.txt أولاً.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال وتجربته على FET 7.12.0 مبنياً من الكود مع Qt 6.8.2 على Debian 13 في Docker.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.