jonathanBieler.github.io

Catching the moon at the right time with Julia

targets

The moon on a night sky is surprisingly bright, especially for a camera with limited dynamic range. And there’s only so many compositions one can do of the moon on a naked sky. For those reasons taking moon pictures at sunrise or sunset, when the moon is close to the horizon and its relative brightness is comparable to ambient light often brings more interesting (or at least varied) results.

Maybe I haven’t searched well enough but I haven’t found a tool giving you the days of the year at which moon rise/set coincide with the sun rise/set for a given location. In any case that’s an excuse for a fun Julia project.

Disclaimer: I’m not a physicist or professional astronomer, if you’re planning something critical (like a rocket launch), maybe double-check with actual experts!

The Plan

The idea is simple: find dates when the sun and moon rise or set within a short time window of each other. This gives us those perfect conditions where you get a nicely lit sky and a visible moon. The main functionalities, namely the moon and sun position at a given date, are available via the AstroLib package.

Setup and converting times

First, let’s define where we’re observing from. I’m using Lyon, France as the example location:

using AstroLib, TimeZones
using Dates, Plots

# Location setup: latitude, longitude in degrees
latitude = 45.7640     # Lyon
longitude = 4.8357
obs_altitude = 212
local_tz = TimeZone("Europe/Paris")

start_date = Date(2025, 8, 4)

We also need our timezone to convert between universal time and our local observations.

AstroLib routines take time in Julian days, which surprisingly aren’t Julia specific units, but units used by astronomers. We can convert from a Julia date to Julian days with jdcnv :

date_to_jd(date) = jdcnv(DateTime(date))

Computing Altitudes

The get_altitude function is doing the real work here. It takes a Julian date and a position function (sunpos or moonpos), gets the celestial coordinates, and converts them to altitude above our horizon. When altitude crosses zero, we have a rise or set event.

function get_altitude(jd, posfun=sunpos)
    sun_ra_dec = posfun(jd)
    sun_alt, sun_az, _ = eq2hor(sun_ra_dec[1], sun_ra_dec[2], jd, latitude, longitude, obs_altitude)
    sun_alt
end

The sunpos function returns the right ascension and declination of the Sun at a given date, right ascension and declination being angles defining a direction on the celestial sphere.

The eq2hor function converts these two angles into an altitude (angle to the horizon) for our local position.

We can make a plot of the altitude for a day. As cou can see on that day the mon rises as the sun is setting, that’s the type of events we are loooking for!

targets

start_date = Date(2025, 8, 8)

jd_start = date_to_jd(start_date)
jd_end   = date_to_jd(start_date + Day(1))

t = LinRange(jd_start, jd_end, 100)

p = plot(t, get_altitude.(t, sunpos), label = "Sun altitude", xlabel = "Julian time")
plot!(t, get_altitude.(t, moonpos), label = "Moon altitude", ylabel = "Altitude")
hline!([0], label = "Horizon",c="gray", dpi=150)

Finding the Crossings

To find the horizon crossings I’m using a basic zero-crossing algorithm, with a 1 minute time resolution. I’ve tried root finding with Roots.jl but it surprisingly was getting stuck sometimes. I don’t need more precision anyway.

When altitude goes from negative to positive, that’s a rise. Positive to negative? That’s a set. Simple but effective.

function find_crossing(start_date, num_days, posfun)
    jd_start = date_to_jd(start_date)
    jd_end = date_to_jd(start_date + Day(num_days))

    times = LinRange(jd_start, jd_end, 24*60*num_days) # 1 minute resolution
    altitudes = get_altitude.(times, posfun)

    crossings =  @NamedTuple{time::DateTime, event::Symbol}[]

    for i in 2:length(altitudes)
        if altitudes[i-1] < 0 && altitudes[i]  0
            push!(crossings, (
                time = daycnv(times[i]),
                event = :rise,
            ))
        elseif altitudes[i-1]  0 && altitudes[i] < 0
            push!(crossings, (
                time = daycnv(times[i]),
                event = :set,
            ))
        end
    end
    crossings
end

The same plot as before with the crossings added :

targets

Moon & Sun Coincidences

No need for sophistication either here, we’ll just test every possible pairs of crossing for a year; if they occur within 30 minutes of each other, we keep them.

# Compute crossings for a full year
d = 360
sun_crossings  = find_crossing(start_date, d, sunpos)
moon_crossings = find_crossing(start_date, d, moonpos)

# Find coincidences within a 30-minute window
coincidences = Tuple{T,T}[]

for s in sun_crossings
    for m in moon_crossings
        if abs(s.time - m.time) < Minute(30)
            push!(coincidences, (s,m))
        end
    end
end

Pretty Printing the Results

Finally, let’s format the results nicely, including the moon phase (illuminated fraction) and local times:

Date Sun Event Sun Time Moon Event Moon Time Moon Phase (%)
8 Aug, 2025 sunset 20h59 moonrise 20h53 99.6
9 Aug, 2025 sunrise 06h38 moonset 06h25 99.9
9 Aug, 2025 sunset 20h58 moonrise 21h18 99.7
21 Aug, 2025 sunset 20h38 moonset 20h12 2.6
22 Aug, 2025 sunset 20h36 moonset 20h35 0.3
23 Aug, 2025 sunrise 06h55 moonrise 06h46 0.0
23 Aug, 2025 sunset 20h35 moonset 20h54 0.3
6 Sep, 2025 sunset 20h09 moonrise 19h42 98.6
7 Sep, 2025 sunset 20h07 moonrise 20h02 100.0
8 Sep, 2025 sunset 20h05 moonrise 20h20 98.6
20 Sep, 2025 sunset 19h41 moonset 19h17 1.2
21 Sep, 2025 sunset 19h39 moonset 19h33 0.0
22 Sep, 2025 sunrise 07h32 moonrise 08h00 0.2
22 Sep, 2025 sunset 19h38 moonset 19h48 0.8
23 Sep, 2025 sunset 19h36 moonset 20h04 3.6
6 Oct, 2025 sunset 19h11 moonrise 18h42 99.7
7 Oct, 2025 sunrise 07h51 moonset 08h10 99.9
7 Oct, 2025 sunset 19h09 moonrise 19h02 99.5
8 Oct, 2025 sunset 19h07 moonrise 19h27 96.3
21 Oct, 2025 sunrise 08h09 moonrise 08h02 0.1
21 Oct, 2025 sunset 18h44 moonset 18h28 0.1
22 Oct, 2025 sunset 18h42 moonset 18h49 1.4
5 Nov, 2025 sunrise 07h30 moonset 07h28 99.8
5 Nov, 2025 sunset 17h21 moonrise 16h52 99.8
6 Nov, 2025 sunset 17h20 moonrise 17h28 97.9
20 Nov, 2025 sunrise 07h51 moonrise 08h12 0.2
20 Nov, 2025 sunset 17h04 moonset 16h47 0.3
21 Nov, 2025 sunset 17h03 moonset 17h25 1.9
4 Dec, 2025 sunrise 08h09 moonset 07h48 99.1
5 Dec, 2025 sunset 16h56 moonrise 16h57 99.1
19 Dec, 2025 sunrise 08h21 moonrise 08h09 0.7
20 Dec, 2025 sunset 16h58 moonset 17h02 0.5
3 Jan, 2026 sunset 17h09 moonrise 16h57 99.8
18 Jan, 2026 sunrise 08h19 moonrise 08h22 0.4
18 Jan, 2026 sunset 17h27 moonset 17h02 0.1
1 Feb, 2026 sunrise 08h05 moonset 08h07 99.4
16 Feb, 2026 sunrise 07h44 moonrise 07h20 1.7
17 Feb, 2026 sunrise 07h42 moonrise 07h43 0.1
17 Feb, 2026 sunset 18h10 moonset 18h23 0.1
18 Feb, 2026 sunrise 07h41 moonrise 08h02 0.7
2 Mar, 2026 sunrise 07h20 moonset 06h59 98.2
3 Mar, 2026 sunrise 07h18 moonset 07h18 99.9
3 Mar, 2026 sunset 18h30 moonrise 18h42 99.9
4 Mar, 2026 sunrise 07h16 moonset 07h36 99.3
18 Mar, 2026 sunrise 06h50 moonrise 06h24 0.9
18 Mar, 2026 sunset 18h51 moonset 18h31 0.2
19 Mar, 2026 sunrise 06h48 moonrise 06h42 0.1
20 Mar, 2026 sunrise 06h46 moonrise 07h01 1.9
1 Apr, 2026 sunrise 07h23 moonset 06h58 99.2
1 Apr, 2026 sunset 20h09 moonrise 19h46 99.8
2 Apr, 2026 sunrise 07h21 moonset 07h15 99.9
3 Apr, 2026 sunrise 07h19 moonset 07h33 98.6
18 Apr, 2026 sunrise 06h52 moonrise 06h49 0.9
1 May, 2026 sunset 20h48 moonrise 21h00 99.8
2 May, 2026 sunrise 06h29 moonset 06h22 99.6
3 May, 2026 sunrise 06h28 moonset 06h50 97.8
16 May, 2026 sunset 21h06 moonset 21h25 0.2
17 May, 2026 sunrise 06h10 moonrise 05h53 0.4
30 May, 2026 sunset 21h21 moonrise 21h04 99.5
1 Jun, 2026 sunrise 05h57 moonset 06h09 99.2
14 Jun, 2026 sunset 21h31 moonset 21h37 0.3
15 Jun, 2026 sunrise 05h53 moonrise 05h26 0.2
29 Jun, 2026 sunset 21h34 moonrise 21h43 99.8
30 Jun, 2026 sunrise 05h57 moonset 05h53 99.9
13 Jul, 2026 sunset 21h28 moonset 21h21 0.7
28 Jul, 2026 sunset 21h13 moonrise 20h55 99.3
29 Jul, 2026 sunrise 06h23 moonset 05h54 99.8
29 Jul, 2026 sunset 21h12 moonrise 21h22 99.9

We can double check that a full moon will indeed around 17h the 3 Jan, 2026 in Lyon :

https://www.timeanddate.com/moon/france/lyon?month=1&year=2026

Code

You can generate similar tables for your location and timezone. It would be cool to provide an interactive page doing that.

https://github.com/jonathanBieler/sun_and_moon

#Julialang #Astronomy #Photography