astronoby 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/.standard.yml +1 -0
  4. data/CHANGELOG.md +203 -3
  5. data/README.md +69 -288
  6. data/UPGRADING.md +267 -0
  7. data/docs/README.md +196 -0
  8. data/docs/angles.md +137 -0
  9. data/docs/celestial_bodies.md +107 -0
  10. data/docs/configuration.md +98 -0
  11. data/docs/coordinates.md +167 -0
  12. data/docs/ephem.md +85 -0
  13. data/docs/equinoxes_solstices_times.md +31 -0
  14. data/docs/glossary.md +152 -0
  15. data/docs/instant.md +139 -0
  16. data/docs/moon_phases.md +79 -0
  17. data/docs/observer.md +65 -0
  18. data/docs/reference_frames.md +138 -0
  19. data/docs/rise_transit_set_times.md +119 -0
  20. data/docs/twilight_times.md +123 -0
  21. data/lib/astronoby/aberration.rb +56 -31
  22. data/lib/astronoby/angle.rb +20 -16
  23. data/lib/astronoby/angles/dms.rb +2 -2
  24. data/lib/astronoby/angles/hms.rb +2 -2
  25. data/lib/astronoby/bodies/earth.rb +62 -0
  26. data/lib/astronoby/bodies/jupiter.rb +28 -0
  27. data/lib/astronoby/bodies/mars.rb +28 -0
  28. data/lib/astronoby/bodies/mercury.rb +32 -0
  29. data/lib/astronoby/bodies/moon.rb +51 -298
  30. data/lib/astronoby/bodies/neptune.rb +32 -0
  31. data/lib/astronoby/bodies/saturn.rb +37 -0
  32. data/lib/astronoby/bodies/solar_system_body.rb +232 -0
  33. data/lib/astronoby/bodies/sun.rb +33 -214
  34. data/lib/astronoby/bodies/uranus.rb +16 -0
  35. data/lib/astronoby/bodies/venus.rb +36 -0
  36. data/lib/astronoby/cache.rb +188 -0
  37. data/lib/astronoby/configuration.rb +92 -0
  38. data/lib/astronoby/constants.rb +17 -2
  39. data/lib/astronoby/constellation.rb +12 -0
  40. data/lib/astronoby/constellations/data.rb +42 -0
  41. data/lib/astronoby/constellations/finder.rb +35 -0
  42. data/lib/astronoby/constellations/repository.rb +20 -0
  43. data/lib/astronoby/coordinates/ecliptic.rb +2 -37
  44. data/lib/astronoby/coordinates/equatorial.rb +28 -10
  45. data/lib/astronoby/coordinates/horizontal.rb +0 -46
  46. data/lib/astronoby/corrections/light_time_delay.rb +90 -0
  47. data/lib/astronoby/data/constellations/constellation_names.dat +88 -0
  48. data/lib/astronoby/data/constellations/indexed_abbreviations.dat +88 -0
  49. data/lib/astronoby/data/constellations/radec_to_index.dat +238 -0
  50. data/lib/astronoby/data/constellations/sorted_declinations.dat +202 -0
  51. data/lib/astronoby/data/constellations/sorted_right_ascensions.dat +237 -0
  52. data/lib/astronoby/deflection.rb +187 -0
  53. data/lib/astronoby/distance.rb +9 -0
  54. data/lib/astronoby/ephem.rb +39 -0
  55. data/lib/astronoby/equinox_solstice.rb +22 -19
  56. data/lib/astronoby/errors.rb +4 -0
  57. data/lib/astronoby/events/moon_phases.rb +15 -13
  58. data/lib/astronoby/events/rise_transit_set_calculator.rb +376 -0
  59. data/lib/astronoby/events/rise_transit_set_event.rb +13 -0
  60. data/lib/astronoby/events/rise_transit_set_events.rb +13 -0
  61. data/lib/astronoby/events/twilight_calculator.rb +221 -0
  62. data/lib/astronoby/events/twilight_event.rb +28 -0
  63. data/lib/astronoby/events/twilight_events.rb +22 -115
  64. data/lib/astronoby/instant.rb +176 -0
  65. data/lib/astronoby/julian_date.rb +78 -0
  66. data/lib/astronoby/mean_obliquity.rb +24 -13
  67. data/lib/astronoby/nutation.rb +235 -42
  68. data/lib/astronoby/observer.rb +55 -0
  69. data/lib/astronoby/precession.rb +102 -18
  70. data/lib/astronoby/reference_frame.rb +50 -0
  71. data/lib/astronoby/reference_frames/apparent.rb +60 -0
  72. data/lib/astronoby/reference_frames/astrometric.rb +21 -0
  73. data/lib/astronoby/reference_frames/geometric.rb +20 -0
  74. data/lib/astronoby/reference_frames/mean_of_date.rb +38 -0
  75. data/lib/astronoby/reference_frames/topocentric.rb +72 -0
  76. data/lib/astronoby/time/greenwich_sidereal_time.rb +2 -2
  77. data/lib/astronoby/true_obliquity.rb +3 -3
  78. data/lib/astronoby/util/maths.rb +70 -73
  79. data/lib/astronoby/util/time.rb +455 -32
  80. data/lib/astronoby/vector.rb +36 -0
  81. data/lib/astronoby/velocity.rb +116 -0
  82. data/lib/astronoby/version.rb +1 -1
  83. data/lib/astronoby.rb +33 -5
  84. metadata +117 -24
  85. data/.tool-versions +0 -1
  86. data/Gemfile +0 -5
  87. data/Gemfile.lock +0 -80
  88. data/benchmark/README.md +0 -131
  89. data/benchmark/benchmark.rb +0 -259
  90. data/benchmark/data/imcce.csv.zip +0 -0
  91. data/benchmark/data/sun_calc.csv.zip +0 -0
  92. data/lib/astronoby/astronomical_models/ephemeride_lunaire_parisienne.rb +0 -143
  93. data/lib/astronoby/epoch.rb +0 -22
  94. data/lib/astronoby/events/observation_events.rb +0 -285
  95. data/lib/astronoby/events/rise_transit_set_iteration.rb +0 -218
  96. data/lib/astronoby/util/astrodynamics.rb +0 -60
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class TwilightCalculator
5
+ TWILIGHTS = [
6
+ CIVIL = :civil,
7
+ NAUTICAL = :nautical,
8
+ ASTRONOMICAL = :astronomical
9
+ ].freeze
10
+
11
+ TWILIGHT_ANGLES = {
12
+ CIVIL => Angle.from_degrees(96),
13
+ NAUTICAL => Angle.from_degrees(102),
14
+ ASTRONOMICAL => Angle.from_degrees(108)
15
+ }.freeze
16
+
17
+ PERIODS_OF_THE_DAY = [
18
+ MORNING = :morning,
19
+ EVENING = :evening
20
+ ].freeze
21
+
22
+ def initialize(observer:, ephem:)
23
+ @observer = observer
24
+ @ephem = ephem
25
+ end
26
+
27
+ def event_on(date, utc_offset: 0)
28
+ start_time = Time
29
+ .new(date.year, date.month, date.day, 0, 0, 0, utc_offset)
30
+ end_time = Time
31
+ .new(date.year, date.month, date.day, 23, 59, 59, utc_offset)
32
+ events = events_between(start_time, end_time)
33
+
34
+ TwilightEvent.new(
35
+ morning_civil_twilight_time:
36
+ events.morning_civil_twilight_times.first,
37
+ evening_civil_twilight_time:
38
+ events.evening_civil_twilight_times.first,
39
+ morning_nautical_twilight_time:
40
+ events.morning_nautical_twilight_times.first,
41
+ evening_nautical_twilight_time:
42
+ events.evening_nautical_twilight_times.first,
43
+ morning_astronomical_twilight_time:
44
+ events.morning_astronomical_twilight_times.first,
45
+ evening_astronomical_twilight_time:
46
+ events.evening_astronomical_twilight_times.first
47
+ )
48
+ end
49
+
50
+ def events_between(start_time, end_time)
51
+ rts_events = Astronoby::RiseTransitSetCalculator.new(
52
+ body: Sun,
53
+ observer: @observer,
54
+ ephem: @ephem
55
+ ).events_between(start_time, end_time)
56
+
57
+ equatorial_by_time = {}
58
+
59
+ (rts_events.rising_times + rts_events.setting_times)
60
+ .compact
61
+ .each do |event_time|
62
+ rounded_time = event_time.round
63
+ next if equatorial_by_time.key?(rounded_time)
64
+
65
+ instant = Instant.from_time(rounded_time)
66
+ sun_at_time = Sun.new(instant: instant, ephem: @ephem)
67
+ equatorial_by_time[rounded_time] = sun_at_time.apparent.equatorial
68
+ end
69
+
70
+ morning_civil = []
71
+ evening_civil = []
72
+ morning_nautical = []
73
+ evening_nautical = []
74
+ morning_astronomical = []
75
+ evening_astronomical = []
76
+
77
+ arrays_by_period = {
78
+ MORNING => {
79
+ CIVIL => morning_civil,
80
+ NAUTICAL => morning_nautical,
81
+ ASTRONOMICAL => morning_astronomical
82
+ },
83
+ EVENING => {
84
+ CIVIL => evening_civil,
85
+ NAUTICAL => evening_nautical,
86
+ ASTRONOMICAL => evening_astronomical
87
+ }
88
+ }
89
+
90
+ [
91
+ [rts_events.rising_times, MORNING],
92
+ [rts_events.setting_times, EVENING]
93
+ ].each do |times, period|
94
+ times.each do |event_time|
95
+ next unless event_time
96
+
97
+ equatorial_coordinates = equatorial_by_time[event_time.round]
98
+ TWILIGHT_ANGLES.each do |twilight, angle|
99
+ arrays_by_period[period][twilight] << compute_twilight_time_from(
100
+ period,
101
+ angle,
102
+ event_time,
103
+ equatorial_coordinates
104
+ )
105
+ end
106
+ end
107
+ end
108
+
109
+ within_range = ->(time) { time && time >= start_time && time <= end_time }
110
+
111
+ TwilightEvents.new(
112
+ morning_civil.select(&within_range),
113
+ evening_civil.select(&within_range),
114
+ morning_nautical.select(&within_range),
115
+ evening_nautical.select(&within_range),
116
+ morning_astronomical.select(&within_range),
117
+ evening_astronomical.select(&within_range)
118
+ )
119
+ end
120
+
121
+ def time_for_zenith_angle(
122
+ date:,
123
+ period_of_the_day:,
124
+ zenith_angle:,
125
+ utc_offset: 0
126
+ )
127
+ unless PERIODS_OF_THE_DAY.include?(period_of_the_day)
128
+ raise IncompatibleArgumentsError,
129
+ "Only #{PERIODS_OF_THE_DAY.join(" or ")} are allowed as " \
130
+ "period_of_the_day, got #{period_of_the_day}"
131
+ end
132
+
133
+ observation_events = get_observation_events(date, utc_offset: utc_offset)
134
+ midday_instant = create_midday_instant(date, utc_offset: utc_offset)
135
+ sun_at_midday = Sun.new(instant: midday_instant, ephem: @ephem)
136
+ equatorial_coordinates = sun_at_midday.apparent.equatorial
137
+
138
+ compute_twilight_time(
139
+ period_of_the_day,
140
+ zenith_angle,
141
+ observation_events,
142
+ equatorial_coordinates
143
+ )
144
+ end
145
+
146
+ private
147
+
148
+ def create_midday_instant(date, utc_offset: 0)
149
+ time = Time.new(date.year, date.month, date.day, 12, 0, 0, utc_offset)
150
+ Instant.from_time(time)
151
+ end
152
+
153
+ def get_observation_events(date, utc_offset: 0)
154
+ Astronoby::RiseTransitSetCalculator.new(
155
+ body: Sun,
156
+ observer: @observer,
157
+ ephem: @ephem
158
+ ).event_on(date, utc_offset: utc_offset)
159
+ end
160
+
161
+ def compute_twilight_time(
162
+ period_of_the_day,
163
+ zenith_angle,
164
+ observation_events,
165
+ equatorial_coordinates
166
+ )
167
+ period_time = if period_of_the_day == MORNING
168
+ observation_events.rising_time
169
+ else
170
+ observation_events.setting_time
171
+ end
172
+
173
+ compute_twilight_time_from(
174
+ period_of_the_day,
175
+ zenith_angle,
176
+ period_time,
177
+ equatorial_coordinates
178
+ )
179
+ end
180
+
181
+ def compute_twilight_time_from(
182
+ period_of_the_day,
183
+ zenith_angle,
184
+ period_time,
185
+ equatorial_coordinates
186
+ )
187
+ # If the sun doesn't rise or set on this day, we can't calculate twilight
188
+ return nil unless period_time
189
+
190
+ hour_angle_at_period = equatorial_coordinates
191
+ .compute_hour_angle(time: period_time, longitude: @observer.longitude)
192
+
193
+ term1 = zenith_angle.cos -
194
+ @observer.latitude.sin * equatorial_coordinates.declination.sin
195
+ term2 = @observer.latitude.cos * equatorial_coordinates.declination.cos
196
+ hour_angle_ratio_at_twilight = term1 / term2
197
+
198
+ # Check if twilight occurs at this location and date
199
+ return nil unless hour_angle_ratio_at_twilight.between?(-1, 1)
200
+
201
+ hour_angle_at_twilight = Angle.acos(hour_angle_ratio_at_twilight)
202
+ time_sign = -1
203
+
204
+ if period_of_the_day == MORNING
205
+ hour_angle_at_twilight = Angle.from_degrees(
206
+ Constants::DEGREES_PER_CIRCLE - hour_angle_at_twilight.degrees
207
+ )
208
+ time_sign = 1
209
+ end
210
+
211
+ twilight_in_hours =
212
+ time_sign * (hour_angle_at_twilight - hour_angle_at_period).hours *
213
+ GreenwichSiderealTime::SIDEREAL_MINUTE_IN_UT_MINUTE
214
+ twilight_in_seconds = time_sign *
215
+ twilight_in_hours *
216
+ Constants::SECONDS_PER_HOUR
217
+
218
+ (period_time + twilight_in_seconds).round
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class TwilightEvent
5
+ attr_reader :morning_civil_twilight_time,
6
+ :evening_civil_twilight_time,
7
+ :morning_nautical_twilight_time,
8
+ :evening_nautical_twilight_time,
9
+ :morning_astronomical_twilight_time,
10
+ :evening_astronomical_twilight_time
11
+
12
+ def initialize(
13
+ morning_civil_twilight_time: nil,
14
+ evening_civil_twilight_time: nil,
15
+ morning_nautical_twilight_time: nil,
16
+ evening_nautical_twilight_time: nil,
17
+ morning_astronomical_twilight_time: nil,
18
+ evening_astronomical_twilight_time: nil
19
+ )
20
+ @morning_civil_twilight_time = morning_civil_twilight_time
21
+ @evening_civil_twilight_time = evening_civil_twilight_time
22
+ @morning_nautical_twilight_time = morning_nautical_twilight_time
23
+ @evening_nautical_twilight_time = evening_nautical_twilight_time
24
+ @morning_astronomical_twilight_time = morning_astronomical_twilight_time
25
+ @evening_astronomical_twilight_time = evening_astronomical_twilight_time
26
+ end
27
+ end
28
+ end
@@ -1,121 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
- module Events
5
- class TwilightEvents
6
- TWILIGHTS = [
7
- CIVIL = :civil,
8
- NAUTICAL = :nautical,
9
- ASTRONOMICAL = :astronomical
10
- ].freeze
11
-
12
- TWILIGHT_ANGLES = {
13
- CIVIL => Angle.from_degrees(96),
14
- NAUTICAL => Angle.from_degrees(102),
15
- ASTRONOMICAL => Angle.from_degrees(108)
16
- }.freeze
17
-
18
- PERIODS_OF_THE_DAY = [
19
- MORNING = :morning,
20
- EVENING = :evening
21
- ].freeze
22
-
23
- attr_reader :morning_civil_twilight_time,
24
- :evening_civil_twilight_time,
25
- :morning_nautical_twilight_time,
26
- :evening_nautical_twilight_time,
27
- :morning_astronomical_twilight_time,
28
- :evening_astronomical_twilight_time
29
-
30
- def initialize(observer:, sun:)
31
- @observer = observer
32
- @sun = sun
33
- PERIODS_OF_THE_DAY.each do |period_of_the_day|
34
- TWILIGHT_ANGLES.each do |twilight, _|
35
- zenith_angle = TWILIGHT_ANGLES[twilight]
36
- instance_variable_set(
37
- :"@#{period_of_the_day}_#{twilight}_twilight_time",
38
- compute(period_of_the_day, zenith_angle)
39
- )
40
- end
41
- end
42
- end
43
-
44
- # @param period_of_the_day [Symbol] :morning or :evening
45
- # @param zenith_angle [Angle] The zenith angle of the twilight
46
- def time_for_zenith_angle(period_of_the_day:, zenith_angle:)
47
- unless PERIODS_OF_THE_DAY.include?(period_of_the_day)
48
- raise IncompatibleArgumentsError,
49
- "Only #{PERIODS_OF_THE_DAY.join(" or ")} are allowed as period_of_the_day, got #{period_of_the_day}"
50
- end
51
-
52
- compute(period_of_the_day, zenith_angle)
53
- end
54
-
55
- private
56
-
57
- # Source:
58
- # Title: Practical Astronomy with your Calculator or Spreadsheet
59
- # Authors: Peter Duffett-Smith and Jonathan Zwart
60
- # Edition: Cambridge University Press
61
- # Chapter: 50 - Twilight
62
- def compute(period_of_the_day, zenith_angle)
63
- period_time = if period_of_the_day == MORNING
64
- observation_events.rising_time
65
- else
66
- observation_events.setting_time
67
- end
68
-
69
- hour_angle_at_period = equatorial_coordinates_at_midday
70
- .compute_hour_angle(time: period_time, longitude: @observer.longitude)
71
-
72
- term1 = zenith_angle.cos -
73
- @observer.latitude.sin *
74
- @equatorial_coordinates_at_midday.declination.sin
75
- term2 = @observer.latitude.cos *
76
- equatorial_coordinates_at_midday.declination.cos
77
- hour_angle_ratio_at_twilight = term1 / term2
78
-
79
- return unless hour_angle_ratio_at_twilight.between?(-1, 1)
80
-
81
- hour_angle_at_twilight = Angle.acos(hour_angle_ratio_at_twilight)
82
- time_sign = -1
83
-
84
- if period_of_the_day == MORNING
85
- hour_angle_at_twilight = Angle.from_degrees(
86
- Constants::DEGREES_PER_CIRCLE - hour_angle_at_twilight.degrees
87
- )
88
- time_sign = 1
89
- end
90
-
91
- twilight_in_hours =
92
- time_sign * (hour_angle_at_twilight - hour_angle_at_period).hours *
93
- GreenwichSiderealTime::SIDEREAL_MINUTE_IN_UT_MINUTE
94
- twilight_in_seconds = time_sign *
95
- twilight_in_hours *
96
- Constants::SECONDS_PER_HOUR
97
-
98
- (period_time + twilight_in_seconds).round
99
- end
100
-
101
- def observation_events
102
- @observation_events ||= @sun.observation_events(observer: @observer)
103
- end
104
-
105
- def midday
106
- date = @sun.time.to_date
107
- Time.utc(date.year, date.month, date.day, 12)
108
- end
109
-
110
- def sun_at_midday
111
- Sun.new(time: midday)
112
- end
113
-
114
- def equatorial_coordinates_at_midday
115
- @equatorial_coordinates_at_midday ||= sun_at_midday
116
- .apparent_ecliptic_coordinates
117
- .to_apparent_equatorial(epoch: Epoch.from_time(midday))
118
- end
4
+ class TwilightEvents
5
+ attr_reader :morning_civil_twilight_times,
6
+ :evening_civil_twilight_times,
7
+ :morning_nautical_twilight_times,
8
+ :evening_nautical_twilight_times,
9
+ :morning_astronomical_twilight_times,
10
+ :evening_astronomical_twilight_times
11
+
12
+ def initialize(
13
+ morning_civil,
14
+ evening_civil,
15
+ morning_nautical,
16
+ evening_nautical,
17
+ morning_astronomical,
18
+ evening_astronomical
19
+ )
20
+ @morning_civil_twilight_times = morning_civil
21
+ @evening_civil_twilight_times = evening_civil
22
+ @morning_nautical_twilight_times = morning_nautical
23
+ @evening_nautical_twilight_times = evening_nautical
24
+ @morning_astronomical_twilight_times = morning_astronomical
25
+ @evening_astronomical_twilight_times = evening_astronomical
119
26
  end
120
27
  end
121
28
  end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Astronoby
6
+ # Represents a specific instant in time using Terrestrial Time (TT) as its
7
+ # internal representation. This class provides conversions between different
8
+ # time scales commonly used in astronomy:
9
+ # - Terrestrial Time (TT)
10
+ # - International Atomic Time (TAI)
11
+ # - Universal Time Coordinated (UTC)
12
+ # - Greenwich Mean Sidereal Time (GMST)
13
+ #
14
+ # @example Create an instant from the current time
15
+ # instant = Astronoby::Instant.from_time(Time.now)
16
+ # instant.tai # Get International Atomic Time
17
+ #
18
+ # @example Create an instant from Terrestrial Time
19
+ # instant = Astronoby::Instant.from_terrestrial_time(2460000.5)
20
+ #
21
+ class Instant
22
+ include Comparable
23
+
24
+ # The adjustment value to align our noon-based Julian Date with the
25
+ # midnight-based epoch required by Ruby's `DateTime.jd` constructor.
26
+ # Our internal time values are standard astronomical Julian Dates, which
27
+ # start at noon. `DateTime.jd` expects a day that starts at the preceding
28
+ # midnight. This constant adds 0.5 days (12 hours) to make the conversion.
29
+ DATETIME_JD_EPOCH_ADJUSTMENT = 0.5
30
+
31
+ class << self
32
+ # Creates a new Instant from a Terrestrial Time value
33
+ #
34
+ # @param terrestrial_time [Numeric] the Terrestrial Time as a Julian Date
35
+ # @return [Astronoby::Instant] a new Instant object
36
+ # @raise [Astronoby::UnsupportedFormatError] if terrestrial_time is not
37
+ # numeric
38
+ def from_terrestrial_time(terrestrial_time)
39
+ new(terrestrial_time)
40
+ end
41
+
42
+ # Creates a new Instant from a Time object
43
+ #
44
+ # @param time [Time] a Time object to convert
45
+ # @return [Astronoby::Instant] a new Instant object
46
+ def from_time(time)
47
+ delta_t = Util::Time.terrestrial_universal_time_delta(time)
48
+ terrestrial_time = time.utc.to_datetime.ajd +
49
+ Rational(delta_t, Constants::SECONDS_PER_DAY)
50
+ from_terrestrial_time(terrestrial_time)
51
+ end
52
+
53
+ # Creates a new Instant from a Julian Date in UTC
54
+ #
55
+ # @param julian_date [Numeric] the Julian Date in UTC
56
+ # @return [Astronoby::Instant] a new Instant object
57
+ def from_utc_julian_date(julian_date)
58
+ delta_t = Util::Time.terrestrial_universal_time_delta(julian_date)
59
+ terrestrial_time = julian_date +
60
+ Rational(delta_t, Constants::SECONDS_PER_DAY)
61
+ from_terrestrial_time(terrestrial_time)
62
+ end
63
+ end
64
+
65
+ attr_reader :terrestrial_time
66
+ alias_method :tt, :terrestrial_time
67
+ alias_method :julian_date, :terrestrial_time
68
+
69
+ # Initialize a new Instant
70
+ #
71
+ # @param terrestrial_time [Numeric] the Terrestrial Time as Julian Date
72
+ # @raise [Astronoby::UnsupportedFormatError] if terrestrial_time is not
73
+ # numeric
74
+ def initialize(terrestrial_time)
75
+ unless terrestrial_time.is_a?(Numeric)
76
+ raise UnsupportedFormatError, "terrestrial_time must be a Numeric"
77
+ end
78
+
79
+ @terrestrial_time = terrestrial_time
80
+ freeze
81
+ end
82
+
83
+ # Calculate the time difference between two Instant objects
84
+ #
85
+ # @param other [Astronoby::Instant] another Instant to compare with
86
+ # @return [Numeric] the difference in days
87
+ def diff(other)
88
+ @terrestrial_time - other.terrestrial_time
89
+ end
90
+
91
+ # Convert to DateTime (UTC)
92
+ #
93
+ # @return [DateTime] the UTC time as DateTime
94
+ def to_datetime
95
+ DateTime.jd(
96
+ @terrestrial_time -
97
+ Rational(delta_t / Constants::SECONDS_PER_DAY) +
98
+ DATETIME_JD_EPOCH_ADJUSTMENT
99
+ )
100
+ end
101
+
102
+ # Convert to Date (UTC)
103
+ #
104
+ # @return [Date] the UTC date
105
+ def to_date
106
+ to_datetime.to_date
107
+ end
108
+
109
+ # Convert to Time (UTC)
110
+ #
111
+ # @return [Time] the UTC time
112
+ def to_time
113
+ to_datetime.to_time.utc
114
+ end
115
+
116
+ # Get the ΔT (Delta T) value for this instant
117
+ # ΔT is the difference between TT and UT1
118
+ #
119
+ # @return [Numeric] Delta T in seconds
120
+ def delta_t
121
+ Util::Time.terrestrial_universal_time_delta(@terrestrial_time)
122
+ end
123
+
124
+ # Get the Greenwich Mean Sidereal Time
125
+ #
126
+ # @return [Numeric] the sidereal time in radians
127
+ def gmst
128
+ GreenwichSiderealTime.from_utc(to_time).time
129
+ end
130
+
131
+ # Get the International Atomic Time (TAI)
132
+ #
133
+ # @return [Numeric] TAI as Julian Date
134
+ def tai
135
+ @terrestrial_time -
136
+ Rational(Constants::TAI_TT_OFFSET, Constants::SECONDS_PER_DAY)
137
+ end
138
+
139
+ # Get the Barycentric Dynamical Time (TDB)
140
+ # Note: Currently approximated as equal to TT
141
+ #
142
+ # @return [Numeric] TDB as Julian Date
143
+ def tdb
144
+ # This is technically false, there is a slight difference between TT and
145
+ # TDB. However, this difference is so small that currenly Astronoby
146
+ # doesn't support it and consider they are the same value.
147
+ @terrestrial_time
148
+ end
149
+
150
+ # Get the offset between TT and UTC for this instant
151
+ #
152
+ # @return [Numeric] the offset in days
153
+ def utc_offset
154
+ @terrestrial_time - to_time.utc.to_datetime.ajd
155
+ end
156
+
157
+ # Calculate hash value for the instant
158
+ #
159
+ # @return [Integer] hash value
160
+ def hash
161
+ [@terrestrial_time, self.class].hash
162
+ end
163
+
164
+ # Compare this instant with another
165
+ #
166
+ # @param other [Astronoby::Instant] another instant to compare with
167
+ # @return [Integer, nil] -1, 0, 1 for less than, equal to, greater than;
168
+ # nil if other is not an Instant
169
+ def <=>(other)
170
+ return unless other.is_a?(self.class)
171
+
172
+ @terrestrial_time <=> other.terrestrial_time
173
+ end
174
+ alias_method :eql?, :==
175
+ end
176
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # @see https://en.wikipedia.org/wiki/Julian_day
5
+ # @see https://en.wikipedia.org/wiki/Epoch_(astronomy)
6
+ class JulianDate
7
+ # Starting year for Besselian epoch calculations
8
+ # @return [Integer] 1900
9
+ BESSELIAN_EPOCH_STARTING_YEAR = 1900
10
+
11
+ # Starting year for Julian epoch calculations
12
+ # @return [Integer] 2000
13
+ JULIAN_EPOCH_STARTING_YEAR = 2000
14
+
15
+ # Julian Date for Besselian epoch 1875.0
16
+ # @return [Float] 2405889.258550475
17
+ B1875 = 2405889.258550475
18
+
19
+ # Julian Date for Besselian epoch 1900.0
20
+ # @return [Float] 2415020.31352
21
+ B1900 = 2415020.31352
22
+
23
+ # Julian Date for Julian epoch 1950.0
24
+ # @return [Float] 2433282.5
25
+ J1950 = 2433282.5
26
+
27
+ # Julian Date for Julian epoch 2000.0 (current standard)
28
+ # @return [Float] 2451545.0
29
+ J2000 = 2451545.0
30
+
31
+ # Default epoch used by the library
32
+ # @return [Float] 2451545.0
33
+ DEFAULT_EPOCH = J2000
34
+
35
+ # Converts a Time object to Julian Date
36
+ #
37
+ # @param time [Time] the time to convert
38
+ # @return [Rational] the Julian Date
39
+ #
40
+ # @example
41
+ # JulianDate.from_time(Time.utc(2000, 1, 1, 12, 0, 0))
42
+ # # => 2451545.0
43
+ def self.from_time(time)
44
+ time.to_datetime.ajd
45
+ end
46
+
47
+ # Converts a Julian year to Julian Date
48
+ #
49
+ # Uses the formula: JD = J2000 + 365.25 * (year - 2000)
50
+ #
51
+ # @param julian_year [Float] the Julian year
52
+ # @return [Float] the Julian Date
53
+ #
54
+ # @example
55
+ # JulianDate.from_julian_year(2025.0)
56
+ # # => 2460676.25
57
+ def self.from_julian_year(julian_year)
58
+ J2000 + Constants::DAYS_PER_JULIAN_YEAR *
59
+ (julian_year - JULIAN_EPOCH_STARTING_YEAR)
60
+ end
61
+
62
+ # Converts a Besselian year to Julian Date
63
+ #
64
+ # Uses the formula: JD = B1900 + 365.242198781 * (year - 1900)
65
+ # where 365.242198781 is the tropical year length at B1900.
66
+ #
67
+ # @param besselian_year [Float] the Besselian year
68
+ # @return [Float] the Julian Date
69
+ #
70
+ # @example
71
+ # JulianDate.from_besselian_year(1875.0)
72
+ # # => 2405889.258550475
73
+ def self.from_besselian_year(besselian_year)
74
+ B1900 + Constants::TROPICAL_YEAR_AT_B1900 *
75
+ (besselian_year - BESSELIAN_EPOCH_STARTING_YEAR)
76
+ end
77
+ end
78
+ end
@@ -1,32 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "date"
3
+ # TODO: This needs to be improved by receiving an instant instead of an Epoch
4
+ # as these coefficients work with TT (Terrestrial Time).
4
5
 
