astronoby 0.7.0 → 0.9.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +145 -3
  4. data/README.md +59 -33
  5. data/UPGRADING.md +75 -21
  6. data/docs/README.md +224 -0
  7. data/docs/angles.md +137 -0
  8. data/docs/configuration.md +98 -0
  9. data/docs/coordinates.md +167 -0
  10. data/docs/deep_sky_bodies.md +101 -0
  11. data/docs/ephem.md +85 -0
  12. data/docs/equinoxes_solstices_times.md +31 -0
  13. data/docs/glossary.md +152 -0
  14. data/docs/instant.md +139 -0
  15. data/docs/moon_phases.md +79 -0
  16. data/docs/observer.md +65 -0
  17. data/docs/reference_frames.md +138 -0
  18. data/docs/rise_transit_set_times.md +119 -0
  19. data/docs/solar_system_bodies.md +107 -0
  20. data/docs/twilight_times.md +123 -0
  21. data/lib/astronoby/angle.rb +6 -2
  22. data/lib/astronoby/angular_velocity.rb +76 -0
  23. data/lib/astronoby/bodies/deep_sky_object.rb +44 -0
  24. data/lib/astronoby/bodies/deep_sky_object_position.rb +127 -0
  25. data/lib/astronoby/bodies/earth.rb +12 -2
  26. data/lib/astronoby/bodies/jupiter.rb +17 -0
  27. data/lib/astronoby/bodies/mars.rb +17 -0
  28. data/lib/astronoby/bodies/mercury.rb +21 -0
  29. data/lib/astronoby/bodies/moon.rb +50 -36
  30. data/lib/astronoby/bodies/neptune.rb +21 -0
  31. data/lib/astronoby/bodies/saturn.rb +26 -0
  32. data/lib/astronoby/bodies/solar_system_body.rb +162 -27
  33. data/lib/astronoby/bodies/sun.rb +25 -2
  34. data/lib/astronoby/bodies/uranus.rb +5 -0
  35. data/lib/astronoby/bodies/venus.rb +25 -0
  36. data/lib/astronoby/cache.rb +189 -0
  37. data/lib/astronoby/configuration.rb +92 -0
  38. data/lib/astronoby/constants.rb +11 -3
  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/equatorial.rb +5 -8
  44. data/lib/astronoby/data/constellations/constellation_names.dat +88 -0
  45. data/lib/astronoby/data/constellations/indexed_abbreviations.dat +88 -0
  46. data/lib/astronoby/data/constellations/radec_to_index.dat +238 -0
  47. data/lib/astronoby/data/constellations/sorted_declinations.dat +202 -0
  48. data/lib/astronoby/data/constellations/sorted_right_ascensions.dat +237 -0
  49. data/lib/astronoby/distance.rb +6 -0
  50. data/lib/astronoby/equinox_solstice.rb +2 -2
  51. data/lib/astronoby/events/extremum_calculator.rb +233 -0
  52. data/lib/astronoby/events/extremum_event.rb +15 -0
  53. data/lib/astronoby/events/moon_phases.rb +15 -14
  54. data/lib/astronoby/events/rise_transit_set_calculator.rb +39 -12
  55. data/lib/astronoby/events/twilight_calculator.rb +116 -61
  56. data/lib/astronoby/events/twilight_events.rb +28 -0
  57. data/lib/astronoby/instant.rb +34 -6
  58. data/lib/astronoby/julian_date.rb +78 -0
  59. data/lib/astronoby/mean_obliquity.rb +8 -10
  60. data/lib/astronoby/nutation.rb +11 -3
  61. data/lib/astronoby/observer.rb +1 -1
  62. data/lib/astronoby/precession.rb +48 -38
  63. data/lib/astronoby/reference_frame.rb +2 -1
  64. data/lib/astronoby/reference_frames/apparent.rb +1 -11
  65. data/lib/astronoby/reference_frames/mean_of_date.rb +1 -1
  66. data/lib/astronoby/reference_frames/topocentric.rb +2 -12
  67. data/lib/astronoby/stellar_propagation.rb +162 -0
  68. data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +22 -0
  69. data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +64 -0
  70. data/lib/astronoby/time/greenwich_sidereal_time.rb +20 -58
  71. data/lib/astronoby/time/local_apparent_sidereal_time.rb +42 -0
  72. data/lib/astronoby/time/local_mean_sidereal_time.rb +42 -0
  73. data/lib/astronoby/time/local_sidereal_time.rb +35 -26
  74. data/lib/astronoby/time/sidereal_time.rb +42 -0
  75. data/lib/astronoby/true_obliquity.rb +2 -3
  76. data/lib/astronoby/util/time.rb +62 -44
  77. data/lib/astronoby/velocity.rb +5 -0
  78. data/lib/astronoby/version.rb +1 -1
  79. data/lib/astronoby.rb +19 -1
  80. metadata +71 -11
  81. data/Gemfile +0 -5
  82. data/Gemfile.lock +0 -102
  83. data/benchmark/README.md +0 -131
  84. data/benchmark/benchmark.rb +0 -259
  85. data/benchmark/data/imcce.csv.zip +0 -0
  86. data/benchmark/data/sun_calc.csv.zip +0 -0
  87. data/lib/astronoby/epoch.rb +0 -22
