January 5, 2026
I'm visiting my parents this winter in the French Alps. I missed the mountains I grew up with, but I had forgotten how much sun they hide at this period of the year. I thought I'd use the cold and shady time to compute when it's going to be sunny at my parent's house, so that I can plan ahead for the optimal reading time (isn't it nicer to read in the sun?)
This computation involves the following steps:
It is sunny when the sun is above the horizon. The horizon is defined by the silhouette formed by mountains around me.
I went ahead and downloaded the topographic map of the area from Copernicus GLO-30 Digital Elevation Model1. I have selected a region large enough to cover most visible mountains from here.
Elevation map around my hometown (red dot). We can already guess that the mountains in the South hide the sun during winter.
Then, I computed the horizon from this map. This is computed as the maximum of along a ray starting at my location. In the above equation, is the elevation at the view point, is that along the ray, and is the distance from the view point. I have repeated this for any azimuth angle (this angle is 0 when pointing north, and 90 degrees when pointing east):
Horizon from my parents' house, computed from the topographic data.
I didn't look for horizon data to double check these results but I easily recognize many mountains, I've seen them so many times. There's the Mole at around 55 degrees, pointe d'Andey at around 110 degrees and the pointe de Cou in the South direction. This latter peak is the main culprit for hiding the sun during winter.
The trajectory of the sun depends on the time of the year. I have used the following code to estimate its trajectory. This is a blind copy-paste from chatGPT.
def day_of_year(d: datetime.date) -> int:
return d.timetuple().tm_yday
def refraction_correction_deg(elevation_deg):
"""
Atmospheric refraction correction in degrees.
Valid for elevations > ~ -1 deg.
"""
h = elevation_deg
if h < -1:
return 0.0
return (1.02 / np.tan(np.deg2rad(h + 10.3 / (h + 5.11)))) / 60.0
def solar_position_noaa(lat_deg, lon_deg, when_local, tz_offset_hours):
"""
Approx solar azimuth/elevation (degrees) using NOAA-style formulas.
lat_deg, lon_deg: observer location (deg), lon positive east.
when_local: naive datetime in LOCAL time (no tzinfo)
tz_offset_hours: e.g. +1 for CET, +2 for CEST
Returns: (azimuth_deg, elevation_deg)
azimuth: degrees clockwise from North (0=N, 90=E)
elevation: degrees above horizon
"""
# --- constants / conversions
deg2rad = np.deg2rad
rad2deg = np.rad2deg
lat = deg2rad(lat_deg)
# Day of year
n = day_of_year(when_local.date())
# Fractional year (gamma) in radians
# hour in local time
hour = when_local.hour + when_local.minute/60 + when_local.second/3600
gamma = 2.0*np.pi/365.0 * (n - 1 + (hour - 12)/24.0)
# Equation of time (minutes)
eqtime = 229.18 * (
0.000075
+ 0.001868*np.cos(gamma)
- 0.032077*np.sin(gamma)
- 0.014615*np.cos(2*gamma)
- 0.040849*np.sin(2*gamma)
)
# Solar declination (radians)
decl = (
0.006918
- 0.399912*np.cos(gamma)
+ 0.070257*np.sin(gamma)
- 0.006758*np.cos(2*gamma)
+ 0.000907*np.sin(2*gamma)
- 0.002697*np.cos(3*gamma)
+ 0.00148*np.sin(3*gamma)
)
# Time offset (minutes)
# lon_deg positive east; tz_offset_hours hours east of UTC
time_offset = eqtime + 4.0*lon_deg - 60.0*tz_offset_hours
# True solar time (minutes)
tst = (hour*60.0 + time_offset) % 1440.0
# Hour angle (degrees)
ha_deg = tst/4.0 - 180.0
ha = deg2rad(ha_deg)
# Solar zenith angle
cos_zenith = np.sin(lat)*np.sin(decl) + np.cos(lat)*np.cos(decl)*np.cos(ha)
cos_zenith = np.clip(cos_zenith, -1.0, 1.0)
zenith = np.arccos(cos_zenith)
elevation = 90.0 - rad2deg(zenith)
# Solar azimuth (NOAA convention)
# Compute azimuth from north, clockwise
sin_az = -np.sin(ha) * np.cos(decl) / np.sin(zenith)
cos_az = (np.sin(decl) - np.sin(lat)*np.cos(zenith)) / (np.cos(lat)*np.sin(zenith))
az = np.arctan2(sin_az, cos_az) # radians, range [-pi, pi]
az_deg = (rad2deg(az) + 360.0) % 360.0
return az_deg, elevation
def solar_track(lat, lon, date, tz_offset_hours, start_h=6, end_h=22, step_min=1):
times = []
azs = []
els = []
t = datetime.datetime(date.year, date.month, date.day, start_h, 0, 0)
t_end = datetime.datetime(date.year, date.month, date.day, end_h, 0, 0)
step = datetime.timedelta(minutes=step_min)
while t <= t_end:
az, el = solar_position_noaa(lat, lon, t, tz_offset_hours)
times.append(t)
azs.append(az)
els.append(el)
t += step
els = np.array([
el + refraction_correction_deg(el)
for el in els
])
return np.array(times), np.array(azs), np.array(els)
In summer, the sun is high up in the sky, and the mountains don't block the light. In winter, like today, the sun is very close and often below the horizon:
Horizon and trajectory of the sun at two different times of the year. The sun is often behind the mountains in winter.
The following figure shows when the sun will be above the horizon today, based on the model above:
Whether the sun will be above or below the horizon on January 5th of 2026.
There are many approximations here: I ignored trees, buildings, clouds, and could use a better model for the sun trajectory. Yet the results seem consistent with what I saw yesterday. Today, I'll be able to read in the sun from 9:27 to 13:52, and then from 14:51 to 15:45. Now I just hope that no cloud will disturb my plans!
European Space Agency (2024). Copernicus Global Digital Elevation Model. Distributed by OpenTopography. https://doi.org/10.5069/G9028PQB. Accessed 2026-01-04. ↩