تعرف على تاريخ ميلادك الحقيقي: الحساب الفلكي والتحليلات الجغرافية المكانية والزمانية في بيثون


يخططون لاحتفالات عيد ميلاد العام المقبل لثلاثة أصدقاء: غابرييل، وجاك، وكاميل. ولد الثلاثة جميعهم في عام 1996، في باريس، فرنسا، لذا سيكونون بعمر 30 عامًا في العام المقبل في عام 2026. ومن المصادفة أن يكون غابرييل وجاك في باريس في عيد ميلاد كل منهما، بينما ستكون كاميل في طوكيو، اليابان، خلال عيد ميلادها. يميل غابرييل وكاميل إلى الاحتفال بأعياد ميلادهما في أي سنة معينة في الأيام “الرسمية” المذكورة في شهادات ميلادهما – 18 يناير و5 مايو على التوالي. جاك، الذي ولد في 29 فبراير، يفضل الاحتفال بعيد ميلاده (أو الذكرى المدنية) في 1 مارس في السنوات غير الكبيسة.

نحن نستخدم سنوات كبيسة للحفاظ على تقويمنا متزامنًا مع مدار الأرض حول الشمس. أ السنة الشمسية – الوقت الذي تستغرقه الأرض لإكمال دورة كاملة حول الشمس – هو حوالي 365.25 يومًا. حسب التقليد، يخصص التقويم الغريغوري 365 يومًا لكل عام، باستثناء السنوات الكبيسة، التي تحصل على 366 يومًا للتعويض عن الانحراف الجزئي بمرور الوقت. وهذا يجعلك تتساءل: هل سيحتفل أي من أصدقائك بعيد ميلاده في الذكرى “الحقيقية” ليوم ميلاده، أي اليوم الذي ستكون فيه الشمس في نفس موضعها في السماء (بالنسبة إلى الأرض) عندما ولدوا؟ هل يمكن أن ينتهي الأمر بأصدقائك إلى الاحتفال ببلوغهم الثلاثين عامًا – وهو إنجاز خاص – في يوم مبكر جدًا أو يوم متأخر جدًا؟

تستخدم المقالة التالية مشكلة عيد الميلاد هذه لتعريف القراء ببعض حزم بايثون لعلوم البيانات مفتوحة المصدر المثيرة للاهتمام والقابلة للتطبيق على نطاق واسع للحساب الفلكي والتحليلات الجغرافية المكانية والزمانية، بما في ذلك skyfield, timezonefinder, geopy، و pytz. لاكتساب خبرة عملية، سوف نستخدم هذه الحزم لحل مشكلتنا الممتعة المتمثلة في التنبؤ الدقيق بـ “عيد الميلاد الحقيقي” (أو تاريخ عودة الشمسية) في سنة مقبلة معينة. سنناقش بعد ذلك كيف يمكن الاستفادة من هذه الحزم في تطبيقات الحياة الواقعية الأخرى.

توقع عيد ميلاد حقيقي

إعداد المشروع

تم اختبار جميع خطوات التنفيذ أدناه على نظام التشغيل macOS Sequoia 15.6.1 ويجب أن تكون متشابهة تقريبًا على نظامي التشغيل Linux وWindows.

لنبدأ بإعداد دليل المشروع. سوف نستخدم uv لإدارة المشروع (انظر تعليمات التثبيت هنا). تحقق من الإصدار المثبت في المحطة:

uv --version

تهيئة دليل المشروع يسمى real-birthday-predictor في مكان مناسب على جهازك المحلي:

uv init --bare real-birthday-predictor

في دليل المشروع، قم بإنشاء ملف requirements.txt الملف مع التبعيات التالية:

skyfield==1.53
timezonefinder==8.0.0
geopy==2.4.1
pytz==2025.2

