astronoby 0.4.0 → 0.5.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.
@@ -3,30 +3,28 @@
3
3
  module Astronoby
4
4
  module Coordinates
5
5
  class Horizontal
6
- attr_reader :azimuth, :altitude, :latitude, :longitude
6
+ attr_reader :azimuth, :altitude, :observer
7
7
 
8
8
  def initialize(
9
9
  azimuth:,
10
10
  altitude:,
11
- latitude:,
12
- longitude:
11
+ observer:
13
12
  )
14
13
  @azimuth = azimuth
15
14
  @altitude = altitude
16
- @latitude = latitude
17
- @longitude = longitude
15
+ @observer = observer
18
16
  end
19
17
 
20
18
  def to_equatorial(time:)
21
- t0 = @altitude.sin * @latitude.sin +
22
- @altitude.cos * @latitude.cos * @azimuth.cos
19
+ t0 = @altitude.sin * latitude.sin +
20
+ @altitude.cos * latitude.cos * @azimuth.cos
23
21
 
24
22
  declination = Angle.asin(t0)
25
23
 
26
- t1 = @altitude.sin - @latitude.sin * declination.sin
24
+ t1 = @altitude.sin - latitude.sin * declination.sin
27
25
 
28
26
  hour_angle_degrees = Angle
29
- .acos(t1 / (@latitude.cos * declination.cos))
27
+ .acos(t1 / (latitude.cos * declination.cos))
30
28
  .degrees
31
29
 
32
30
  if @azimuth.sin.positive?
@@ -38,7 +36,7 @@ module Astronoby
38
36
  hour_angle_hours = Angle.from_degrees(hour_angle_degrees).hours
39
37
  lst = GreenwichSiderealTime
40
38
  .from_utc(time.utc)
41
- .to_lst(longitude: @longitude)
39
+ .to_lst(longitude: longitude)
42
40
  right_ascension_decimal = lst.time - hour_angle_hours
43
41
 
44
42
  if right_ascension_decimal.negative?
@@ -52,6 +50,16 @@ module Astronoby
52
50
  declination: declination
53
51
  )
54
52
  end
53
+
54
+ private
55
+
56
+ def latitude
57
+ @observer.latitude
58
+ end
59
+
60
+ def longitude
61
+ @observer.longitude
62
+ end
55
63
  end
56
64
  end