@@ -15,17 +15,19 @@ module Astronoby
15
15
  # @param month [Integer] Requested month
16
16
  # @return [Array<Astronoby::MoonPhase>] List of Moon phases
17
17
  def self.phases_for(year:, month:)
18
- [
19
- MoonPhase.first_quarter(new(year, month, :first_quarter, -0.75).time),
20
- MoonPhase.full_moon(new(year, month, :full_moon, -0.5).time),
21
- MoonPhase.last_quarter(new(year, month, :last_quarter, -0.25).time),
22
- MoonPhase.new_moon(new(year, month, :new_moon, 0).time),
23
- MoonPhase.first_quarter(new(year, month, :first_quarter, 0.25).time),
24
- MoonPhase.full_moon(new(year, month, :full_moon, 0.5).time),
25
- MoonPhase.last_quarter(new(year, month, :last_quarter, 0.75).time),
26
- MoonPhase.new_moon(new(year, month, :new_moon, 1).time),
27
- MoonPhase.first_quarter(new(year, month, :first_quarter, 1.25).time)
28
- ].select { _1.time.month == month }
18
+ Astronoby.cache.fetch([:moon_phases, year, month]) do
19
+ [
20
+ MoonPhase.first_quarter(new(year, month, :first_quarter, -0.75).time),
21
+ MoonPhase.full_moon(new(year, month, :full_moon, -0.5).time),
22
+ MoonPhase.last_quarter(new(year, month, :last_quarter, -0.25).time),
23
+ MoonPhase.new_moon(new(year, month, :new_moon, 0).time),
24
+ MoonPhase.first_quarter(new(year, month, :first_quarter, 0.25).time),
25
+ MoonPhase.full_moon(new(year, month, :full_moon, 0.5).time),
26
+ MoonPhase.last_quarter(new(year, month, :last_quarter, 0.75).time),
27
+ MoonPhase.new_moon(new(year, month, :new_moon, 1).time),
28
+ MoonPhase.first_quarter(new(year, month, :first_quarter, 1.25).time)
29
+ ].select { _1.time.month == month }
30
+ end
29
31
  end
30
32
 
31
33
  # @param year [Integer] Requested year
@@ -43,13 +45,12 @@ module Astronoby
43
45
  def time
44
46
  correction = moon_phases_periodic_terms
45
47
  .public_send(:"#{@phase}_correction")
46
- terrestrial_time = Epoch.to_utc(
48
+ terrestrial_time = Instant.from_terrestrial_time(
47
49
  julian_ephemeris_day +
48
50
  correction +
49
51
  moon_phases_periodic_terms.additional_corrections
50
52
  )
51
- delta = Util::Time.terrestrial_universal_time_delta(terrestrial_time)
52
- (terrestrial_time - delta).round
53
+ terrestrial_time.to_time.round
53
54
  end
54
55
 
55
56
  private
@@ -23,7 +23,11 @@ module Astronoby
23
23
  SUN_REFRACTION_ANGLE = -Angle.from_dms(0, 50, 0)
24
24
  EVENT_TYPES = [:rising, :transit, :setting].freeze
25
25
 
26
- def initialize(body:, observer:, ephem:)
26
+ # @param body [Astronoby::SolarSystemBody, Astronoby::DeepSkyObject]
27
+ # Celestial body for which to calculate events
28
+ # @param observer [Astronoby::Observer] Observer location
29
+ # @param ephem [::Ephem::SPK, nil] Ephemeris data source
30
+ def initialize(body:, observer:, ephem: nil)
27
31
  @body = body
28
32
  @observer = observer
29
33
  @ephem = ephem
@@ -237,13 +241,14 @@ module Astronoby
237
241
  )
238
242
 
239
243
  # Calculate time differences
240
- time_differences = new_instants.each_with_index.map do |instant, i|
241
- Instant.from_terrestrial_time(instant.tt - old_instants[i].tt)
244
+ time_differences_in_days = new_instants.each_with_index.map do |instant, i|
245
+ instant.tt - old_instants[i].tt
242
246
  end
243
247
 
244
248
  # Calculate hour angle rate (radians per day)
245
249
  hour_angle_rates = hour_angle_changes.each_with_index.map do |angle, i|
246
- angle.radians / time_differences[i].tt
250
+ denominator = time_differences_in_days[i]
251
+ angle.radians / denominator
247
252
  end
248
253
 
249
254
  # Store current values for next iteration
@@ -255,7 +260,8 @@ module Astronoby
255
260
  .each_with_index
256
261
  .map do |angle, i|
257
262
  ratio = angle.radians / hour_angle_rates[i]
258
- [ratio.nan? ? 0 : ratio, MIN_TIME_ADJUSTMENT].max
263
+ time_adjustment = (ratio.nan? || ratio.infinite?) ? 0 : ratio
264
+ [time_adjustment, MIN_TIME_ADJUSTMENT].max
259
265
  end
260
266
 
261
267
  # Apply time adjustments
@@ -268,10 +274,31 @@ module Astronoby
268
274
  end
269
275
 
270
276
  def calculate_positions_at_instants(instants)
271
- instants.map do |instant|
272
- @body
273
- .new(instant: instant, ephem: @ephem)
274
- .observed_by(@observer)
277
+ if Astronoby.configuration.cache_enabled?
278
+ instants.map do |instant|
279
+ cache_key = CacheKey.generate(
280
+ :observed_by,
281
+ instant,
282
+ @body.to_s,
283
+ @observer.hash
284
+ )
285
+ Astronoby.cache.fetch(cache_key) do
286
+ @body
287
+ .at(instant, ephem: @ephem)
288
+ .observed_by(@observer)
289
+ end
290
+ end
291
+ else
292
+ @positions_cache ||= {}
293
+ precision = Astronoby.configuration.cache_precision(:observed_by)
294
+ instants.map do |instant|
295
+ rounded_instant = Instant.from_terrestrial_time(
296
+ instant.tt.round(precision)
297
+ )
298
+ @positions_cache[rounded_instant] ||= @body
299
+ .at(rounded_instant, ephem: @ephem)
300
+ .observed_by(@observer)
301
+ end
275
302
  end
276
303
  end
277
304
 
@@ -329,10 +356,9 @@ module Astronoby
329
356
  end
330
357
 
331
358
  def horizon_angle(distance)
332
- case @body.name
333
- when "Astronoby::Sun"
359
+ if @body == Astronoby::Sun
334
360
  SUN_REFRACTION_ANGLE
335
- when "Astronoby::Moon"
361
+ elsif @body == Astronoby::Moon
336
362
  STANDARD_REFRACTION_ANGLE -
337
363
  Angle.from_radians(Moon::EQUATORIAL_RADIUS.m / distance.m)
338
364
  else
@@ -347,6 +373,7 @@ module Astronoby
347
373
  @sample_instants = nil
348
374
  @start_instant = nil
349
375
  @end_instant = nil
376
+ @positions_cache = nil
350
377
  end