وفيما يلي لمحة موجزة عن كل من هذه الحزم:

  • skyfield يوفر وظائف للحساب الفلكي. ويمكن استخدامه لحساب المواقع الدقيقة للأجرام السماوية (مثل الشمس والقمر والكواكب والأقمار الصناعية) للمساعدة في تحديد أوقات الارتفاع/الغروب والكسوف والمسارات المدارية. ويعتمد على ما يسمى التقويمات الفلكية (جداول البيانات الموضعية لمختلف الأجرام السماوية تم استقراءها على مدى سنوات عديدة)، والتي تحتفظ بها منظمات مثل مختبر الدفع النفاث التابع لناسا (JPL). في هذه المقالة، سنستخدم ملف التقويم الفلكي DE421 خفيف الوزن، والذي يغطي التواريخ من 29 يوليو 1899 إلى 9 أكتوبر 2053.
  • timezonefinder يحتوي على وظائف لتعيين الإحداثيات الجغرافية (خطوط الطول والعرض) للمناطق الزمنية (على سبيل المثال، “أوروبا/باريس”). ويمكنه القيام بذلك دون الاتصال بالإنترنت.
  • geopy يوفر وظائف للتحليلات الجغرافية المكانية، مثل رسم الخرائط بين العناوين والإحداثيات الجغرافية. سوف نستخدمها مع Nominatim المشفر الجغرافي لبيانات OpenStreetMap لتعيين أسماء المدن والبلدان للإحداثيات.
  • pytz يوفر وظائف للتحليلات الزمنية وتحويل المنطقة الزمنية. سنستخدمه للتحويل بين التوقيت العالمي المنسق (UTC) والتوقيت المحلي باستخدام قواعد التوقيت الصيفي الإقليمية.

سوف نستخدم أيضًا بعض الوحدات المضمنة الأخرى، مثل datetime لتحليل ومعالجة قيم التاريخ/الوقت، calendar للتحقق من السنوات الكبيسة، و time للنوم بين محاولات إعادة الترميز الجغرافي.

بعد ذلك، قم بإنشاء بيئة Python 3.12 افتراضية داخل دليل المشروع، وقم بتنشيط البيئة، وتثبيت التبعيات:

uv venv --python=3.12 
source .venv/bin/activate
uv add -r requirements.txt

تأكد من تثبيت التبعيات:

uv pip list

تطبيق

في هذا القسم، سنتعرف قطعة تلو الأخرى على الكود الخاص بالتنبؤ بتاريخ ووقت عيد الميلاد “الحقيقي” في سنة مقبلة معينة ومكان الاحتفال. أولاً، نقوم باستيراد الوحدات الضرورية:

from datetime import datetime, timedelta
from skyfield.api import load, wgs84
from timezonefinder import TimezoneFinder
from geopy.geocoders import Nominatim
from geopy.exc import GeocoderTimedOut
import pytz
import calendar
import time

ثم نحدد الطريقة باستخدام أسماء متغيرات ذات معنى ونص مستندي:

def get_real_birthday_prediction(
    official_birthday: str,
    official_birth_time: str,
    birth_country: str,
    birth_city: str,
    current_country: str,
    current_city: str,
    target_year: str = None
):
    """
    Predicts the "real" birthday (solar return) for a given year,
    accounting for the time zone at the birth location and the time zone
    at the current location. Uses March 1 in non-leap years for the civil 
    anniversary if the official birth date is February 29.
    """

لاحظ أن current_country و current_city قم بالإشارة بشكل مشترك إلى الموقع الذي سيتم فيه الاحتفال بعيد الميلاد في السنة المستهدفة.

