astronoby 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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