Skip to content

Introduce a better rise/transit/set calculator#168

Merged
rhannequin merged 6 commits intomainfrom
rise-transit-set-again
Apr 30, 2025
Merged

Introduce a better rise/transit/set calculator#168
rhannequin merged 6 commits intomainfrom
rise-transit-set-again

Conversation

@rhannequin
Copy link
Owner

@rhannequin rhannequin commented Apr 24, 2025

Yes, I know, it must be the third calculator I introduce between two versions, I just never feel completely satisfied.

The previous calculator was accurate and provided many different attribute, but it was way too slow (see comparison below).

There are two common use cases I identified:

  • user is interested in the rising and setting times for a given date
  • user is interested in the rising and setting times of several consecutive days, possibly many

Another thing, because of time zones and the motion of celestial bodies in space, there are occurrences with several events the same day, or no event at all.

I realised it might be foolish to try to apply a strict definition (and a strict API) to phenomenons that don't happen the same way. At the end of the day, Astronoby is here to provide astronomical data, it is not an integrated tool with a specific use-case in mind.

This PR introduces Astronoby::RiseTransitSetCalculator (yes the name changed slightly, mostly because it's shorter), a calculator of rising, transit and setting times (and only times) with a more performant and readable algorithm. It is inspired by Skyfield's almanac algorithm, but behaves slightly different.

Initialization

It takes 3 key arguments:

  • observer (Astronoby::Observer)
  • body (Astronoby::SolarSystemBody, i.e. Astronoby::Sun)
  • ephem (Astronoby::Ephem)

Calculation

The calculator enables to compute times for a given date, of that would happen in a time range.

Please note that the rising and setting's azimuth angles, and the transit's elevation angle are not computed anymore. The new algorithm focuses on performance and computing events (times). If one is interested in having these angles, they can be computed individually from the topocentric position.

Depending on the method used, it will return either a RiseTransitSetEvent or a RiseTransitSetEvents object.

Astronoby::RiseTransitSetEvent exposes 3 attributes:

  • #rising_time (Time)
  • #transit_time (Time)
  • #setting_time (Time)

Astronoby::RiseTransitSetEvents exposes 3 attributes:

  • #rising_times (Array)
  • #transit_times (Array)
  • #setting_times (Array)

As you can see, RiseTransitSetEvents exposes arrays. While in 99+% of cases a celestial body will rise, transit and set only once in a day, it is possible to have no or several values, especially for rising and setting times.

One common example relates to the Moon. Because of its rotation around the Earth, its rising, transit and setting times are very different each day. When one of the happens very close to the beginning or the end of a day, the next one might "skip" a day.

Another example relates to the Sun and time zones. When dealing with UTC times, it might look like the Sun rises or sets twice in the same date, which is not true from the observer's point of view, but a "UTC day" is not the same defined period of time as the day from an observer at a longitude far from the Greenwich meridian.

Single date, support for only one event: #event_on

ephem = Astronoby::Ephem.load("inpop19a.bsp")
observer = Astronoby::Observer.new(
  latitude: Astronoby::Angle.from_degrees(48),
  longitude: Astronoby::Angle.from_degrees(2)
)
date = Date.new(2025, 4, 24)
utc_offset = "+02:00"

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

event = calculator.event_on(date, utc_offset: utc_offset)
# Returns a Astronoby::RiseTransitSetEvent object

event.rising_time
# => 2025-04-24 04:45:42 UTC

event.transit_time
# => 2025-04-24 11:50:04 UTC

event.setting_time
# => 2025-04-24 18:55:24 UTC

Single date, support for multiple events: #events_on

ephem = Astronoby::Ephem.load("inpop19a.bsp")
observer = Astronoby::Observer.new(
  latitude: Astronoby::Angle.from_degrees(48),
  longitude: Astronoby::Angle.from_degrees(2)
)
date = Date.new(2025, 4, 24)
utc_offset = "+02:00"

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

events = calculator.events_on(date, utc_offset: utc_offset)
# Returns a Astronoby::RiseTransitSetEvents object

events.rising_times.first
# => 2025-04-24 04:45:42 UTC

events.transit_times.first
# => 2025-04-24 11:50:04 UTC

events.setting_times.first
# => 2025-04-24 18:55:24 UTC

Time range: #events_between

start_time = Time.utc(2025, 3, 1)
end_time = Time.utc(2025, 4, 1)

events = calculator.events_between(start_time, end_time)
# Returns a Astronoby::RiseTransitSetEvents object

events.rising_times.size
# => 31

events.rising_times.first
# => 2025-03-01 06:32:44 UTC

events.rising_times.last
# => 2025-03-31 05:32:10 UTC

Performance improvements

This was the main reason for implementing a new algorithm. Here is an example of code meant to compute the events for the whole year of 2025 using the previous algorithm which was only able to compute one day at a time:

test_starts_at = Time.now
ephem = Astronoby::Ephem.load("inpop19a_2025_excerpt.bsp")
observer = Astronoby::Observer.new(
  latitude: Astronoby::Angle.from_degrees(48),
  longitude: Astronoby::Angle.from_degrees(2)
)
initial_date = Date.new(2025, 1, 1)
calculator = Astronoby::RisingTransitSettingEventsCalculator.new(
  observer: observer,
  target_body: Astronoby::Sun,
  ephem: ephem
)

(0...365).each do |day|
  calculator.events_on(initial_date + day)
end
test_ends_at = Time.now

puts "Task completed in #{test_ends_at - test_starts_at} seconds"
# => Task completed in 17.438254 seconds

Now, the same experiment but using the new algorithm:

test_starts_at = Time.now
ephem = Astronoby::Ephem.load("inpop19a_2025_excerpt.bsp")
observer = Astronoby::Observer.new(
  latitude: Astronoby::Angle.from_degrees(48),
  longitude: Astronoby::Angle.from_degrees(2)
)
start_time = Time.utc(2025, 1, 1)
end_time = Time.utc(2026, 1, 1)
calculator = Astronoby::RiseTransitSetCalculator.new(
  body: Astronoby::Sun,
  observer: observer,
  ephem: ephem
)

calculator.events_between(start_time, end_time)
test_ends_at = Time.now

puts "Task completed in #{test_ends_at - test_starts_at} seconds"
# => Task completed in 3.692661 seconds

The new algorithm is more than 4.7 times faster over a large range of dates.

Twilight events adaptation

This was the opportunity to adapt the twilght events into a similar behaviour. Now, it also uses a calculator:

ephem = Astronoby::Ephem.load("inpop19a.bsp")
observer = Astronoby::Observer.new(
  latitude: Astronoby::Angle.from_degrees(48),
  longitude: Astronoby::Angle.from_degrees(2)
)
date = Date.new(2025, 4, 24)

calculator = Astronoby::TwilightCalculator.new(
  observer: observer,
  ephem: ephem
)

event = calculator.event_on(date)
# Returns a Astronoby::TwilightEvent object

event.morning_astronomical_twilight_time
# => 2025-04-24 02:43:38 UTC

event.morning_nautical_twilight_time
# => 2025-04-24 03:30:45 UTC

event.morning_civil_twilight_time
# => 2025-04-24 04:12:38 UTC

event.evening_civil_twilight_time
# => 2025-04-24 19:27:34 UTC

event.evening_nautical_twilight_time
# => 2025-04-24 20:09:27 UTC

event.evening_astronomical_twilight_time
# => 2025-04-24 20:56:34 UTC

It also still supports the time_for_zenith_angle method:

ephem = Astronoby::Ephem.load("inpop19a.bsp")
observer = Astronoby::Observer.new(
  latitude: Astronoby::Angle.from_degrees(48),
  longitude: Astronoby::Angle.from_degrees(2)
)
date = Date.new(2025, 4, 24)
zenith_angle = Astronoby::Angle.from_degrees(90 + 17)

calculator = Astronoby::TwilightCalculator.new(
  observer: observer,
  ephem: ephem
)

calculator.time_for_zenith_angle(
  date: date,
  period_of_the_day: :morning,
  zenith_angle: zenith_angle
)
# => 2025-04-24 02:52:01 UTC

@rhannequin rhannequin requested a review from Copilot April 24, 2025 15:44
@rhannequin rhannequin self-assigned this Apr 24, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

The PR replaces the previous rising/transit/setting events calculator with a new, faster algorithm while updating tests and related integrations.

  • Renames and refactors the calculator to use array-based event times (rising_times, transit_times, setting_times).
  • Removes the old RisingTransitSettingEventsCalculator and adds a new RiseTransitSetCalculator.
  • Updates supporting files and tests (including twilight events and maths helpers) to integrate the new event calculations.

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
spec/astronoby/events/rise_transit_set_calculator_spec.rb Renamed spec and updated expectations to use array-based event times.
lib/astronoby/util/maths.rb Added a new linspace helper for array generation in interpolation.
lib/astronoby/events/twilight_events.rb Updated event access and passed utc_offset to align with new API.
lib/astronoby/events/rise_transit_set_calculator.rb Introduced the new calculator with a more performant interpolation logic.
lib/astronoby/events/rise_transit_set_events.rb Added a new events container class reflecting the new API.
lib/astronoby/constants.rb Added MICROSECOND_IN_DAYS constant used in time adjustments.
lib/astronoby.rb Updated requires to load the new calculator and events class.
Comments suppressed due to low confidence (1)

spec/astronoby/events/rise_transit_set_calculator_spec.rb:29

  • [nitpick] It would improve clarity to document that the first element in the array is assumed to be the primary event, given that a day may yield multiple events.
expect(events.rising_times.first)

@rhannequin rhannequin force-pushed the rise-transit-set-again branch from e2fd7d3 to 3f10c79 Compare April 29, 2025 21:17
@rhannequin rhannequin requested a review from Copilot April 29, 2025 21:17
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new, more performant and streamlined rise/transit/set calculator along with a corresponding twilight calculator while deprecating the old rising/transit/setting events calculator. Key changes include replacing the old calculator with the new RiseTransitSetCalculator, updating related test specs, and adding a utility method (linspace) for numerical interpolation across the codebase.

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.

Show a summary per file
File Description
spec/astronoby/util/maths_spec.rb Added tests for the new linspace method.
spec/astronoby/events/twilight_calculator_spec.rb Updated specs to use the new TwilightCalculator API.
spec/astronoby/events/rise_transit_set_calculator_spec.rb Updated tests to match renaming of calculator and event attribute changes.
lib/astronoby/util/maths.rb Introduced the linspace method.
lib/astronoby/events/* Refactored calculator-related implementations and removed deprecated modules.
Comments suppressed due to low confidence (1)

spec/astronoby/events/twilight_calculator_spec.rb:355

  • The variable name 'calulator' appears to be a typo. It should be renamed to 'calculator' to maintain clarity and consistency.
calulator = described_class.new(

@rhannequin rhannequin merged commit a2b8eb8 into main Apr 30, 2025
38 checks passed
@rhannequin rhannequin deleted the rise-transit-set-again branch April 30, 2025 09:53
@rhannequin rhannequin mentioned this pull request Apr 30, 2025
rhannequin added a commit that referenced this pull request May 12, 2025
## 0.7.0 - 2025-05-12

_If you are upgrading: please see [UPGRADING.md]._

### Bug fixes

* Fix Moon monthly phase events calculation by @valeriy-sokoloff in ([#124])

### Features

* Add `Instant` value object ([#121])
* Introduce barycentric position of Solar System major bodies ([#127])
* Introduce Astrometric position for planets ([#129])
* Rename Barycentric into Geometric ([#130])
* Rename IRCF and remove module Position ([#131])
* Geometric and Astrometric reference frames with coordinates ([#132])
* Ecliptic coordinates for Geometric and Astrometric reference frames ([#134])
* Add Geometric and Astrometric positions for `Sun` and `Moon` ([#135])
* Implement new aberration correction ([#136])
* Precession matrix for 2006 P03 model ([#137])
* Introduce `MeanOfDate` reference frame ([#138])
* New nutation model ([#141])
* Light deflection correction ([#142])
* Introduce `Apparent` reference frame ([#143])
* Introduce `Topocentric` reference frame ([#145])
* Improve Vector integration with value objects ([#146])
* Handle refracted topocentric horizontal coordinates ([#147])
* Add `#angular_diameter` to apparent and topocentric reference frames ([#149])
* Introduce new calculator for rising, transit and setting times ([#148])
* Clean code after Ephem refactoring ([#152])
* Improve `RisingTransitSettingEventsCalculator` ([#155])
* Simplify `RisingTransitSettingEventsCalculator` ([#156])
* Lazy-load reference frames ([#157])
* Overall performance improvements ([#163])
* Add support for IMCCE INPOP by @JoelQ and @rhannequin ([#166])
* Update INPOP excerpt in spec data ([#167])
* Introduce a better rise/transit/set calculator ([#168])
* Drop `Astronoby::Observer#observe` ([#174])

### Improvements

* Bump standard from 1.42.1 to 1.49.0 by @dependabot ([#123], [#128], [#150], [#165])
* Bump rubyzip from 2.3.2 to 2.4.1 by @dependabot ([#120])
* Add more tests for Julian Date conversion ([#122])
* Upgrade main Ruby version and supported ones ([#125])
* Update email address and gem description ([#126])
* Increase precision of mean obliquity ([#133])
* Add supported Rubies ([#139])
* Set Ruby 3.4.2 as default version ([#140])
* Fix dependency secutiry patch ([#151])
* Improve HMS/DMS formats ([#153])
* Use excerpts ephemerides for specs of Sun and Moon ([#154])
* Add link to deprecated documentation ([#160])
* Default Ruby 3.4.3 and support recent rubies ([#169])
* Better Moon phases test coverage ([#172])
* Optimize Observer with GMST from Instant ([#173])
* Update README about documentation location ([#175])
* Add GitHub Actions permissions ([#176])

### New Contributors

* @valeriy-sokoloff made their first contribution in #124
* @JoelQ made their first contribution in #166

**Full Changelog**: v0.6.0...v0.7.0

[#120]: #120
[#121]: #121
[#122]: #122
[#123]: #123
[#124]: #124
[#125]: #125
[#126]: #126
[#127]: #127
[#128]: #128
[#129]: #129
[#130]: #130
[#131]: #131
[#132]: #132
[#133]: #133
[#134]: #134
[#135]: #135
[#136]: #136
[#137]: #137
[#138]: #138
[#139]: #139
[#140]: #140
[#141]: #141
[#142]: #142
[#143]: #143
[#145]: #145
[#146]: #146
[#147]: #147
[#148]: #148
[#149]: #149
[#150]: #150
[#151]: #151
[#152]: #152
[#153]: #153
[#154]: #154
[#155]: #155
[#156]: #156
[#157]: #157
[#160]: #160
[#163]: #163
[#165]: #165
[#166]: #166
[#167]: #167
[#168]: #168
[#169]: #169
[#172]: #172
[#173]: #173
[#174]: #174
[#175]: #175
[#176]: #176
[UPGRADING.md]: https://github.com/rhannequin/astronoby/blob/main/UPGRADING.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants