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
@@ -73,23 +73,23 @@ module Astronoby
73
73
  [8, 15.45, 16859.074]
74
74
  ].freeze
75
75
 
76
- def self.march_equinox(year)
77
- new(year, MARCH_EQUINOX).compute
76
+ def self.march_equinox(year, ephem)
77
+ new(year, MARCH_EQUINOX, ephem).time
78
78
  end
79
79
 
80
- def self.june_solstice(year)
81
- new(year, JUNE_SOLSTICE).compute
80
+ def self.june_solstice(year, ephem)
81
+ new(year, JUNE_SOLSTICE, ephem).time
82
82
  end
83
83
 
84
- def self.september_equinox(year)
85
- new(year, SEPTEMBER_EQUINOX).compute
84
+ def self.september_equinox(year, ephem)
85
+ new(year, SEPTEMBER_EQUINOX, ephem).time
86
86
  end
87
87
 
88
- def self.december_solstice(year)
89
- new(year, DECEMBER_SOLSTICE).compute
88
+ def self.december_solstice(year, ephem)
89
+ new(year, DECEMBER_SOLSTICE, ephem).time
90
90
  end
91
91
 
92
- def initialize(year, event)
92
+ def initialize(year, event, ephem)
93
93
  unless EVENTS.include?(event)
94
94
  raise UnsupportedEventError.new(
95
95
  "Expected a format between #{EVENTS.join(", ")}, got #{event}"
@@ -98,10 +98,19 @@ module Astronoby
98
98
 
99
99
  @event = event
100
100
  @year = (year.to_i - 2000) / 1000.0
101
+ uncorrected_time = compute
102
+ @instant = Instant.from_time(uncorrected_time)
103
+ @sun = Sun.new(ephem: ephem, instant: @instant)
101
104
  end
102
105
 
106
+ def time
107
+ Instant.from_terrestrial_time(@instant.tt + corrected).to_time.round
108
+ end
109
+
110
+ private
111
+
103
112
  def compute
104
- t = (julian_day - Epoch::J2000) / Constants::DAYS_PER_JULIAN_CENTURY
113
+ t = (julian_day - JulianDate::J2000) / Constants::DAYS_PER_JULIAN_CENTURY
105
114
  w = Angle.from_degrees(35999.373 * t) - Angle.from_degrees(2.47)
106
115
  delta = 1 +
107
116
  0.0334 * w.cos +
@@ -113,13 +122,10 @@ module Astronoby
113
122
 
114
123
  delta_days = 0.00001 * s / delta
115
124
  epoch = julian_day + delta_days
116
- epoch += correction(epoch)
117
125
 
118
- Epoch.to_utc(epoch).round
126
+ Instant.from_terrestrial_time(epoch).to_time
119
127
  end
120
128
 
121
- private
122
-
123
129
  def julian_day
124
130
  component = JDE_COMPONENTS[@event]
125
131
  component[0] +
@@ -129,11 +135,8 @@ module Astronoby
129
135
  component[4] * @year**4
130
136
  end
131
137
 
132
- def correction(epoch)
133
- time = Epoch.to_utc(epoch)
134
- sun = Sun.new(time: time)
135
- longitude = sun.apparent_ecliptic_coordinates.longitude
136
-
138
+ def corrected
139
+ longitude = @sun.apparent.ecliptic.longitude
137
140
  58 * Angle.from_degrees(@event * 90 - longitude.degrees).sin
138
141
  end
139
142
  end
@@ -4,4 +4,8 @@ module Astronoby
4
4
  class UnsupportedFormatError < ArgumentError; end
5
5
 
6
6
  class UnsupportedEventError < ArgumentError; end
7
+
8
+ class CalculationError < StandardError; end
9
+
10
+ class EphemerisError < StandardError; end
7
11
  end
@@ -15,16 +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
- ].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
28
31
  end
29
32
 
30
33
  # @param year [Integer] Requested year
@@ -42,13 +45,12 @@ module Astronoby
42
45
  def time
43
46
  correction = moon_phases_periodic_terms
44
47
  .public_send(:"#{@phase}_correction")
45
- terrestrial_time = Epoch.to_utc(
48
+ terrestrial_time = Instant.from_terrestrial_time(
46
49
  julian_ephemeris_day +
47
50
  correction +
48
51
  moon_phases_periodic_terms.additional_corrections
49
52
  )
50
- delta = Util::Time.terrestrial_universal_time_delta(terrestrial_time)
51
- (terrestrial_time - delta).round
53
+ terrestrial_time.to_time.round
52
54
  end
53
55
 
54
56
  private
@@ -0,0 +1,376 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class RiseTransitSetCalculator
5
+ class PotentialEvent
6
+ attr_reader :hour_angle, :can_occur
7
+
8
+ def initialize(hour_angle, can_occur)
9
+ @hour_angle = hour_angle
10
+ @can_occur = can_occur
11
+ end
12
+
13
+ def negated
14
+ self.class.new(-@hour_angle, @can_occur)
15
+ end
16
+ end
17
+
18
+ TAU = Math::PI * 2
19
+ SAMPLE_THRESHOLD = 0.8
20
+ REFINEMENT_ITERATIONS = 3
21
+ MIN_TIME_ADJUSTMENT = Constants::MICROSECOND_IN_DAYS
22
+ STANDARD_REFRACTION_ANGLE = -Angle.from_dms(0, 34, 0)
23
+ SUN_REFRACTION_ANGLE = -Angle.from_dms(0, 50, 0)
24
+ EVENT_TYPES = [:rising, :transit, :setting].freeze
25
+
26
+ def initialize(body:, observer:, ephem:)
27
+ @body = body
28
+ @observer = observer
29
+ @ephem = ephem
30
+ end
31
+
32
+ def event_on(date, utc_offset: 0)
33
+ events = events_on(date, utc_offset: utc_offset)
34
+ RiseTransitSetEvent.new(
35
+ events.rising_times.first,
36
+ events.transit_times.first,
37
+ events.setting_times.first
38
+ )
39
+ end
40
+
41
+ def events_on(date, utc_offset: 0)
42
+ start_time = Time.new(
43
+ date.year,
44
+ date.month,
45
+ date.day,
46
+ 0, 0, 0, utc_offset
47
+ )
48
+ end_time = Time.new(
49
+ date.year,
50
+ date.month,
51
+ date.day,
52
+ 23, 59, 59, utc_offset
53
+ )
54
+ events_between(start_time, end_time)
55
+ end
56
+
57
+ def events_between(start_time, end_time)
58
+ reset_state
59
+ @start_instant = Instant.from_time(start_time)
60
+ @end_instant = Instant.from_time(end_time)
61
+ events
62
+ end
63
+
64
+ private
65
+
66
+ def events
67
+ rising_events = calculate_initial_positions.map do |position|
68
+ calculate_rising_event(position)
69
+ end
70
+ setting_events = rising_events.map(&:negated)
71
+ transit_event = PotentialEvent.new(Angle.zero, true)
72
+
73
+ event_data = {
74
+ rising: rising_events,
75
+ transit: [transit_event],
76
+ setting: setting_events
77
+ }
78
+
79
+ results = EVENT_TYPES.each_with_object({}) do |event_type, results|
80
+ results[event_type] = calculate_event_times(
81
+ event_type,
82
+ event_data[event_type]
83
+ )
84
+ end
85
+
86
+ RiseTransitSetEvents.new(
87
+ results[:rising],
88
+ results[:transit],
89
+ results[:setting]
90
+ )
91
+ end
92
+
93
+ def calculate_rising_event(position)
94
+ declination = position.equatorial.declination
95
+ latitude = @observer.latitude
96
+ altitude = horizon_angle(position.distance)
97
+
98
+ # Calculate the unmodified ratio to check if the body rises/sets
99
+ numerator = altitude.sin - latitude.sin * declination.sin
100
+ denominator = latitude.cos * declination.cos
101
+ ratio = numerator / denominator
102
+
103
+ # Determine if the body can rise/set and calculate the hour angle
104
+ can_rise_set = ratio.abs <= 1.0
105
+ angle = can_rise_set ? -Angle.acos(ratio.clamp(-1.0, 1.0)) : Angle.zero
106
+
107
+ PotentialEvent.new(angle, can_rise_set)
108
+ end
109
+
110
+ def calculate_event_times(event_type, events)
111
+ desired_hour_angles = events.map(&:hour_angle)
112
+
113
+ # Calculate differences between current and desired hour angles
114
+ angle_differences = calculate_angle_differences(
115
+ calculate_initial_hour_angles,
116
+ desired_hour_angles
117
+ )
118
+
119
+ # Find intervals where the body crosses the desired hour angle
120
+ crossing_indexes = find_crossing_intervals(angle_differences)
121
+
122
+ # Skip if no relevant crossing points found
123
+ return [] if crossing_indexes.empty?
124
+
125
+ valid_crossings = if events.size == 1
126
+ # For transit (single event), all crossings are valid
127
+ crossing_indexes.map { true }
128
+ else
129
+ # For rise/set, check if each crossing corresponds to a valid event
130
+ crossing_indexes.map { |i| events[i].can_occur }
131
+ end
132
+
133
+ old_hour_angles = calculate_initial_hour_angles
134
+ .values_at(*crossing_indexes)
135
+ old_instants = sample_instants.values_at(*crossing_indexes)
136
+
137
+ # Initial estimate of event times using linear interpolation
138
+ new_instants = interpolate_initial_times(
139
+ crossing_indexes,
140
+ angle_differences
141
+ )
142
+
143
+ # Refine the estimates through iteration
144
+ refined_times = refine_time_estimates(
145
+ event_type,
146
+ new_instants,
147
+ old_hour_angles,
148
+ old_instants
149
+ )
150
+
151
+ # Filter out times for bodies that never rise/set
152
+ refined_times
153
+ .zip(valid_crossings)
154
+ .filter_map { |time, valid| time if valid }
155
+ end
156
+
157
+ def sample_count
158
+ @sample_count ||=
159
+ ((@end_instant.tt - @start_instant.tt) / SAMPLE_THRESHOLD).ceil + 1
160
+ end
161
+
162
+ def sample_instants
163
+ @sample_instants ||= Util::Maths.linspace(
164
+ @start_instant.tt,
165
+ @end_instant.tt,
166
+ sample_count
167
+ ).map { |tt| Instant.from_terrestrial_time(tt) }
168
+ end
169
+
170
+ def calculate_initial_positions
171
+ @initial_positions ||= calculate_positions_at_instants(sample_instants)
172
+ end
173
+
174
+ def calculate_initial_hour_angles
175
+ @initial_hour_angles ||=
176
+ calculate_hour_angles(calculate_initial_positions)
177
+ end
178
+
179
+ def calculate_angle_differences(hour_angles, angles)
180
+ hour_angles.each_with_index.map do |hour_angle, i|
181
+ angle = (angles.size == 1) ? angles[0] : angles[i]
182
+ Angle.from_radians((angle - hour_angle).radians % TAU)
183
+ end
184
+ end
185
+
186
+ def find_crossing_intervals(differences)
187
+ differences
188
+ .each_cons(2)
189
+ .map { |a, b| b - a }
190
+ .each_with_index
191
+ .filter_map { |diff, i| i if diff.radians > 0.0 }
192
+ end
193
+
194
+ def interpolate_initial_times(crossing_indexes, angle_differences)
195
+ crossing_indexes.map do |index|
196
+ a = angle_differences[index].radians
197
+ b = TAU - angle_differences[index + 1].radians
198
+ c = sample_instants[index].tt
199
+ d = sample_instants[index + 1].tt
200
+
201
+ # Linear interpolation formula
202
+ tt = (b * c + a * d) / (a + b)
203
+ Instant.from_terrestrial_time(tt)
204
+ end
205
+ end
206
+
207
+ def refine_time_estimates(
208
+ event_type,
209
+ new_instants,
210
+ old_hour_angles,
211
+ old_instants
212
+ )
213
+ REFINEMENT_ITERATIONS.times do |iteration|
214
+ # Calculate positions at current estimates
215
+ apparent_positions = calculate_positions_at_instants(new_instants)
216
+
217
+ # Calculate current hour angles
218
+ current_hour_angles = calculate_hour_angles(apparent_positions)
219
+
220
+ # Calculate desired hour angles based on current positions
221
+ desired_hour_angles = calculate_desired_hour_angles(
222
+ event_type,
223
+ apparent_positions
224
+ )
225
+
226
+ # Calculate hour angle adjustments
227
+ hour_angle_adjustments = calculate_hour_angle_adjustments(
228
+ current_hour_angles,
229
+ desired_hour_angles
230
+ )
231
+
232
+ # Calculate hour angle changes for rate determination
233
+ hour_angle_changes = calculate_hour_angle_changes(
234
+ current_hour_angles,
235
+ old_hour_angles,
236
+ iteration
237
+ )
238
+
239
+ # Calculate time differences
240
+ time_differences_in_days = new_instants.each_with_index.map do |instant, i|
241
+ instant.tt - old_instants[i].tt
242
+ end
243
+
244
+ # Calculate hour angle rate (radians per day)
245
+ hour_angle_rates = hour_angle_changes.each_with_index.map do |angle, i|
246
+ denominator = time_differences_in_days[i]
247
+ angle.radians / denominator
248
+ end
249
+
250
+ # Store current values for next iteration
251
+ old_hour_angles = current_hour_angles
252
+ old_instants = new_instants
253
+
254
+ # Calculate time adjustments
255
+ time_adjustments = hour_angle_adjustments
256
+ .each_with_index
257
+ .map do |angle, i|
258
+ ratio = angle.radians / hour_angle_rates[i]
259
+ time_adjustment = (ratio.nan? || ratio.infinite?) ? 0 : ratio
260
+ [time_adjustment, MIN_TIME_ADJUSTMENT].max
261
+ end
262
+
263
+ # Apply time adjustments
264
+ new_instants = new_instants.each_with_index.map do |instant, i|
265
+ Instant.from_terrestrial_time(instant.tt + time_adjustments[i])
266
+ end
267
+ end
268
+
269
+ new_instants.map { _1.to_time.round }
270
+ end
271
+
272
+ def calculate_positions_at_instants(instants)
273
+ if Astronoby.configuration.cache_enabled?
274
+ instants.map do |instant|
275
+ cache_key = CacheKey.generate(
276
+ :observed_by,
277
+ instant,
278
+ @body.to_s,
279
+ @observer.hash
280
+ )
281
+ Astronoby.cache.fetch(cache_key) do
282
+ @body
283
+ .new(instant: instant, ephem: @ephem)
284
+ .observed_by(@observer)
285
+ end
286
+ end
287
+ else
288
+ @positions_cache ||= {}
289
+ precision = Astronoby.configuration.cache_precision(:observed_by)
290
+ instants.map do |instant|
291
+ rounded_instant = Instant.from_terrestrial_time(
292
+ instant.tt.round(precision)
293
+ )
294
+ @positions_cache[rounded_instant] ||= @body
295
+ .new(instant: rounded_instant, ephem: @ephem)
296
+ .observed_by(@observer)
297
+ end
298
+ end
299
+ end
300
+
301
+ def calculate_hour_angles(positions)
302
+ positions.map do |position|
303
+ position.equatorial.compute_hour_angle(
304
+ time: position.instant.to_time,
305
+ longitude: @observer.longitude
306
+ )
307
+ end
308
+ end
309
+
310
+ def calculate_desired_hour_angles(event_type, positions)
311
+ positions.map do |position|
312
+ if event_type == :transit
313
+ Angle.zero
314
+ else
315
+ declination = position.equatorial.declination
316
+ ha = rising_hour_angle(
317
+ @observer.latitude,
318
+ declination,
319
+ horizon_angle(position.distance)
320
+ )
321
+ (event_type == :rising) ? ha : -ha
322
+ end
323
+ end
324
+ end
325
+
326
+ def calculate_hour_angle_adjustments(current_angles, angles)
327
+ current_angles.each_with_index.map do |angle, i|
328
+ radians = ((angles[i] - angle).radians + Math::PI) % TAU - Math::PI
329
+ Angle.from_radians(radians)
330
+ end
331
+ end
332
+
333
+ def calculate_hour_angle_changes(current_angles, old_angles, iteration)
334
+ current_angles.each_with_index.map do |angle, i|
335
+ radians = angle.radians - old_angles[i].radians
336
+
337
+ if iteration == 0
338
+ radians %= TAU
339
+ else
340
+ radians = (radians + Math::PI) % TAU - Math::PI
341
+ end
342
+
343
+ Angle.from_radians(radians)
344
+ end
345
+ end
346
+
347
+ def rising_hour_angle(latitude, declination, altitude)
348
+ numerator = altitude.sin - latitude.sin * declination.sin
349
+ denominator = latitude.cos * declination.cos
350
+ ratio = (numerator / denominator).clamp(-1.0, 1.0)
351
+ -Angle.acos(ratio)
352
+ end
353
+
354
+ def horizon_angle(distance)
355
+ case @body.name
356
+ when "Astronoby::Sun"
357
+ SUN_REFRACTION_ANGLE
358
+ when "Astronoby::Moon"
359
+ STANDARD_REFRACTION_ANGLE -
360
+ Angle.from_radians(Moon::EQUATORIAL_RADIUS.m / distance.m)
361
+ else
362
+ STANDARD_REFRACTION_ANGLE
363
+ end
364
+ end
365
+
366
+ def reset_state
367
+ @initial_hour_angles = nil
368
+ @initial_positions = nil
369
+ @sample_count = nil
370
+ @sample_instants = nil
371
+ @start_instant = nil
372
+ @end_instant = nil
373
+ @positions_cache = nil
374
+ end
375
+ end
376
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class RiseTransitSetEvent
5
+ attr_reader :rising_time, :transit_time, :setting_time
6
+
7
+ def initialize(rising, transit, setting)
8
+ @rising_time = rising
9
+ @transit_time = transit
10
+ @setting_time = setting
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class RiseTransitSetEvents
5
+ attr_reader :rising_times, :transit_times, :setting_times
6
+
7
+ def initialize(risings, transits, settings)
8
+ @rising_times = risings
9
+ @transit_times = transits
10
+ @setting_times = settings
11
+ end
12
+ end
13
+ end