5
6
  module Astronoby
6
7
  class MeanObliquity
7
8
  # Source:
8
9
  # IAU resolution in 2006 in favor of the P03 astronomical model
9
- # The Astronomical Almanac for 2010
10
+ # https://syrte.obspm.fr/iau2006/aa03_412_P03.pdf
10
11
 
11
- EPOCH_OF_REFERENCE = Epoch::DEFAULT_EPOCH
12
+ EPOCH_OF_REFERENCE = JulianDate::DEFAULT_EPOCH
12
13
  OBLIQUITY_OF_REFERENCE = 23.4392794
13
14
 
14
- def self.for_epoch(epoch)
15
- return obliquity_of_reference if epoch == EPOCH_OF_REFERENCE
15
+ def self.at(instant)
16
+ return obliquity_of_reference if instant.julian_date == EPOCH_OF_REFERENCE
16
17
 
17
- t = (epoch - EPOCH_OF_REFERENCE) / Constants::DAYS_PER_JULIAN_CENTURY
18
+ t = Rational(
19
+ instant.julian_date - EPOCH_OF_REFERENCE,
20
+ Constants::DAYS_PER_JULIAN_CENTURY
21
+ )
22
+
23
+ epsilon0 = obliquity_of_reference_in_arcseconds
24
+ c1 = -46.836769
25
+ c2 = -0.0001831
26
+ c3 = 0.00200340
27
+ c4 = -0.000000576
28
+ c5 = -0.0000000434
18
29
 
19
- Angle.from_degrees(
20
- obliquity_of_reference.degrees - (
21
- 46.815 * t -
22
- 0.0006 * t * t +
23
- 0.00181 * t * t * t
24
- ) / Constants::SECONDS_PER_DEGREE
30
+ Angle.from_degree_arcseconds(
31
+ epsilon0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * c5))))
25
32
  )
26
33
  end
27
34
 
28
35
  def self.obliquity_of_reference
29
- Angle.from_dms(23, 26, 21.45)
36
+ Angle.from_degree_arcseconds(obliquity_of_reference_in_arcseconds)
37
+ end
38
+
39
+ def self.obliquity_of_reference_in_arcseconds
40
+ 84381.406
30
41
  end
31
42
  end
32
43
  end