DEV Community

Cover image for How to calculate the best days for planetary observation
Rémy Hannequin
Rémy Hannequin

Posted on

How to calculate the best days for planetary observation

A few months ago, I started a tradition: each time I release a new version of Astronoby, I write a blog post showing how Ruby and the amateur astronomy communities can use it to compute astronomical events.

Today, to celebrate v0.8.0, we'll compute the best days for planetary observation. This illustrates Astronoby's new caching and performance improvements, and hints at features coming in future versions.

But first, a bit of context.

What is planetary observation?

In amateur astronomy, when we say planetary, we usually mean three types of bodies:

  • the planets
  • the Moon
  • the Sun

If you jumped because the Moon and the Sun are not planets, you're right, but please hear me out.

Planetary observation differs from deep sky observation. Deep sky objects are very different from planets, usually much fainter. But also they always appear at the same place in the sky at the same time of the year. They represent nebulae, galaxies, stars clusters from our own galaxy or surrounding. On a human timescale, they appear fixed in the sky. Only Earth's revolution around the Sun changes their visibility throughout the year.

Planets on the other hand are very close and very bright, and they have their own motion which is noticeable on a daily basis. The Moon changes phases, while some planets like Mars is best to be observed every two years.

Apparent reversal of Mars' movement in the night skyCredits: BBC Sky at Night Magazine

Planetary observation depends on many constantly changing factors.

Sun observation

Let's start with the easiest one on paper, but also the most dangerous. The "astronomer code of conduct" requires me to remind you, even if you might already know it, to never look at the Sun with the naked eye or any equipment, unless you are using a very secure and safe one.

You might think, why would we observe something that can burn the retina? Because, with a dedicated equipment, the Sun is full of great things to see like sun spots, which are cooler areas on the surface, or flares, which are ejections of coronal mass, and many more.

The Sun is easy because it doesn't really move, our perspective of movement is mainly due to the Earth's rotation and revolution.

The Earth's tilt makes the Sun appear more or less high in the sky across the year. If you want to know more about the why and how, check out my previous blog post on the sub-solar point.

Therefore, the best time to observe the Sun is around the summer solstice, June in the northern hemisphere, December in the southern, when it is highest in the sky and visible for the longest time.

The Sun AnalemmaCredits: Sky & Telescope

You don't need an astronomy library to know when are the few weeks around the solstice. But you might be interested to know what is the highest altitude the Sun could go as seen from your location. Let's use Astronoby to compute the day of solstice, and then compute the highest altitude of the Sun that day, when it transits. As someone living in France, I'm going to use the June solstice.

# Download an ephemeris if you don't have one yet.
# Check out https://github.com/rhannequin/astronoby/wiki/Ephem

# Load an ephemeris
ephem = Astronoby::Ephem.load("inpop19a.bsp")

# Set up the observer with your GPS coordinates
# For the example: Paris, France
observer = Astronoby::Observer.new(
  latitude: Astronoby::Angle.from_degrees(48.8575),
  longitude: Astronoby::Angle.from_degrees(2.3514)
)

# Compute solstice day
solstice_day = Astronoby::EquinoxSolstice
  .june_solstice(2025, ephem)
  .to_date
# => #<Date: 2025-06-21 ((2460848j,0s,0n),+0s,2299161j)>

calculator = Astronoby::RiseTransitSetCalculator.new(
  body: Astronoby::Sun,
  observer: observer,
  ephem: ephem
)

# Compute time of Sun's transit
transit_time = calculator
  .event_on(solstice_day)
  .transit_time

# Compute Sun' state at this instant
sun = Astronoby::Sun.new(
  instant : Astronoby::Instant.from_time(transit_time),
  ephem: ephem
)

# Get the altitude in degrees
altitude = sun
  .observed_by(observer)
  .horizontal
  .altitude
  .degrees
  .round(2)
# => 64.58
Enter fullscreen mode Exit fullscreen mode

Paris is decently North, so the Sun gets pretty high during the solstice but not as high as observed from the Tropic of Cancer.

Planets observation

Things get a bit more complicated from here. First, because the planets are constantly moving, and so does Earth, so they cannot be expected to be at the same location in the sky every year.

But also, some planets have orbits closer to the Sun than Earth, and some are further, which changes the orientation of looking at them.

Inferior planets

Let's start with the only two planets whose orbits lie closer to the Sun than Earth's: Mercury and Venus.

From our perspective, they are always quite close to the Sun. Sometimes, when they are between the Sun and the Earth, or when the Sun is between them and the Earth, they cannot be seen at all. That's why some cultures call Venus the Morning Star: it's often visible just before sunrise (or just after sunset).

Elongation configurations of an inferior planetCredits: University of Nebraska-Lincoln

In conclusion, the best moment to observe these planets is a range of a few days or weeks around when the planet is the further from the Sun, from our perspective. This moment is called the greatest elongation. It is the maximal angular distance between the planet and the Sun, as observed from Earth.

We are going to use Astronoby to compute the top 30 days when Venus is the further from the Sun.

Astronoby doesn't have a built-in function (yet) for this, so we're going to do it ourselves. However, it means we are going to compute Venus' position for every day of the year, this can be quite expensive.

That's where I can advertise on Astronoby's latest feature: internal caching. You just have to enable it and Astronoby will internally save some calculations that are redundant, making the total computation faster. Then, we are going to use some trigonometry to compute the daily elongation, and save the 30 largest.

# Assuming you have already downloaded
# and loaded the ephemeris, cf above.

# Enable caching and better performance
Astronoby.configuration.cache_enabled = true

# Iterate over every day of the year
results = (Date.new(2025, 1, 1)..Date.new(2025, 12, 31))
  .each_with_object({}) do |date, results|
    # Let's check elongation at midnight UTC
    time = Time.utc(date.year, date.month, date.day)
    instant = Astronoby::Instant.from_time(time)

    # Compute Sun and Venus positions
    sun = Astronoby::Sun.new(instant: instant, ephem: ephem)
    venus = Astronoby::Venus.new(instant: instant, ephem: ephem)

    # Extract their astrometric unit vectors
    # We normalize the position vectors of Venus and the Sun
    # to compute the angle between them.
    venus_unit_vector = venus
      .astrometric
      .position
      .map { |x| x.m / venus.astrometric.position.norm.m }
    sun_unit_vector = sun
      .astrometric
      .position
      .map { |x| x.m / sun.astrometric.position.norm.m }

    # Compute the elongation angle and store it
    results[date] = Astronoby::Angle.acos(
      Astronoby::Util::Maths.dot_product(
        venus_unit_vector,
        sun_unit_vector
      )
    )
  end

top_days = results
  .sort_by { |_date, angle| -angle }
  .first(30)
  .map { |date, _angle| date }
# => [
#   #<Date: 2025-01-10 ((2460686j,0s,0n),+0s,2299161j)>,
#   ...
Enter fullscreen mode Exit fullscreen mode

In 2025, the greatest elongation of Venus happened on January 10 and the best days to observe Venus were around this day.

Note that if you look at the whole array, you will notice the presence of some days from June, it's because Venus reached its greatest elongation twice in 2025 (East in January, West in June).

Superior planets

Now, superior planets are the ones which orbit is further from the Sun than the Earth's, which include Jupiter, Saturn, Uranus and Neptune.

For these planets, the best time to observe them is at opposition, when Earth is exactly between the Sun and the planet. At that point, the planet is visible all night and reaches transit near midnight, giving the best conditions for observation.

Opposition configuration of a superior planetCredits: University of Nebraska-Lincoln

The geometrical and physical definition of a planet's opposition is to have the difference between the Sun and the planet's geocentric apparent ecliptic longitude to equal 180°.

Good news, we also have this information available straight from Astronoby. Let's calculate the day of opposition by finding the day when this angle is the closest to 180°.

# Assuming you have already downloaded
# and loaded the ephemeris, cf above.

# Enable caching and better performance
Astronoby.configuration.cache_enabled = true

# Iterate over every day of the year
results = (Date.new(2025, 1, 1)..Date.new(2025, 12, 31))
  .each_with_object({}) do |date, results|
    # Let's check opposition at midnight UTC
    time = Time.utc(date.year, date.month, date.day)
    instant = Astronoby::Instant.from_time(time)

    # Compute Sun and Saturn positions
    sun = Astronoby::Sun.new(instant: instant, ephem: ephem)
    saturn = Astronoby::Saturn.new(instant: instant, ephem: ephem)

    # Compute the opposition angle
    # This calculates the difference in ecliptic longitude,
    # opposition is when it's 180°.
    angle = sun.apparent.ecliptic.longitude - saturn.apparent.ecliptic.longitude

    # Store the absolute angle reduced by 180° for ease of use
    angle = Astronoby::Angle.from_degrees((angle.degrees.abs - 180).abs)
    results[date] = angle
  end

top_days = results
  .sort_by { |_date, angle| angle }
  .first(30)
  .map { |date, _angle| date }
# => [
#   #<Date: 2025-09-21 ((2460940j,0s,0n),+0s,2299161j)>,
#   ...
Enter fullscreen mode Exit fullscreen mode

In 2025, the opposition of Saturn will happen on September 10 and the best days to observe Saturn were around this day.

And you don't have to trust my math, you can also use Astronoby to get the transit time on the opposition day to ensure Saturn will reach its maximum altitude in the middle of the night:

opposition_date = Date.new(2025, 9, 21)

# Set up the observer with your GPS coordinates
# For the example: Paris, France
observer = Astronoby::Observer.new(
  latitude: Astronoby::Angle.from_degrees(48.8575),
  longitude: Astronoby::Angle.from_degrees(2.3514)
)