نحن نتحقق من صحة المدخلات قبل العمل معهم:

    # Determine target year
    if target_year is None:
        target_year = datetime.now().year
    else:
        try:
            target_year = int(target_year)
        except ValueError:
            raise ValueError(f"Invalid target year '{target_year}'. Please use 'yyyy' format.")

    # Validate and parse birth date
    try:
        birth_date = datetime.strptime(official_birthday, "%d-%m-%Y")
    except ValueError:
        raise ValueError(
            f"Invalid birth date '{official_birthday}'. "
            "Please use 'dd-mm-yyyy' format with a valid calendar date."
        )

    # Validate and parse birth time
    try:
        birth_hour, birth_minute = map(int, official_birth_time.split(":"))
    except ValueError:
        raise ValueError(
            f"Invalid birth time '{official_birth_time}'. "
            "Please use 'hh:mm' 24-hour format."
        )

    if not (0 <= birth_hour <= 23):
        raise ValueError(f"Hour '{birth_hour}' is out of range (0-23).")
    if not (0 <= birth_minute <= 59):
        raise ValueError(f"Minute '{birth_minute}' is out of range (0-59).")

التالي نستخدم geopy مع Nominatim المشفر الجغرافي للتأكد من الميلاد والمواقع الحالية. لتجنب حدوث أخطاء المهلة، قمنا بتعيين قيمة مهلة طويلة إلى حد معقول تبلغ عشر ثوانٍ؛ هذه هي المدة لدينا safe_geocode تنتظر الوظيفة استجابة خدمة الترميز الجغرافي قبل رفع ملف geopy.exc.GeocoderTimedOut استثناء. لكي تكون أكثر أمانًا، تحاول الوظيفة إجراء البحث ثلاث مرات مع تأخير لمدة ثانية واحدة قبل الاستسلام:

    geolocator = Nominatim(user_agent="birthday_tz_lookup", timeout=10)

    # Helper function to call geocode API with retries
    def safe_geocode(query, retries=3, delay=1):
        for attempt in range(retries):
            try:
                return geolocator.geocode(query)
            except GeocoderTimedOut:
                if attempt < retries - 1:
                    time.sleep(delay)
                else:
                    raise RuntimeError(
                        f"Could not retrieve location for '{query}' after {retries} attempts. "
                        "The geocoding service may be slow or unavailable. Please try again later."
                    )
    
    birth_location = safe_geocode(f"{birth_city}, {birth_country}")
    current_location = safe_geocode(f"{current_city}, {current_country}")

    if not birth_location or not current_location:
        raise ValueError("Could not find coordinates for one of the locations. Please check spelling.")

باستخدام الإحداثيات الجغرافية للولادة والمواقع الحالية، نحدد المناطق الزمنية المعنية وتاريخ ووقت الميلاد بالتوقيت العالمي المنسق (UTC). نفترض أيضًا أن الأفراد مثل جاك، الذين ولدوا في 29 فبراير، سيفضلون الاحتفال بعيد ميلادهم في 1 مارس في السنوات غير الكبيسة:

    # Get time zones
    tf = TimezoneFinder()
    birth_tz_name = tf.timezone_at(lng=birth_location.longitude, lat=birth_location.latitude)
    current_tz_name = tf.timezone_at(lng=current_location.longitude, lat=current_location.latitude)

    if not birth_tz_name or not current_tz_name:
        raise ValueError("Could not determine timezone for one of the locations.")

    birth_tz = pytz.timezone(birth_tz_name)
    current_tz = pytz.timezone(current_tz_name)

    # Set civil anniversary date to March 1 for February 29 birthdays in non-leap years
    birth_month, birth_day = birth_date.month, birth_date.day
    if (birth_month, birth_day) == (2, 29):
        if not calendar.isleap(birth_date.year):
            raise ValueError(f"{birth_date.year} is not a leap year, so February 29 is invalid.")
        civil_anniversary_month, civil_anniversary_day = (
            (3, 1) if not calendar.isleap(target_year) else (2, 29)
        )
    else:
        civil_anniversary_month, civil_anniversary_day = birth_month, birth_day

    # Parse birth datetime in birth location's local time
    birth_local_dt = birth_tz.localize(datetime(
        birth_date.year, birth_month, birth_day,
        birth_hour, birth_minute
    ))
    birth_dt_utc = birth_local_dt.astimezone(pytz.utc)