351
378
  end
352
379
  end
@@ -24,72 +24,114 @@ module Astronoby
24
24
  @ephem = ephem
25
25
  end
26
26
 
27
- def event_on(date)
28
- observation_events = get_observation_events(date)
29
- midday_instant = create_midday_instant(date)
30
- sun_at_midday = Sun.new(instant: midday_instant, ephem: @ephem)
31
- equatorial_coordinates = sun_at_midday.apparent.equatorial
32
-
33
- morning_civil = compute_twilight_time(
34
- MORNING,
35
- TWILIGHT_ANGLES[CIVIL],
36
- observation_events,
37
- equatorial_coordinates
38
- )
39
-
40
- evening_civil = compute_twilight_time(
41
- EVENING,
42
- TWILIGHT_ANGLES[CIVIL],
43
- observation_events,
44
- equatorial_coordinates
45
- )
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)
46
33
 
47
- morning_nautical = compute_twilight_time(
48
- MORNING,
49
- TWILIGHT_ANGLES[NAUTICAL],
50
- observation_events,
51
- equatorial_coordinates
52
- )
53
-
54
- evening_nautical = compute_twilight_time(
55
- EVENING,
56
- TWILIGHT_ANGLES[NAUTICAL],
57
- observation_events,
58
- equatorial_coordinates
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
59
47
  )
48
+ end
60
49
 
61
- morning_astronomical = compute_twilight_time(
62
- MORNING,
63
- TWILIGHT_ANGLES[ASTRONOMICAL],
64
- observation_events,
65
- equatorial_coordinates
66
- )
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
67
108
 
68
- evening_astronomical = compute_twilight_time(
69
- EVENING,
70
- TWILIGHT_ANGLES[ASTRONOMICAL],
71
- observation_events,
72
- equatorial_coordinates
73
- )
109
+ within_range = ->(time) { time && time >= start_time && time <= end_time }
74
110
 
75
- TwilightEvent.new(
76
- morning_civil_twilight_time: morning_civil,
77
- evening_civil_twilight_time: evening_civil,
78
- morning_nautical_twilight_time: morning_nautical,
79
- evening_nautical_twilight_time: evening_nautical,
80
- morning_astronomical_twilight_time: morning_astronomical,
81
- evening_astronomical_twilight_time: evening_astronomical
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)
82
118
  )
83
119
  end
84
120
 
85
- def time_for_zenith_angle(date:, period_of_the_day:, zenith_angle:)
121
+ def time_for_zenith_angle(
122
+ date:,
123
+ period_of_the_day:,
124
+ zenith_angle:,
125
+ utc_offset: 0
126
+ )
86
127
  unless PERIODS_OF_THE_DAY.include?(period_of_the_day)
87
128
  raise IncompatibleArgumentsError,
88
- "Only #{PERIODS_OF_THE_DAY.join(" or ")} are allowed as period_of_the_day, got #{period_of_the_day}"
129
+ "Only #{PERIODS_OF_THE_DAY.join(" or ")} are allowed as " \
130
+ "period_of_the_day, got #{period_of_the_day}"
89
131
  end
90
132
 
91
- observation_events = get_observation_events(date)
92
- midday_instant = create_midday_instant(date)
133
+ observation_events = get_observation_events(date, utc_offset: utc_offset)
134
+ midday_instant = create_midday_instant(date, utc_offset: utc_offset)
93
135
  sun_at_midday = Sun.new(instant: midday_instant, ephem: @ephem)
94
136
  equatorial_coordinates = sun_at_midday.apparent.equatorial
95
137
 
@@ -103,17 +145,17 @@ module Astronoby
103
145
 
104
146
  private
105
147
 
106
- def create_midday_instant(date)
107
- time = Time.utc(date.year, date.month, date.day, 12)
148
+ def create_midday_instant(date, utc_offset: 0)
149
+ time = Time.new(date.year, date.month, date.day, 12, 0, 0, utc_offset)
108
150
  Instant.from_time(time)
109
151
  end
110
152
 