57
65
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class Distance
5
+ include Comparable
6
+
7
+ class << self
8
+ def zero
9
+ new(0)
10
+ end
11
+
12
+ def from_meters(meters)
13
+ new(meters)
14
+ end
15
+ alias_method :from_m, :from_meters
16
+
17
+ def from_kilometers(kilometers)
18
+ meters = kilometers * Constants::KILOMETER_IN_METERS
19
+ from_meters(meters)
20
+ end
21
+ alias_method :from_km, :from_kilometers
22
+
23
+ def from_astronomical_units(astronomical_units)
24
+ meters = astronomical_units * Constants::ASTRONOMICAL_UNIT_IN_METERS
25
+ from_meters(meters)
26
+ end
27
+ alias_method :from_au, :from_astronomical_units
28
+ end
29
+
30
+ attr_reader :meters
31
+ alias_method :m, :meters
32
+
33
+ def initialize(meters)
34
+ @meters = meters
35
+ freeze
36
+ end
37
+
38
+ def kilometers
39
+ @meters / Constants::KILOMETER_IN_METERS.to_f
40
+ end
41
+ alias_method :km, :kilometers
42
+
43
+ def astronomical_units
44
+ @meters / Constants::ASTRONOMICAL_UNIT_IN_METERS.to_f
45
+ end
46
+ alias_method :au, :astronomical_units
47
+
48
+ def +(other)
49
+ self.class.from_meters(meters + other.meters)
50
+ end
51
+
52
+ def -(other)
53
+ self.class.from_meters(@meters - other.meters)
54
+ end
55
+
56
+ def -@
57
+ self.class.from_meters(-@meters)
58
+ end
59
+
60
+ def positive?
61
+ meters > 0
62
+ end
63
+
64
+ def negative?
65
+ meters < 0
66
+ end
67
+
68
+ def zero?
69
+ meters.zero?
70
+ end
71
+
72
+ def hash
73
+ [meters, self.class].hash
74
+ end
75
+
76
+ def <=>(other)
77
+ return unless other.is_a?(self.class)
78
+
79
+ meters <=> other.meters
80
+ end
81
+ alias_method :eql?, :==
82
+ end
83
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Events
5
+ class MoonPhases
6
+ BASE_YEAR = 2000
7
+
8
+ # Source:
9
+ # Title: Astronomical Algorithms
10
+ # Author: Jean Meeus
11
+ # Edition: 2nd edition
12
+ # Chapter: 49 - Phases of the Moon
13
+
14
+ # @param year [Integer] Requested year
15
+ # @param month [Integer] Requested month
16
+ # @return [Array<Astronoby::MoonPhase>] List of Moon phases
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
+ ].select { _1.time.month == month }
28
+ end
29
+
30
+ # @param year [Integer] Requested year
31
+ # @param month [Integer] Requested month
32
+ # @param phase [Symbol] Moon phase
33
+ # @param phase_increment [Float] Phase increment
34
+ def initialize(year, month, phase, phase_increment)
35
+ @year = year
36
+ @month = month
37
+ @phase = phase
38
+ @phase_increment = phase_increment
39
+ end
40
+
41
+ # @return [Time] Time of the Moon phase
42
+ def time
43
+ correction = moon_phases_periodic_terms
44
+ .public_send(:"#{@phase}_correction")
45
+ terrestrial_time = Epoch.to_utc(
46
+ julian_ephemeris_day +
47
+ correction +
48
+ moon_phases_periodic_terms.additional_corrections
49
+ )
50
+ delta = Util::Time.terrestrial_universal_time_delta(terrestrial_time)
51
+ (terrestrial_time - delta).round
52
+ end
53
+
54
+ private
55
+
56
+ def portion_of_year
57
+ days_in_year = Date.new(@year, 12, 31) - Date.new(@year, 1, 1)
58
+ mid_month = Date.new(@year, @month, 15)
59
+ mid_month.yday / days_in_year.to_f
60
+ end
61
+
62
+ def approximate_time
63
+ ((@year + portion_of_year - BASE_YEAR) * 12.3685).floor + @phase_increment
64
+ end
65
+
66
+ def julian_centuries
67
+ approximate_time / 1236.85
68
+ end
69
+
70
+ def julian_ephemeris_day
71
+ 2451550.09766 +
72
+ 29.530588861 * approximate_time +
73
+ 0.00015437 * julian_centuries**2 -
74
+ 0.000000150 * julian_centuries**3 +
75
+ 0.00000000073 * julian_centuries**4
76
+ end
77
+
78
+ def eccentricity_correction
79
+ 1 -
80
+ 0.002516 * julian_centuries -
81
+ 0.0000074 * julian_centuries**2
82
+ end
83
+
84
+ def sun_mean_anomaly
85
+ Angle.from_degrees(
86
+ (
87
+ 2.5534 +
88
+ 29.10535670 * approximate_time -
89
+ 0.0000014 * julian_centuries**2 -
90
+ 0.00000011 * julian_centuries**3
91
+ ) % 360
92
+ )
93
+ end
94
+
95
+ def moon_mean_anomaly
96
+ Angle.from_degrees(
97
+ (
98
+ 201.5643 +
99
+ 385.81693528 * approximate_time +
100
+ 0.0107582 * julian_centuries**2 +
101
+ 0.00001238 * julian_centuries**3 -
102
+ 0.000000058 * julian_centuries**4
103
+ ) % 360
104
+ )
105
+ end
106
+
107
+ def moon_argument_of_latitude
108
+ Angle.from_degrees(
109
+ (
110
+ 160.7108 +
111
+ 390.67050284 * approximate_time -
112
+ 0.0016118 * julian_centuries**2 -
113
+ 0.00000227 * julian_centuries**3 +
114
+ 0.000000011 * julian_centuries**4
115
+ ) % 360
116
+ )
117
+ end
118
+
119
+ def longitude_of_the_ascending_node
120
+ Angle.from_degrees(
121
+ (
122
+ 124.7746 -
123
+ 1.56375588 * approximate_time +
124
+ 0.0020672 * julian_centuries**2 +
125
+ 0.00000215 * julian_centuries**3
126
+ ) % 360
127
+ )
128
+ end
129
+
130
+ def moon_phases_periodic_terms
131
+ MoonPhasesPeriodicTerms.new(
132
+ julian_centuries: julian_centuries,
133
+ time: approximate_time,
134
+ eccentricity_correction: eccentricity_correction,
135
+ moon_mean_anomaly: moon_mean_anomaly,
136
+ sun_mean_anomaly: sun_mean_anomaly,
137
+ moon_argument_of_latitude: moon_argument_of_latitude,
138
+ longitude_of_the_ascending_node: longitude_of_the_ascending_node
139
+ )
140
+ end
141
+ end
142
+ end
143
+ end
@@ -6,6 +6,7 @@ module Astronoby
6
6
  STANDARD_ALTITUDE = Angle.from_dms(0, -34, 0)
