are planning subsequent 12 months’s birthday celebrations for 3 mates: Gabriel, Jacques, and Camille. All three of them have been born in 1996, in Paris, France, so they are going to be 30 years previous subsequent 12 months in 2026. Gabriel and Jacques will occur to be in Paris on their respective birthdays, whereas Camille might be in Tokyo, Japan, throughout hers. Gabriel and Camille are likely to have fun their birthdays in any given 12 months on the “official” days talked about on their delivery certificates — January 18 and Might 5, respectively. Jacques, who was born on February 29, prefers to have fun his birthday (or civil anniversary) on March 1 in non-leap years.
We use leap years to maintain our calendar in sync with the Earth’s orbit across the Solar. A photo voltaic 12 months — the time it takes the Earth to finish one full orbit across the Solar — is roughly 365.25 days. By conference, the Gregorian calendar assigns one year to every 12 months, apart from leap years, which get three hundred and sixty six days to compensate for the fractional drift over time. This makes you surprise: will any of your mates be celebrating their birthday on the “actual” anniversary of their day of delivery, i.e., the day that the Solar might be in the identical place within the sky (relative to the Earth) because it was after they have been born? May or not it’s that your mates will find yourself celebrating turning 30 — a particular milestone — a day too quickly or a day too late?
The next article makes use of this birthday downside to introduce readers to some attention-grabbing and broadly relevant open-source knowledge science Python packages for astronomical computation and geospatial-temporal analytics, together with skyfield
, timezonefinder
, geopy
, and pytz
. To achieve hands-on expertise, we are going to use these packages to unravel our enjoyable downside of precisely predicting the “actual birthday” (or date of photo voltaic return) in a given future 12 months. We’ll then focus on how such packages might be leveraged in different real-life purposes.
Actual Birthday Predictor
Mission Setup
All implementation steps beneath have been examined on macOS Sequoia 15.6.1 and ought to be roughly comparable on Linux and Home windows.
Allow us to begin by organising the challenge listing. We might be utilizing uv
to handle the challenge (see set up directions right here). Confirm the put in model within the Terminal:
uv --version
Initialize a challenge listing known as real-birthday-predictor
at an appropriate location in your native machine:
uv init --bare real-birthday-predictor
Within the challenge listing, create a necessities.txt
file with the next dependencies:
skyfield==1.53
timezonefinder==8.0.0
geopy==2.4.1
pytz==2025.2
Here’s a temporary overview of every of those packages:
skyfield
offers capabilities for astronomical computation. It may be used to compute exact positions of celestial our bodies (e.g., Solar, Moon, planets, and satellites) to assist decide rise/set instances, eclipses, and orbital paths. It depends on so-called ephemerides (tables of positional knowledge for numerous celestial our bodies extrapolated over a few years), that are maintained by organizations such because the NASA Jet Propulsion Laboratory (JPL). For this text, we are going to use the light-weight DE421 ephemeris file, which covers dates from July 29, 1899, by way of October 9, 2053.timezonefinder
has capabilities for mapping geographical coordinates (latitudes and longitudes) to timezones (e.g., “Europe/Paris”). It could actually do that offline.geopy
provides capabilities for geospatial analytics, comparable to mapping between addresses and geographical coordinates. We’ll use it along with theNominatim
geocoder for OpenStreetMap knowledge to map the names of cities and international locations to coordinates.pytz
offers capabilities for temporal analytics and time zone conversion. We’ll use it to transform between UTC and native instances utilizing regional daylight-saving guidelines.
We may also use a number of different built-in modules, comparable to datetime
for parsing and manipulating date/time values, calendar
for checking leap years, and time
for sleeping between geocoding retries.
Subsequent, create a digital Python 3.12 surroundings contained in the challenge listing, activate the surroundings, and set up the dependencies:
uv venv --python=3.12
supply .venv/bin/activate
uv add -r necessities.txt
Verify that the dependencies have been put in:
uv pip record
Implementation
On this part, we are going to go piece by piece by way of the code for predicting the “actual” birthday date and time in a given future 12 months and site of celebration. First, we import the required modules:
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
Then we outline the tactic, utilizing significant variable names and docstring textual content:
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 "actual" birthday (photo voltaic return) for a given 12 months,
accounting for the time zone on the delivery location and the time zone
on the present location. Makes use of March 1 in non-leap years for the civil
anniversary if the official delivery date is February 29.
"""
Observe that current_country
and current_city
collectively consult with the situation at which the birthday is to be celebrated within the goal 12 months.
We validate the inputs earlier than working with them:
# Decide goal 12 months
if target_year is None:
target_year = datetime.now().12 months
else:
attempt:
target_year = int(target_year)
besides ValueError:
elevate ValueError(f"Invalid goal 12 months '{target_year}'. Please use 'yyyy' format.")
# Validate and parse delivery date
attempt:
birth_date = datetime.strptime(official_birthday, "%d-%m-%Y")
besides ValueError:
elevate ValueError(
f"Invalid delivery date '{official_birthday}'. "
"Please use 'dd-mm-yyyy' format with a sound calendar date."
)
# Validate and parse delivery time
attempt:
birth_hour, birth_minute = map(int, official_birth_time.break up(":"))
besides ValueError:
elevate ValueError(
f"Invalid delivery time '{official_birth_time}'. "
"Please use 'hh:mm' 24-hour format."
)
if not (0 <= birth_hour <= 23):
elevate ValueError(f"Hour '{birth_hour}' is out of vary (0-23).")
if not (0 <= birth_minute <= 59):
elevate ValueError(f"Minute '{birth_minute}' is out of vary (0-59).")
Subsequent, we use geopy
with the Nominatim
geocoder to determine the delivery and present areas. To keep away from getting timeout errors, we set a fairly lengthy timeout worth of ten seconds; that is how lengthy our safe_geocode
perform waits for the geocoding service to reply earlier than elevating a geopy.exc.GeocoderTimedOut
exception. To be further protected, the perform makes an attempt the lookup process 3 times with one-second delays earlier than giving up:
geolocator = Nominatim(user_agent="birthday_tz_lookup", timeout=10)
# Helper perform to name geocode API with retries
def safe_geocode(question, retries=3, delay=1):
for try in vary(retries):
attempt:
return geolocator.geocode(question)
besides GeocoderTimedOut:
if try < retries - 1:
time.sleep(delay)
else:
elevate RuntimeError(
f"Couldn't retrieve location for '{question}' after {retries} makes an attempt. "
"The geocoding service could also be sluggish or unavailable. Please attempt once more 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:
elevate ValueError("Couldn't discover coordinates for one of many areas. Please test spelling.")
Utilizing the geographical coordinates of the delivery and present areas, we establish the respective time zones and the UTC date and time at delivery. We additionally assume that people like Jacques, who have been born on February 29, will favor to have fun their birthday on March 1 in non-leap years:
# 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:
elevate ValueError("Couldn't decide timezone for one of many areas.")
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.12 months):
elevate ValueError(f"{birth_date.12 months} isn't a bissextile 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 delivery datetime in delivery location's native time
birth_local_dt = birth_tz.localize(datetime(
birth_date.12 months, birth_month, birth_day,
birth_hour, birth_minute
))
birth_dt_utc = birth_local_dt.astimezone(pytz.utc)
Utilizing the DE421 ephemeris knowledge, we calculate the place the Solar was (i.e., its ecliptic longitude) on the actual time and place the person was born:
# Load ephemeris knowledge and get Solar's ecliptic longitude at delivery
eph = load("de421.bsp") # Covers dates 1899-07-29 by way of 2053-10-09
ts = load.timescale()
solar = eph["sun"]
earth = eph["earth"]
t_birth = ts.utc(birth_dt_utc.12 months, birth_dt_utc.month, birth_dt_utc.day,
birth_dt_utc.hour, birth_dt_utc.minute, birth_dt_utc.second)
# Delivery longitude in tropical body from POV of delivery observer on Earth's floor
birth_observer = earth + wgs84.latlon(birth_location.latitude, birth_location.longitude)
ecl = birth_observer.at(t_birth).observe(solar).obvious().ecliptic_latlon(epoch='date')
birth_longitude = ecl[1].levels
Observe that, the primary time the road eph = load("de421.bsp")
is executed, the de421.bsp
file might be downloaded and positioned within the challenge listing; in all future executions, the downloaded file might be used straight. Additionally it is potential to change the code to load one other ephemeris file (e.g., de440s.bsp
, which covers years by way of January 22, 2150).
Now comes an attention-grabbing a part of the perform: we are going to make an preliminary guess of the “actual” birthday date and time within the goal 12 months, outline protected higher and decrease bounds for the true date and time worth (e.g., two days both facet of the preliminary guess), and carry out a binary search with early-stopping to effectively dwelling in on the true worth:
# Preliminary guess for goal 12 months photo voltaic 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 Solar longitude from POV of present observer on Earth's floor
current_observer = earth + wgs84.latlon(current_location.latitude, current_location.longitude)
def sun_longitude_at(dt):
t = ts.utc(dt.12 months, dt.month, dt.day, dt.hour, dt.minute, dt.second)
ecl = current_observer.at(t).observe(solar).obvious().ecliptic_latlon(epoch='date')
return ecl[1].levels
def angle_diff(a, b):
return (a - b + 180) % 360 - 180
# Set protected higher and decrease bounds for search house
dt1 = approx_dt_utc - timedelta(days=2)
dt2 = approx_dt_utc + timedelta(days=2)
# Use binary search with early-stopping to unravel for actual photo voltaic return in UTC
old_angle_diff = 999
for _ in vary(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 situation
break
if curr_angle_diff > 0:
dt2 = mid
else:
dt1 = mid
old_angle_diff = curr_angle_diff
real_dt_utc = dt1 + (dt2 - dt1) / 2
See this article for extra examples of utilizing binary search and to grasp why this algorithm is a crucial one for knowledge scientists to grasp.
Lastly, the date and time of the “actual” birthday recognized by the binary search is transformed to the present location’s time zone, formatted as wanted, and returned:
# Convert to present location's native 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
Testing
Now we’re able to foretell the “actual” birthdays of Gabriel, Jacques, and Camille in 2026.
To make the perform output simpler to digest, here’s a helper perform we are going to use to pretty-print the outcomes of every question:
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):
"""Fairly-print output whereas hiding verbose error traces."""
print("Official birthday and time:", official_birthday, "at", official_birth_time)
attempt:
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 12 months {target_year}, your actual birthday is on {date_str} at {time_str} ({current_tz_name})n")
besides ValueError as e:
print("Error:", e)
Listed below are the check circumstances:
# 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"
)
And listed below are the outcomes:
Official birthday and time: 18-01-1996 at 02:30
In 12 months 2026, your actual birthday is on 17/01 at 09:21 (Europe/Paris)
Official birthday and time: 29-02-1996 at 05:45
In 12 months 2026, your actual birthday is on 28/02 at 12:37 (Europe/Paris)
Official birthday and time: 05-05-1996 at 20:30
In 12 months 2026, your actual birthday is on 06/05 at 09:48 (Asia/Tokyo)
As we see, the “actual” birthday (or second of photo voltaic return) is completely different from the official birthday for all three of your mates: Gabriel and Jacques might theoretically begin celebrating a day earlier than their official birthdays in Paris, whereas Camille ought to attend yet one more day earlier than celebrating her thirtieth in Tokyo.
As an easier various to following the steps above, the creator of this text has created a Python library known as solarius
to realize the identical outcome (see particulars right here). Set up the library with pip set up solarius
or uv add solarius
and use it as proven beneath:
from solarius.mannequin import SolarReturnCalculator
calculator = SolarReturnCalculator(ephemeris_file="de421.bsp")
# Predict with out 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 comfort 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"
)
In fact, there may be extra to birthdays than predicting photo voltaic returns — these particular days are steeped in centuries of custom. Here’s a brief video on the fascinating origins of birthdays:
Past Birthdays
The intention of the above part was to provide readers a enjoyable and intuitive use case for making use of the assorted packages for astronomical computation and geospatial-temporal analytics. Nonetheless, the usefulness of such packages goes far past predicting birthdays.
For instance, all of those packages can be utilized for different circumstances of astronomical occasion prediction (e.g., figuring out when a dawn, sundown, or eclipse will occur on a future date in a given location). Predicting the motion of satellites and different celestial our bodies might additionally play an vital half in planning house missions.
The packages may be used to optimize the deployment of photo voltaic panels in a specific location, comparable to a residential neighborhood or a business website. The target could be to foretell how a lot daylight is prone to fall on that location at completely different instances of the 12 months and use this data to regulate the location, tilt, and utilization schedules of the photo voltaic panels for optimum vitality seize.
Lastly, the packages might be leveraged for historic occasion reconstruction (e.g., within the context of archaeological or historic analysis, and even authorized forensics). The target right here could be to recreate the sky circumstances for a particular previous date and site to assist researchers higher perceive the lighting and visibility circumstances at the moment.
In the end, by combining these open-source packages and built-in modules in numerous methods, it’s potential to unravel attention-grabbing issues that reduce throughout quite a few domains.