111
- def get_observation_events(date)
153
+ def get_observation_events(date, utc_offset: 0)
112
154
  Astronoby::RiseTransitSetCalculator.new(
113
155
  body: Sun,
114
156
  observer: @observer,
115
157
  ephem: @ephem
116
- ).event_on(date)
158
+ ).event_on(date, utc_offset: utc_offset)
117
159
  end
118
160
 
119
161
  def compute_twilight_time(
@@ -128,8 +170,21 @@ module Astronoby
128
170
  observation_events.setting_time
129
171
  end
130
172
 
131
- # If the sun doesn't rise or set on this day, we can't calculate
132
- # twilight
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
133
188
  return nil unless period_time
134
189
 
135
190
  hour_angle_at_period = equatorial_coordinates
@@ -155,7 +210,7 @@ module Astronoby
155
210
 
156
211
  twilight_in_hours =
157
212
  time_sign * (hour_angle_at_twilight - hour_angle_at_period).hours *
158
- GreenwichSiderealTime::SIDEREAL_MINUTE_IN_UT_MINUTE
213
+ GreenwichMeanSiderealTime::SIDEREAL_MINUTE_IN_UT_MINUTE
159
214
  twilight_in_seconds = time_sign *
160
215
  twilight_in_hours *
161
216
  Constants::SECONDS_PER_HOUR
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
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
26
+ end
27
+ end
28
+ end
@@ -21,7 +21,12 @@ module Astronoby
21
21
  class Instant
22
22
  include Comparable
23
23
 
24
- JULIAN_DAY_NUMBER_OFFSET = 0.5
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
25
30
 
26
31
  class << self
27
32
  # Creates a new Instant from a Terrestrial Time value
@@ -89,8 +94,8 @@ module Astronoby
89
94
  def to_datetime
90
95
  DateTime.jd(
91
96
  @terrestrial_time -
92
- Rational(delta_t / Constants::SECONDS_PER_DAY) +
93
- JULIAN_DAY_NUMBER_OFFSET
97
+ Rational(delta_t, Constants::SECONDS_PER_DAY) +
98
+ DATETIME_JD_EPOCH_ADJUSTMENT
94
99
  )
95
100
  end
96
101
 
@@ -118,9 +123,32 @@ module Astronoby
118
123
 
119
124
  # Get the Greenwich Mean Sidereal Time
120
125
  #
121
- # @return [Numeric] the sidereal time in radians
126
+ # @return [Numeric] the sidereal time in hours
122
127
  def gmst
123
- GreenwichSiderealTime.from_utc(to_time).time
128
+ GreenwichMeanSiderealTime.from_utc(to_time).time
129
+ end
130
+
131
+ # Get the Greenwich Apparent Sidereal Time
132
+ #
133
+ # @return [Numeric] the sidereal time in hours
134
+ def gast
135
+ GreenwichApparentSiderealTime.from_utc(to_time).time
136
+ end
137
+
138
+ # Get the Local Mean Sidereal Time
139
+ #
140
+ # @param longitude [Astronoby::Angle] the observer's longitude
141
+ # @return [Numeric] the sidereal time in hours
142
+ def lmst(longitude:)
143
+ LocalMeanSiderealTime.from_utc(to_time, longitude: longitude).time
144
+ end
145
+
146
+ # Get the Local Apparent Sidereal Time
147
+ #
148
+ # @param longitude [Astronoby::Angle] the observer's longitude
149
+ # @return [Numeric] the sidereal time in hours
150
+ def last(longitude:)
151
+ LocalApparentSiderealTime.from_utc(to_time, longitude: longitude).time
124
152
  end
125
153
 
126
154
  # Get the International Atomic Time (TAI)
@@ -146,7 +174,7 @@ module Astronoby
146
174
  #
147
175
  # @return [Numeric] the offset in days
148
176
  def utc_offset
149
- @terrestrial_time - to_time.utc.to_datetime.ajd
177
+ Rational(delta_t / Constants::SECONDS_PER_DAY)
150
178
  end
151
179
 
152
180
  # Calculate hash value for the instant
@@ -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
@@ -9,36 +9,34 @@ module Astronoby
9
9
  # IAU resolution in 2006 in favor of the P03 astronomical model
10
10
  # https://syrte.obspm.fr/iau2006/aa03_412_P03.pdf
11
11
 
12
- EPOCH_OF_REFERENCE = Epoch::DEFAULT_EPOCH
12
+ EPOCH_OF_REFERENCE = JulianDate::DEFAULT_EPOCH
13
13
  OBLIQUITY_OF_REFERENCE = 23.4392794
14
14
 
15
- def self.for_epoch(epoch)
16
- 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
17
17
 
18
18
  t = Rational(
19
- (epoch - EPOCH_OF_REFERENCE),
19
+ instant.julian_date - EPOCH_OF_REFERENCE,
20
20
  Constants::DAYS_PER_JULIAN_CENTURY
21
21
  )
22
22
 
23
- epsilon0 = obliquity_of_reference_in_milliarcseconds
23
+ epsilon0 = obliquity_of_reference_in_arcseconds
24
24
  c1 = -46.836769
25
25
  c2 = -0.0001831
26
26
  c3 = 0.00200340
27
27
  c4 = -0.000000576
28
28
  c5 = -0.0000000434
29
29
 
30
- Angle.from_dms(
31
- 0,
32
- 0,
30
+ Angle.from_degree_arcseconds(
33
31
  epsilon0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * c5))))
34
32
  )
35
33
  end
36
34
 
37
35
  def self.obliquity_of_reference
38
- Angle.from_dms(0, 0, obliquity_of_reference_in_milliarcseconds)
36
+ Angle.from_degree_arcseconds(obliquity_of_reference_in_arcseconds)
39
37
  end
40
38
 
41
- def self.obliquity_of_reference_in_milliarcseconds
39
+ def self.obliquity_of_reference_in_arcseconds
42
40
  84381.406
43
41
  end
44
42
  end
@@ -104,7 +104,7 @@ module Astronoby
104
104
 
105
105
  # @return [Matrix] The nutation matrix
106
106
  def matrix
107
- mean_obliquity = MeanObliquity.for_epoch(@instant.tt)
107
+ mean_obliquity = MeanObliquity.at(@instant)
108
108
  true_obliquity = mean_obliquity + nutation_in_obliquity
109
109
  build_nutation_matrix(
110
110
  mean_obliquity: mean_obliquity,
@@ -125,6 +125,14 @@ module Astronoby
125
125
 
126
126
  private
127
127
 
128
+ def cache_key
129
+ @_cache_key ||= CacheKey.generate(:nutation, @instant)
130
+ end
131
+
132
+ def cache
133
+ Astronoby.cache
134
+ end
135
+
128
136
  def iau2000a
129
137
  a = fundamental_arguments
130
138
 
@@ -172,7 +180,7 @@ module Astronoby
172
180
  end
173
181
 
174
182
  def iau2000b_angles
175
- @iau2000b_angles ||= begin
183
+ cache.fetch(cache_key) do
176
184
  dpsi, deps = iau2000b
177
185
  dpsi = Angle.from_degree_arcseconds(dpsi / 1e7)
178
186
  deps = Angle.from_degree_arcseconds(deps / 1e7)
@@ -206,7 +214,7 @@ module Astronoby
206
214
 
207
215
  def julian_centuries
208
216
  @julian_centuries ||=
209
- (@instant.tt - Epoch::J2000) / Constants::DAYS_PER_JULIAN_CENTURY
217
+ (@instant.tt - JulianDate::J2000) / Constants::DAYS_PER_JULIAN_CENTURY
210
218
  end
211
219
 
212
220
  # IAU 2006/2000A formula for the mean anomaly of the Moon
@@ -62,7 +62,7 @@ module Astronoby
62
62
  def earth_fixed_rotation_matrix_for(instant)
63
63
  dpsi = Nutation.new(instant: instant).nutation_in_longitude
64
64
 
65
- mean_obliquity = MeanObliquity.for_epoch(instant.tt)
65
+ mean_obliquity = MeanObliquity.at(instant)
66
66
 
67
67
  gast = Angle.from_radians(
68
68
  Angle.from_hours(instant.gmst).radians +