calculator = Astronoby::RiseTransitSetCalculator.new(
  body: Astronoby::Saturn,
  observer: observer,
  ephem: ephem
)

# Compute time of Saturn's transit, Paris local time
transit_time = calculator
  .event_on(opposition_date)
  .transit_time
  .localtime("+01:00")
# => 2025-09-22 00:44:30 +0100
Enter fullscreen mode Exit fullscreen mode

Moon observation

Last stop of our journey: The Moon.

You might think this is easy: the Moon is visible almost every day, so observing it should be straightforward. But it's not that simple.

It depends on what you want to observe, when, and in what conditions.

First, the Moon rotates around the Earth in around 29 days, its phase changes every day. Whether you want to observe a full Moon or a thin crescent, the desired phase isn't available every day.

Also, you probably want to observe it when it's high in the sky, visible for a long time. Observing it close to the horizon might make the observation hard due to the atmospheric refraction bending the light.

You might have noticed it, but sometimes the Moon can be seen very high in the sky, and sometimes very close to the horizon. This is because the Moon's orbit is inclined by 5.14° to the ecliptic, in addition to the Earth's axial tilt to orbit of 23.44°. The combination of these tilts constantly changes how the Moon appears in the sky. There are also many small and long-term effects that have an influence but let's not get too complex.

The main thing to understand is that the perfect configuration for you might happen very rarely, that's why computing when it happens with Astronoby is very helpful.

For the example, I'm going to be very selective to show how precise you can get with Astronoby. Let's say I want to know the days when I can observe the Moon in the evening, not too close to the Full Moon because I want to observe craters with grazing light, when it will be high enough in the sky at transit.

To be more technical, this translates into the following requirements:

  • Altitude at transit at least 45°
  • Phase angle below 110° so that the Moon doesn't appear too close to the Sun
  • Illuminated fraction below 0.85 to avoid the Full Moon and the days around it
  • Transit happens after noon to allow for a nice observation in the evening

Here's how the Moon would look like when meeting all the requirements. Some craters are beautiful during this phase.

Picture of the Moon meeting all the requirements

Last note before we jump into the code. I'm going to use the tzinfo gem to have an accurate local noon and dates across the year.

require "tzinfo"

# Assuming you have already downloaded
# and loaded the ephemeris, cf above.

# Enable caching and better performance
Astronoby.configuration.cache_enabled = true

# Set up the observer with your GPS coordinates
# For the example: Paris, France
observer = Astronoby::Observer.new(
  latitude: Astronoby::Angle.from_degrees(48.8575),
  longitude: Astronoby::Angle.from_degrees(2.3514)
)

# Use Paris' time zone for accurate time and date conversion
tz = TZInfo::Timezone.get("Europe/Paris")

# Define requirements
minimum_altitude = Astronoby::Angle.from_degrees(45)
maximum_phase_angle = Astronoby::Angle.from_degrees(110)
maximum_illuminated_fraction = 0.85

calculator = Astronoby::RiseTransitSetCalculator.new(
  body: Astronoby::Moon,
  observer: observer,
  ephem: ephem
)

# Compute all the transit times of year 2025
# Thanks to the caching feature, this is quite fast
transit_times = calculator
  .events_between(
    Time.utc(2025, 1, 1),
    Time.utc(2026, 1, 1)
  )
  .transit_times

results = transit_times
  .each_with_object([]) do |transit_time, results|
    date = tz.to_local(transit_time).to_date
    transit_instant = Astronoby::Instant.from_time(transit_time)

    # Compute the Moon's position at transit time
    moon = Astronoby::Moon.new(
      instant: transit_instant,
      ephem: ephem
    )

    # Get the Moon's topocentric altitude
    # Altitude as seen by an observer on Earth
    altitude = moon
      .observed_by(observer)
      .horizontal
      .altitude

    # Define when noon happens in Paris that day
    local_noon = tz
      .local_time(date.year, date.month, date.day, 12, 0, 0)

    # Decide whether or not the requirements are met
    requirements_pass = altitude > minimum_altitude &&
      moon.phase_angle < maximum_phase_angle &&
      moon.illuminated_fraction < maximum_illuminated_fraction &&
      transit_time > local_noon

    results << date if requirements_pass
  end

results.size
# => 28
Enter fullscreen mode Exit fullscreen mode

There are only 28 days in 2025 where I can observe the Moon from Paris in the particular configuration I require.

Of course I was very strict for the sake of the example and the Moon is nice to observe any time it is visible.

In this post, we saw how Astronoby can compute the best days for observing the Sun, planets, and Moon, from solstices and elongations to oppositions and lunar phases. These tools help astronomers plan sessions months in advance. In future releases, Astronoby will support even more events. Until then, I'd love to hear what you'd like to compute with Astronoby!


Cover picture credits: NASA

Top comments (0)