باستخدام بيانات التقويم الفلكي DE421، نحسب مكان وجود الشمس (أي موقعها خط الطول مسير الشمس) في الوقت المحدد والمكان الذي ولد فيه الفرد:

    # Load ephemeris data and get Sun's ecliptic longitude at birth
    eph = load("de421.bsp")  # Covers dates 1899-07-29 through 2053-10-09
    ts = load.timescale()
    sun = eph["sun"]
    earth = eph["earth"]
    t_birth = ts.utc(birth_dt_utc.year, birth_dt_utc.month, birth_dt_utc.day,
                     birth_dt_utc.hour, birth_dt_utc.minute, birth_dt_utc.second)
    
    # Birth longitude in tropical frame from POV of birth observer on Earth's surface
    birth_observer = earth + wgs84.latlon(birth_location.latitude, birth_location.longitude)
    ecl = birth_observer.at(t_birth).observe(sun).apparent().ecliptic_latlon(epoch="date")
    birth_longitude = ecl[1].degrees

لاحظ أنه في المرة الأولى السطر eph = load("de421.bsp") يتم تنفيذه، de421.bsp سيتم تنزيل الملف ووضعه في دليل المشروع؛ وفي جميع عمليات التنفيذ المستقبلية، سيتم استخدام الملف الذي تم تنزيله مباشرة. من الممكن أيضًا تعديل التعليمات البرمجية لتحميل ملف تقويم فلكي آخر (على سبيل المثال، de440s.bsp، والذي يغطي السنوات حتى 22 يناير 2150).

الآن يأتي جزء مثير للاهتمام من الوظيفة: سنقوم بإجراء تخمين أولي لتاريخ ووقت عيد الميلاد “الحقيقي” في السنة المستهدفة، وتحديد الحدود العليا والدنيا الآمنة لقيمة التاريخ والوقت الحقيقيين (على سبيل المثال، يومين على جانبي التخمين الأولي)، وإجراء بحث ثنائي مع التوقف المبكر للوصول بكفاءة إلى القيمة الحقيقية:

    # Initial guess for target year solar return
    approx_dt_local_birth_tz = birth_tz.localize(datetime(
        target_year, civil_anniversary_month, civil_anniversary_day,
        birth_hour, birth_minute
    ))
    approx_dt_utc = approx_dt_local_birth_tz.astimezone(pytz.utc)

    # Compute Sun longitude from POV of current observer on Earth's surface
    current_observer = earth + wgs84.latlon(current_location.latitude, current_location.longitude)

    def sun_longitude_at(dt):
        t = ts.utc(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
        ecl = current_observer.at
        return ecl[1].degrees

    def angle_diff(a, b):
        return (a - b + 180) % 360 - 180

    # Set safe upper and lower bounds for search space
    dt1 = approx_dt_utc - timedelta(days=2)
    dt2 = approx_dt_utc + timedelta(days=2)

    # Use binary search with early-stopping to solve for exact solar return in UTC
    old_angle_diff = 999
    for _ in range(50):
        mid = dt1 + (dt2 - dt1) / 2
        curr_angle_diff = angle_diff(sun_longitude_at(mid), birth_longitude)
        if old_angle_diff == curr_angle_diff:  # Early-stopping condition
            break
        if curr_angle_diff > 0:
            dt2 = mid
        else:
            dt1 = mid
        old_angle_diff = curr_angle_diff

    real_dt_utc = dt1 + (dt2 - dt1) / 2

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

أخيرًا، يتم تحويل تاريخ ووقت عيد الميلاد “الحقيقي” المحدد بواسطة البحث الثنائي إلى المنطقة الزمنية للموقع الحالي، وتنسيقه حسب الحاجة، وإعادته:

    # Convert to current location's local time and format output
    real_dt_local_current = real_dt_utc.astimezone(current_tz)
    date_str = real_dt_local_current.strftime("%d/%m")
    time_str = real_dt_local_current.strftime("%H:%M")

    return date_str, time_str, current_tz_name

اختبار

نحن الآن في وضع يسمح لنا بالتنبؤ بأعياد الميلاد “الحقيقية” لغابرييل وجاك وكاميل في عام 2026.

لتسهيل استيعاب مخرجات الدالة، إليك دالة مساعدة سنستخدمها لطباعة نتائج كل استعلام بشكل جميل:

def print_real_birthday(
    official_birthday: str,
    official_birth_time: str,
    birth_country: str,
    birth_city: str,
    current_country: str,
    current_city: str,
    target_year: str = None):
    """Pretty-print output while hiding verbose error traces."""

    print("Official birthday and time:", official_birthday, "at", official_birth_time)

    try:
        date_str, time_str, current_tz_name = get_real_birthday_prediction(
            official_birthday,
            official_birth_time,
            birth_country,
            birth_city,
            current_country,
            current_city,
            target_year
        )

        print(f"In year {target_year}, your real birthday is on {date_str} at {time_str} ({current_tz_name})\n")

    except ValueError as e:
        print("Error:", e)

فيما يلي حالات الاختبار:

# Gabriel
print_real_birthday(
    official_birthday="18-01-1996", 
    official_birth_time="02:30",
    birth_country="France",
    birth_city="Paris",
    current_country="France",
    current_city="Paris",
    target_year="2026"
)

# Jacques
print_real_birthday(
    official_birthday="29-02-1996", 
    official_birth_time="05:45",
    birth_country="France",
    birth_city="Paris",
    current_country="France",
    current_city="Paris",
    target_year="2026"
)

# Camille
print_real_birthday(
    official_birthday="05-05-1996", 
    official_birth_time="20:30",
    birth_country="Paris",
    birth_city="France",
    current_country="Japan",
    current_city="Tokyo",
    target_year="2026"
)

وهنا النتائج:

Official birthday and time: 18-01-1996 at 02:30
In year 2026, your real birthday is on 17/01 at 09:21 (Europe/Paris)

Official birthday and time: 29-02-1996 at 05:45
In year 2026, your real birthday is on 28/02 at 12:37 (Europe/Paris)

Official birthday and time: 05-05-1996 at 20:30
In year 2026, your real birthday is on 06/05 at 09:48 (Asia/Tokyo)

كما نرى، فإن عيد الميلاد “الحقيقي” (أو لحظة عودة الشمس) يختلف عن عيد الميلاد الرسمي لأصدقائك الثلاثة: يمكن أن يبدأ غابرييل وجاك نظريًا الاحتفال قبل يوم واحد من أعياد ميلادهما الرسمية في باريس، بينما يتعين على كاميل الانتظار يومًا آخر قبل الاحتفال بعيد ميلادها الثلاثين في طوكيو.

كبديل أبسط لاتباع الخطوات المذكورة أعلاه، قام مؤلف هذه المقالة بإنشاء مكتبة بايثون تسمى solarius لتحقيق نفس النتيجة (انظر التفاصيل هنا). تثبيت المكتبة مع pip install solarius أو uv add solarius واستخدامها كما هو موضح أدناه:

from solarius.model import SolarReturnCalculator

calculator = SolarReturnCalculator(ephemeris_file="de421.bsp")

# Predict without printing
date_str, time_str, tz_name = calculator.predict(
    official_birthday="18-01-1996",
    official_birth_time="02:30",
    birth_country="France",
    birth_city="Paris",
    current_country="France",
    current_city="Paris",
    target_year="2026"
)

print(date_str, time_str, tz_name)

# Or use the convenience printer
calculator.print_real_birthday(
    official_birthday="18-01-1996",
    official_birth_time="02:30",
    birth_country="France",
    birth_city="Paris",
    current_country="France",
    current_city="Paris",
    target_year="2026"
)

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

ما وراء أعياد الميلاد

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

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

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

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

في النهاية، من خلال الجمع بين هذه الحزم مفتوحة المصدر والوحدات المضمنة بطرق مختلفة، من الممكن حل المشكلات المثيرة للاهتمام التي تشمل عددًا من المجالات.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

زر الذهاب إلى الأعلى