Closed rhannequin closed 7 months ago
* `Sun`'s rising/setting time/azimuth methods have been replaced by: * `#rise_transit_set_times` which returns an array of times * `#rise_set_azimuths` which returns an array of azimuth angles
What's the motivation for this change? For me it seems not to be an improvement:
#rise_transit_set_times
instead of a simple #rise_time
, #transit_time
or #set_time
btw. astronoby is a great library with very readable code so far. :-)
@janfri Thanks a lot for the feedback. It means a lot to me to know some people are actually following the project and I'm glad to receive feedback to increase its quality 🙏
The main reason of this API change is to be more performant.
The implemented algorithm calculates almost everything at the same time: rise/transit/set times, rise/set azimuths and transit elevation. This new algorithm is fairly more expensive than the previous one, which already suffered from having to calculate almost the same data multiple times when calling rising_time
, setting_time
, rising_azimuth
, etc, independently.
By implementing all these methods independently, the program will compute all the data at each call, regardless of if only one value will be accessed by the developer in the end.
This is mainly because the Sun
is initialized with an epoch, but observation methods take an observer
parameter. The epoch
is memoized, but not the observer
.
If would need to find a way to memoize each RiseTransitSet
object for each different value of observer
to provide separated methods on Sun
while keeping the computation performant.
Another way to solve this would be to add observer
to the Sun
's constructor, but it doesn't feel right to me. The Sun, as a celestial object, has attributes based on an instant in time (equation of time, ecliptic coordinates, true anomaly, etc), and is not affected by who it is observed by.
Do you have an opinion on how to resolve this?
@janfri Your feedback (and what I first answered) got me thinking and I think you're right, the developer should not be impacted by internal constraints. If the calculation is expensive and calculates all the data immediately, the developer should not have to adapt to this situation but only benefit from a clear and pleasant API.
I implemented a memoization mechanism based on the observer
, by making it behave like a value object: 79793860acdd1db46422fd6266ba84ed88429c3a
This way, is it possible to call sun.rising_time(observer: observer)
and sun.setting_time(observer: observer)
with only one calculation.
I kept #rise_transit_set_time
and #rise_set_azimuths
as I noticed in other libraries that it was quite custom to provide an array or tuple for this data. I can see the value of getting all the times or azimuths in one variable assignment statement, without making the developer have to go this way.
Thank you for the feedback, I would be happy to know what you think.
If I understand it correct the new algorithm calculates data which is horizon related: times and azimuths. If I'm correct it could be also used to calculate values for twilight related data (astronomical twilight, nautical twilight, civil twilight). In your current approach you would need a method which returns an array with very many values and a very long name.
So maybe it would be better to return an object with explicit accessors (for example implemented as Struct):
sun = Astronoby::Sun.new(epoch: epoch)
he = sun.horizontal_events(observer: observer)
he.astronomical_twilight_end_time
he.nautical_twilight_end_time
he.civil_twilight_end_time
he.rising_time
he.transit_time
he.setting_time
he.civil_twilight_begin_time
he.nautical_twilight_begin_time
he.astronomical_twilight_begin_time
# same for azimuth
Yes, we need a better name for horizontal_events
but I hope the idea is clear.
The advantages of this approach would be:
The only disadvantage I see is a new class with a good name and an according method name for the sun instance. ;-)
What do you think?
@janfri Something is bothering me about having a "horizontal events" class: the fact that it looks universal, while some events only apply to the Sun.
I have this PR ready for twilight times, they are currently available from Sun
. Rising, transit and setting times (and azimuths) apply to any body, like stars and planets, but the twilight only applies to the Sun's light.
I can see the value of extracting twilight events times into a dedicated class if it doesn't feel right to have them in Sun
, but I feel like they still should be separated from "universal" events like rising and setting that apply to any body on the celestial sphere.
The algorithm (from Practical Astronomy with your Calculator or Spreadsheet) requires data from the observer and the sunrise and sunset times, which are publicly available from Sun
, so it is technically not a problem to extract twilight methods into a dedicated class. But in that case, the sources of information for sunrise/sunset + twilight times are separated, while they are all related to the sun, which feels like a missed opportunity.
I would like also to point out that part of what you suggested (and thank you for suggesting solutions 🙏) is currently possible with the current implementation of RiseTransitSet
:
he = Astronoby::RiseTransitSet.new(
observer: observer,
date: date,
coordinates_of_the_previous_day: coordinates_of_the_previous_day,
coordinates_of_the_day: coordinates_of_the_day,
coordinates_of_the_next_day: coordinates_of_the_next_day,
additional_altitude: Angle.from_dms(0, 50, 0) # Sun's semi-diameter
)
he.times
he.azimuths
he.transit_altitude
I admit it's not perfect, having three different coordinates is important for close moving objects (by opposition with stars that look almost fixed within a few days) and the current API is not split into multiple methods like #rising_time
.
How about having a "universal" class for "universal events" and a subclass for "events" of the sun:
class UniversalEvents
attr_accessor :rising_time, :rising_azimuth, :transit_time, :transit_altitude, :setting_time, :setting_azimuth
end
class SunEvents < UniversalEvents
attr_accessor :morning_civil_twilight_time, :evening_civil_twilight_time, :morning_nautical_twilight_time # ...
end
events1 = some_star.events(observer: observer)
events1.class # => UniversalEvents
events1.rising_time
events2 = sun.events(observer: observer)
events2.class # => SunEvents
events2.morning_civil_twilight_time
events2.rising_time
events3 = sun.events(observer: a_different_observer)
events3.class # => SunEvents
events3.morning_civil_twilight_time
events3.rising_time
The names of the classes "UniversalEvents" and "SunEvents" and the method name "events" are only to demonstrate my idea. I'm sure there are better alternatives.
This approach doesn't need any memoization for different observers because the data is stored in an explicit instance of UniversalEvents or SunEvents.
My main point is: use an object with clever method names instead of an array as return value of the method called with the sun (or other celestial body).
Btw. I'm only a weired Ruby guy who looks with a naive view at your api. But one who is developing in Ruby since over two decades. You are doing all the hard work with the maths. :)
I like this approach, @janfri!
I think I'm going to implement this in a follow-up PR as this one is already quite big and is required for other work. However I won't make a new version until this is fully worked out, so that hopefully the Sun's API is stable.
Btw. I'm only a weired Ruby guy who looks with a naive view at your api. But one who is developing in Ruby since over two decades. You are doing all the hard work with the maths. :)
I really appreciate you taking time to share your experience. Because this kind of work already exists in other languages, but not in Ruby, it is important to me to develop the library with quality Ruby code, following conventions and Ruby's spirit. I might be less experienced, but what you suggested so far totally makes sense to me. It follows an object-oriented approach that is important in Ruby programming and it looks familiar to other gems I've explored.
This change is a complete refactoring of rising and setting times and azimuth. It is based on Astronomical Algorithms by Jean Meeus with a more complex but more accurate method than the ones used so far. It also introduces transit time and altitude.
The overall precision of these events have been increased, especially for the Sun. Most of the times are accurate with the IMCCE within 2 minutes precision, with results sometimes accurate to a few seconds.
More work may be needed in the future to increase the precision for distant stars, that should be in theory simpler to deal with but are less accurate for some reason (angles are off by a dozen minutes of arc).
The following methods have been added to
Sun
:#rise_transit_set_times
#rise_set_azimuths
#transit_time
#transit_altitude
They all require anobserver
key argument. Memoization on the observer has been implemented so that each of these methods calls don't compute the values all over again.This change includes a breaking change:
Astronoby::Body
is dropped. It will probably be replaced by something likeDistantStar
,Moon
andPlanet
in the future, in the same way we haveSun
.