7
7
  RISING_SETTING_HOUR_ANGLE_RATIO_RANGE = (-1..1)
8
8
  EARTH_SIDEREAL_ROTATION_RATE = 360.98564736629
9
+ ITERATION_PRECISION = 0.0001
9
10
 
10
11
  attr_reader :rising_time,
11
12
  :rising_azimuth,
@@ -51,46 +52,80 @@ module Astronoby
51
52
  private
52
53
 
53
54
  def compute
54
- @transit_time = Util::Time.decimal_hour_to_time(@date, initial_transit)
55
+ @initial_transit = initial_transit
56
+ @transit_time = Util::Time.decimal_hour_to_time(@date, @initial_transit)
55
57
  @transit_altitude = local_horizontal_altitude_transit
56
58
 
57
59
  return if h0.nil?
58
60
 
59
- delta_m_rising = (local_horizontal_altitude_rising - shift).degrees./(
60
- Constants::DEGREES_PER_CIRCLE *
61
- declination_rising.cos *
62
- @observer.latitude.cos *
63
- local_hour_angle_rising.sin
61
+ initial_rising = rationalize_decimal_time(
62
+ @initial_transit - h0.degrees / Constants::DEGREES_PER_CIRCLE
64
63
  )
65
- delta_m_transit = -local_hour_angle_transit.degrees / Constants::DEGREES_PER_CIRCLE
66
- delta_m_setting = (local_horizontal_altitude_setting - shift).degrees./(
67
- Constants::DEGREES_PER_CIRCLE *
68
- declination_setting.cos *
69
- @observer.latitude.cos *
70
- local_hour_angle_setting.sin
64
+
65
+ initial_setting = rationalize_decimal_time(
66
+ @initial_transit + h0.degrees / Constants::DEGREES_PER_CIRCLE
71
67
  )
72
68
 
73
- corrected_rising = rationalize_decimal_hours(
74
- Constants::HOURS_PER_DAY * (initial_rising + delta_m_rising)
69
+ @final_rising, @final_transit, @final_setting =
70
+ iterate(initial_rising, @initial_transit, initial_setting)
71
+
72
+ rationalized_corrected_rising = rationalize_decimal_hours(
73
+ Constants::HOURS_PER_DAY * @final_rising
75
74
  )
76
- corrected_transit = rationalize_decimal_hours(
77
- Constants::HOURS_PER_DAY * (initial_transit + delta_m_transit)
75
+ rationalized_corrected_transit = rationalize_decimal_hours(
76
+ Constants::HOURS_PER_DAY * @final_transit
78
77
  )
79
- corrected_setting = rationalize_decimal_hours(
80
- Constants::HOURS_PER_DAY * (initial_setting + delta_m_setting)
78
+ rationalized_corrected_setting = rationalize_decimal_hours(
79
+ Constants::HOURS_PER_DAY * @final_setting
81
80
  )
82
81
 
83
- @rising_time = Util::Time.decimal_hour_to_time(@date, corrected_rising)
82
+ @rising_time = Util::Time.decimal_hour_to_time(@date, rationalized_corrected_rising)
84
83
  @rising_azimuth = local_horizontal_azimuth_rising
85
- @transit_time = Util::Time.decimal_hour_to_time(@date, corrected_transit)
86
- @setting_time = Util::Time.decimal_hour_to_time(@date, corrected_setting)
84
+ @transit_time = Util::Time.decimal_hour_to_time(@date, rationalized_corrected_transit)
85
+ @transit_altitude = local_horizontal_altitude_transit
86
+ @setting_time = Util::Time.decimal_hour_to_time(@date, rationalized_corrected_setting)
87
87
  @setting_azimuth = local_horizontal_azimuth_setting
88
88
  end
89
89
 
90
+ def iterate(initial_rising, initial_transit, initial_setting)
91
+ delta = 1
92
+ corrected_rising = initial_rising
93
+ corrected_transit = initial_transit
94
+ corrected_setting = initial_setting
95
+ until delta < ITERATION_PRECISION
96
+ iterate = RiseTransitSetIteration.new(
97
+ observer: @observer,
98
+ date: @date,
99
+ coordinates_of_the_next_day: @coordinates_of_the_next_day,
100
+ coordinates_of_the_day: @coordinates_of_the_day,
101
+ coordinates_of_the_previous_day: @coordinates_of_the_previous_day,
102
+ shift: shift,
103
+ initial_rising: corrected_rising,
104
+ initial_transit: corrected_transit,
105
+ initial_setting: corrected_setting
106
+ ).iterate
107
+ delta = iterate.sum
108
+ corrected_rising = rationalize_decimal_time corrected_rising + iterate[0]
109
+ corrected_transit = rationalize_decimal_time corrected_transit + iterate[1]
110
+ corrected_setting = rationalize_decimal_time corrected_setting + iterate[2]
111
+ end
112
+ [corrected_rising, corrected_transit, corrected_setting]
113
+ end
114
+
90
115
  def observer_longitude
91
116
  # Longitude must be treated positively westwards from the meridian of
92
117
  # Greenwich, and negatively to the east
93
- @observer_longitude ||= -@observer.longitude
118
+ -@observer.longitude
119
+ end
120
+
121
+ def initial_transit
122
+ rationalize_decimal_time(
123
+ (
124
+ @coordinates_of_the_day.right_ascension.degrees +
125
+ observer_longitude.degrees -
126
+ apparent_gst_at_midnight.degrees
127
+ ) / Constants::DEGREES_PER_CIRCLE
128
+ )
94
129
  end
95
130
 
96
131
  def h0
@@ -106,115 +141,46 @@ module Astronoby
106
141
  end
107
142
 
108
143
  def apparent_gst_at_midnight
109
- @apparent_gst_at_midnight ||= Angle.from_hours(
144
+ Angle.from_hours(
110
145
  GreenwichSiderealTime.from_utc(
111
146
  Time.utc(@date.year, @date.month, @date.day)
112
147
  ).time
113
148
  )
114
149
  end
115
150
 
116
- def initial_transit
117
- @initial_transit ||= rationalize_decimal_time(
118
- (
119
- @coordinates_of_the_day.right_ascension.degrees +
120
- observer_longitude.degrees -
121
- apparent_gst_at_midnight.degrees
122
- ) / Constants::DEGREES_PER_CIRCLE
123
- )
124
- end
125
-
126
- def initial_rising
127
- @initial_rising ||=
128
- rationalize_decimal_time(
129
- initial_transit - h0.degrees / Constants::DEGREES_PER_CIRCLE
130
- )
131
- end
132
-
133
- def initial_setting
134
- @initial_setting ||=
135
- rationalize_decimal_time(
136
- initial_transit + h0.degrees / Constants::DEGREES_PER_CIRCLE
137
- )
138
- end
139
-
140
- def gst_rising
141
- @gst_rising ||= Angle.from_degrees(
142
- apparent_gst_at_midnight.degrees +
143
- EARTH_SIDEREAL_ROTATION_RATE * initial_rising
144
- )
145
- end
146
-
147
151
  def gst_transit
148
- @gst_transit ||= Angle.from_degrees(
149
- apparent_gst_at_midnight.degrees +
150
- EARTH_SIDEREAL_ROTATION_RATE * initial_transit
151
- )
152
- end
153
-
154
- def gst_setting
155
- @gst_setting ||= Angle.from_degrees(
152
+ Angle.from_degrees(
156
153
  apparent_gst_at_midnight.degrees +
157
- EARTH_SIDEREAL_ROTATION_RATE * initial_setting
154
+ EARTH_SIDEREAL_ROTATION_RATE * (@final_transit || @initial_transit)
158
155
  )
159
156
  end
160
157
 
161
158
  def leap_day_portion
162
- @leap_day_portion ||= begin
163
- leap_seconds = Util::Time.terrestrial_universal_time_delta(@date)
164
- leap_seconds / Constants::SECONDS_PER_DAY
165
- end
166
- end
167
-
168
- def local_hour_angle_rising
169
- @local_hour_angle_rising ||=
170
- gst_rising - observer_longitude - right_ascension_rising
159
+ leap_seconds = Util::Time.terrestrial_universal_time_delta(@date)
160
+ leap_seconds / Constants::SECONDS_PER_DAY
171
161
  end
172
162
 
173
163
  def local_hour_angle_transit
174
- @local_hour_angle_transit ||=
175
- gst_transit - observer_longitude - right_ascension_transit
176
- end
177
-
178
- def local_hour_angle_setting
179
- @local_hour_angle_setting ||=
180
- gst_setting - observer_longitude - right_ascension_setting
181
- end
182
-
183
- def local_horizontal_altitude_rising
184
- @local_horizontal_altitude_rising ||= Angle.asin(
185
- @observer.latitude.sin * declination_rising.sin +
186
- @observer.latitude.cos * declination_rising.cos * local_hour_angle_rising.cos
187
- )
164
+ gst_transit - observer_longitude - right_ascension_transit
188
165
  end
189
166
 
190
167
  def local_horizontal_azimuth_rising
191
- @local_horizontal_azimuth_rising ||= begin
192
- shift = -@standard_altitude
193
- term1 = declination_rising.sin + shift.sin * @observer.latitude.cos
194
- term2 = shift.cos * @observer.latitude.cos
195
- angle = term1 / term2
196
- Angle.acos(angle)
197
- end
168
+ term1 = declination_rising.sin + (-shift).sin * @observer.latitude.cos
169
+ term2 = (-shift).cos * @observer.latitude.cos
170
+ angle = term1 / term2
171
+ Angle.acos(angle)
198
172
  end
199
173
 
200
174
  def local_horizontal_altitude_transit
201
- @local_horizontal_altitude_transit ||= Angle.asin(
175
+ Angle.asin(
202
176
  @observer.latitude.sin * declination_transit.sin +
203
177
  @observer.latitude.cos * declination_transit.cos * local_hour_angle_transit.cos
204
178
  )
205
179
  end
206
180
 
207
- def local_horizontal_altitude_setting
208
- @local_horizontal_altitude_setting ||= Angle.asin(
209
- @observer.latitude.sin * declination_setting.sin +
210
- @observer.latitude.cos * declination_setting.cos * local_hour_angle_setting.cos
211
- )
212
- end
213
-
214
181
  def local_horizontal_azimuth_setting
215
- shift = -@standard_altitude
216
- term1 = declination_setting.sin + shift.sin * @observer.latitude.cos
217
- term2 = shift.cos * @observer.latitude.cos
182
+ term1 = declination_setting.sin + (-shift).sin * @observer.latitude.cos
183
+ term2 = (-shift).cos * @observer.latitude.cos
218
184
  angle = term1 / term2
219
185
  Angle.from_degrees(
220
186
  Constants::DEGREES_PER_CIRCLE - Angle.acos(angle).degrees
@@ -233,19 +199,6 @@ module Astronoby
233
199
  decimal_hours
234
200
  end
235
201
 
236
- def right_ascension_rising
237
- Angle.from_degrees(
238
- Util::Maths.interpolate(
239
- [
240
- @coordinates_of_the_previous_day.right_ascension.degrees,
241
- @coordinates_of_the_day.right_ascension.degrees,
242
- @coordinates_of_the_next_day.right_ascension.degrees
243
- ],
244
- initial_rising + leap_day_portion
245
- )
246
- )
247
- end
248
-
249
202
  def right_ascension_transit
250
203
  Angle.from_degrees(
251
204
  Util::Maths.interpolate(
@@ -254,20 +207,7 @@ module Astronoby
254
207
  @coordinates_of_the_day.right_ascension.degrees,
255
208
  @coordinates_of_the_next_day.right_ascension.degrees
256
209
  ],
257
- initial_transit + leap_day_portion
258
- )
259
- )
260
- end
261
-
262
- def right_ascension_setting
263
- Angle.from_degrees(
264
- Util::Maths.interpolate(
265
- [
266
- @coordinates_of_the_previous_day.right_ascension.degrees,
267
- @coordinates_of_the_day.right_ascension.degrees,
268
- @coordinates_of_the_next_day.right_ascension.degrees
269
- ],
270
- initial_setting + leap_day_portion
210
+ (@final_transit || @initial_transit) + leap_day_portion
271
211
  )
272
212
  )
273
213
  end
@@ -280,7 +220,7 @@ module Astronoby
280
220
  @coordinates_of_the_day.declination.degrees,
281
221
  @coordinates_of_the_next_day.declination.degrees
282
222
  ],
283
- initial_rising + leap_day_portion
223
+ @final_rising + leap_day_portion
284
224
  )
285
225
  )
286
226
  end
@@ -293,7 +233,7 @@ module Astronoby
293
233
  @coordinates_of_the_day.declination.degrees,
294
234
  @coordinates_of_the_next_day.declination.degrees
295
235
  ],
296
- initial_transit + leap_day_portion
236
+ (@final_transit || @initial_transit) + leap_day_portion
297
237
  )
298
238
  )
299
239
  end
@@ -306,7 +246,7 @@ module Astronoby
306
246
  @coordinates_of_the_day.declination.degrees,
307
247
  @coordinates_of_the_next_day.declination.degrees
308
248
  ],
309
- initial_setting + leap_day_portion
249
+ @final_setting + leap_day_portion
310
250
  )
311
251
  )
312
252
  end