astronoby 0.9.0 → 0.11.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +235 -0
  4. data/README.md +19 -1
  5. data/UPGRADING.md +270 -0
  6. data/docs/README.md +105 -16
  7. data/docs/altitude_crossings.md +92 -0
  8. data/docs/angles.md +1 -0
  9. data/docs/configuration.md +28 -17
  10. data/docs/coordinates.md +72 -12
  11. data/docs/deep_sky_bodies.md +46 -1
  12. data/docs/ephem.md +5 -2
  13. data/docs/equinoxes_solstices_times.md +4 -3
  14. data/docs/glossary.md +99 -1
  15. data/docs/iers.md +40 -0
  16. data/docs/instant.md +33 -21
  17. data/docs/lunar_eclipses.md +208 -0
  18. data/docs/lunar_observation.md +87 -0
  19. data/docs/moon_phases.md +4 -1
  20. data/docs/observer.md +20 -6
  21. data/docs/planetary_phenomena.md +78 -0
  22. data/docs/reference_frames.md +192 -34
  23. data/docs/rise_transit_set_times.md +6 -4
  24. data/docs/solar_system_bodies.md +26 -4
  25. data/docs/twilight_times.md +43 -24
  26. data/lib/astronoby/angle.rb +63 -2
  27. data/lib/astronoby/angles/dms.rb +18 -1
  28. data/lib/astronoby/angles/hms.rb +14 -1
  29. data/lib/astronoby/angular_velocity.rb +21 -0
  30. data/lib/astronoby/bodies/deep_sky_object.rb +6 -1
  31. data/lib/astronoby/bodies/deep_sky_object_position.rb +35 -38
  32. data/lib/astronoby/bodies/earth.rb +7 -44
  33. data/lib/astronoby/bodies/jupiter.rb +10 -0
  34. data/lib/astronoby/bodies/mars.rb +10 -0
  35. data/lib/astronoby/bodies/mercury.rb +10 -0
  36. data/lib/astronoby/bodies/moon.rb +158 -32
  37. data/lib/astronoby/bodies/neptune.rb +10 -0
  38. data/lib/astronoby/bodies/saturn.rb +10 -0
  39. data/lib/astronoby/bodies/solar_system_body.rb +240 -61
  40. data/lib/astronoby/bodies/sun.rb +79 -4
  41. data/lib/astronoby/bodies/uranus.rb +10 -0
  42. data/lib/astronoby/bodies/venus.rb +10 -0
  43. data/lib/astronoby/body.rb +6 -0
  44. data/lib/astronoby/cache.rb +13 -0
  45. data/lib/astronoby/center.rb +84 -0
  46. data/lib/astronoby/constants.rb +1 -2
  47. data/lib/astronoby/constellation.rb +9 -1
  48. data/lib/astronoby/constellations/data.rb +1 -1
  49. data/lib/astronoby/coordinates/ecliptic.rb +10 -1
  50. data/lib/astronoby/coordinates/equatorial.rb +79 -13
  51. data/lib/astronoby/coordinates/geodetic.rb +102 -0
  52. data/lib/astronoby/coordinates/horizontal.rb +13 -1
  53. data/lib/astronoby/delta_t.rb +44 -0
  54. data/lib/astronoby/distance.rb +35 -0
  55. data/lib/astronoby/duration.rb +116 -0
  56. data/lib/astronoby/earth_rotation.rb +70 -0
  57. data/lib/astronoby/equinox_solstice.rb +31 -8
  58. data/lib/astronoby/errors.rb +11 -0
  59. data/lib/astronoby/events/altitude_crossing.rb +29 -0
  60. data/lib/astronoby/events/altitude_crossing_calculator.rb +511 -0
  61. data/lib/astronoby/events/altitude_extreme.rb +31 -0
  62. data/lib/astronoby/events/conjunction.rb +51 -0
  63. data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
  64. data/lib/astronoby/events/extremum_calculator.rb +23 -176
  65. data/lib/astronoby/events/greatest_elongation.rb +58 -0
  66. data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
  67. data/lib/astronoby/events/lunar_eclipse.rb +111 -0
  68. data/lib/astronoby/events/lunar_eclipse_calculator.rb +397 -0
  69. data/lib/astronoby/events/lunar_eclipse_geometry.rb +115 -0
  70. data/lib/astronoby/events/lunar_eclipse_phase.rb +45 -0
  71. data/lib/astronoby/events/lunar_eclipse_visibility.rb +296 -0
  72. data/lib/astronoby/events/opposition.rb +19 -0
  73. data/lib/astronoby/events/rise_transit_set_calculator.rb +9 -13
  74. data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
  75. data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
  76. data/lib/astronoby/events/twilight_calculator.rb +37 -147
  77. data/lib/astronoby/events/twilight_event.rb +24 -6
  78. data/lib/astronoby/events/twilight_events.rb +26 -6
  79. data/lib/astronoby/extremum_finder.rb +148 -0
  80. data/lib/astronoby/horizon.rb +40 -0
  81. data/lib/astronoby/horizontal_transformation.rb +112 -0
  82. data/lib/astronoby/instant.rb +56 -16
  83. data/lib/astronoby/libration.rb +25 -0
  84. data/lib/astronoby/mean_obliquity.rb +8 -0
  85. data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
  86. data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
  87. data/lib/astronoby/nutation.rb +10 -20
  88. data/lib/astronoby/observer.rb +67 -49
  89. data/lib/astronoby/orientation.rb +107 -0
  90. data/lib/astronoby/position.rb +16 -0
  91. data/lib/astronoby/precession.rb +62 -61
  92. data/lib/astronoby/reference_frame.rb +80 -9
  93. data/lib/astronoby/reference_frames/apparent.rb +43 -12
  94. data/lib/astronoby/reference_frames/astrometric.rb +14 -1
  95. data/lib/astronoby/reference_frames/geometric.rb +7 -1
  96. data/lib/astronoby/reference_frames/mean_of_date.rb +19 -1
  97. data/lib/astronoby/reference_frames/teme.rb +159 -0
  98. data/lib/astronoby/reference_frames/topocentric.rb +36 -4
  99. data/lib/astronoby/refraction.rb +26 -5
  100. data/lib/astronoby/root_finder.rb +90 -0
  101. data/lib/astronoby/rotation.rb +49 -0
  102. data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +19 -8
  103. data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +44 -5
  104. data/lib/astronoby/time/greenwich_sidereal_time.rb +21 -0
  105. data/lib/astronoby/time/local_apparent_sidereal_time.rb +21 -0
  106. data/lib/astronoby/time/local_mean_sidereal_time.rb +21 -0
  107. data/lib/astronoby/time/local_sidereal_time.rb +24 -0
  108. data/lib/astronoby/time/sidereal_time.rb +23 -1
  109. data/lib/astronoby/true_obliquity.rb +4 -0
  110. data/lib/astronoby/util/maths.rb +8 -0
  111. data/lib/astronoby/util/time.rb +0 -497
  112. data/lib/astronoby/vector.rb +10 -0
  113. data/lib/astronoby/velocity.rb +39 -0
  114. data/lib/astronoby/version.rb +1 -1
  115. data/lib/astronoby.rb +30 -0
  116. metadata +80 -5
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Base class for solar system bodies. Provides the reference frame chain
5
+ # (geometric -> astrometric -> mean-of-date -> apparent -> topocentric)
6
+ # and common observational properties (phase angle, magnitude, etc.).
4
7
  class SolarSystemBody
8
+ include Position
9
+ extend Body
10
+
5
11
  SOLAR_SYSTEM_BARYCENTER = 0
6
12
  SUN = 10
7
13
  MERCURY_BARYCENTER = 1
@@ -17,16 +23,37 @@ module Astronoby
17
23
  URANUS_BARYCENTER = 7
18
24
  NEPTUNE_BARYCENTER = 8
19
25
 
20
- attr_reader :geometric, :instant
26
+ # @return [Astronoby::Instant] the time instant
27
+ attr_reader :instant
28
+
29
+ # @return [::Ephem::SPK] the ephemeris data source
30
+ attr_reader :ephem
21
31
 
22
- def self.at(instant, ephem:)
23
- new(ephem: ephem, instant: instant)
32
+ # @return [Astronoby::Orientation, nil] the orientation kernel, if provided
33
+ attr_reader :orientation
34
+
35
+ # Creates a new body instance at the given instant.
36
+ #
37
+ # @param instant [Astronoby::Instant] the time instant
38
+ # @param ephem [::Ephem::SPK] ephemeris data source
39
+ # @param orientation [Astronoby::Orientation, nil] orientation kernel
40
+ # @return [Astronoby::SolarSystemBody] a new body instance
41
+ def self.at(instant, ephem:, orientation: nil)
42
+ new(ephem: ephem, instant: instant, orientation: orientation)
24
43
  end
25
44
 
45
+ # Computes the geometric reference frame for this body.
46
+ #
47
+ # @param ephem [::Ephem::SPK] ephemeris data source
48
+ # @param instant [Astronoby::Instant] the time instant
49
+ # @return [Astronoby::Geometric] the geometric frame
26
50
  def self.geometric(ephem:, instant:)
27
51
  compute_geometric(ephem: ephem, instant: instant)
28
52
  end
29
53
 
54
+ # @param ephem [::Ephem::SPK] ephemeris data source
55
+ # @param instant [Astronoby::Instant] the time instant
56
+ # @return [Astronoby::Geometric] the geometric frame
30
57
  def self.compute_geometric(ephem:, instant:)
31
58
  segments = ephemeris_segments(ephem.type)
32
59
  segment1 = segments[0]
@@ -66,63 +93,196 @@ module Astronoby
66
93
  end
67
94
  end
68
95
 
96
+ # @param _ephem_source [Symbol] the ephemeris source type
97
+ # @return [Array<Array>] ephemeris segment identifiers
98
+ # @raise [NotImplementedError] must be implemented by subclasses
69
99
  def self.ephemeris_segments(_ephem_source)
70
100
  raise NotImplementedError
71
101
  end
72
102
 
103
+ # @return [Float, nil] absolute magnitude of the body
73
104
  def self.absolute_magnitude
74
105
  nil
75
106
  end
76
107
 
77
- def initialize(ephem:, instant:)
108
+ # @return [Boolean] true for an inferior planet (Mercury, Venus)
109
+ def self.inferior_planet?
110
+ false
111
+ end
112
+
113
+ # @return [Boolean] true for a superior planet (Mars through Neptune)
114
+ def self.superior_planet?
115
+ false
116
+ end
117
+
118
+ # @return [Boolean] true for a planet (excludes the Sun, Earth and Moon)
119
+ def self.planet?
120
+ inferior_planet? || superior_planet?
121
+ end
122
+
123
+ # @param observer [Astronoby::Observer] Observer for whom to calculate rise,
124
+ # transit, and set events
125
+ # @param ephem [::Ephem::SPK] Ephemeris data source
126
+ # @param date [Date] Date for which to calculate rise, transit, and set
127
+ # events (optional)
128
+ # @param start_time [Time] Start time for rise, transit, and set event
129
+ # calculation (optional)
130
+ # @param end_time [Time] End time for rise, transit, and set event
131
+ # calculation (optional)
132
+ # @param utc_offset [String] UTC offset for the given date (e.g., "+02:00")
133
+ # @return [Astronoby::RiseTransitSetEvent,
134
+ # Array<Astronoby::RiseTransitSetEvent>] Rise, transit, and set events for
135
+ # the given date or time range.
136
+ def self.rise_transit_set_events(
137
+ observer:,
138
+ ephem:,
139
+ date: nil,
140
+ start_time: nil,
141
+ end_time: nil,
142
+ utc_offset: 0
143
+ )
144
+ calculator = RiseTransitSetCalculator.new(
145
+ body: self,
146
+ observer: observer,
147
+ ephem: ephem
148
+ )
149
+ if date
150
+ calculator.events_on(date, utc_offset: utc_offset)
151
+ else
152
+ calculator.events_between(start_time, end_time)
153
+ end
154
+ end
155
+
156
+ # @param ephem [::Ephem::SPK] ephemeris data source
157
+ # @param start_time [Time] start time
158
+ # @param end_time [Time] end time
159
+ # @param samples_per_period [Integer] number of samples per synodic period
160
+ # @return [Array<Astronoby::Conjunction>] conjunctions with the Sun
161
+ # @raise [Astronoby::UnsupportedEventError] unless the body is a planet
162
+ def self.conjunction_events(
163
+ ephem:,
164
+ start_time:,
165
+ end_time:,
166
+ samples_per_period: 60
167
+ )
168
+ unless planet?
169
+ raise UnsupportedEventError, "#{self} has no conjunctions with the Sun"
170
+ end
171
+
172
+ ConjunctionOppositionCalculator.new(
173
+ body: self,
174
+ ephem: ephem,
175
+ samples_per_period: samples_per_period
176
+ ).conjunction_events_between(start_time, end_time)
177
+ end
178
+
179
+ # @param ephem [::Ephem::SPK] ephemeris data source
180
+ # @param start_time [Time] start time
181
+ # @param end_time [Time] end time
182
+ # @param samples_per_period [Integer] number of samples per synodic period
183
+ # @return [Array<Astronoby::Opposition>] oppositions with the Sun
184
+ # @raise [Astronoby::UnsupportedEventError] unless the body is a superior
185
+ # planet
186
+ def self.opposition_events(
187
+ ephem:,
188
+ start_time:,
189
+ end_time:,
190
+ samples_per_period: 60
191
+ )
192
+ unless superior_planet?
193
+ raise UnsupportedEventError, "#{self} has no oppositions with the Sun"
194
+ end
195
+
196
+ ConjunctionOppositionCalculator.new(
197
+ body: self,
198
+ ephem: ephem,
199
+ samples_per_period: samples_per_period
200
+ ).opposition_events_between(start_time, end_time)
201
+ end
202
+
203
+ # @param ephem [::Ephem::SPK] ephemeris data source
204
+ # @param start_time [Time] start time
205
+ # @param end_time [Time] end time
206
+ # @param samples_per_period [Integer] number of samples per synodic period
207
+ # @return [Array<Astronoby::GreatestElongation>] greatest elongations
208
+ # @raise [Astronoby::UnsupportedEventError] unless the body is an inferior
209
+ # planet
210
+ def self.greatest_elongation_events(
211
+ ephem:,
212
+ start_time:,
213
+ end_time:,
214
+ samples_per_period: 60
215
+ )
216
+ unless inferior_planet?
217
+ raise UnsupportedEventError,
218
+ "#{self} has no greatest elongations from the Sun"
219
+ end
220
+
221
+ GreatestElongationCalculator.new(
222
+ body: self,
223
+ ephem: ephem,
224
+ samples_per_period: samples_per_period
225
+ ).greatest_elongation_events_between(start_time, end_time)
226
+ end
227
+
228
+ # @param ephem [::Ephem::SPK] Ephemeris data source
229
+ # @param instant [Astronoby::Instant] Instant for which to calculate the
230
+ # phase angle
231
+ # @param orientation [Astronoby::Orientation, nil] Orientation kernel,
232
+ # enabling arcsecond-accurate lunar libration and axis position angle
233
+ def initialize(ephem:, instant:, orientation: nil)
234
+ @ephem = ephem
78
235
  @instant = instant
79
- @geometric = compute_geometric(ephem)
80
- @earth_geometric = Earth.geometric(ephem: ephem, instant: instant)
81
- @light_time_corrected_position,
82
- @light_time_corrected_velocity =
83
- Correction::LightTimeDelay.compute(
84
- center: @earth_geometric,
85
- target: @geometric,
86
- ephem: ephem
87
- )
88
- compute_sun(ephem) if requires_sun_data?
236
+ @orientation = orientation
237
+ end
238
+
239
+ # @return [Astronoby::Geometric] the geometric reference frame (BCRS)
240
+ def geometric
241
+ @geometric ||= self.class.compute_geometric(
242
+ ephem: @ephem,
243
+ instant: @instant
244
+ )
245
+ end
246
+
247
+ # @return [Astronoby::Geometric] Earth's geometric reference frame
248
+ def earth_geometric
249
+ @earth_geometric ||= Earth.geometric(ephem: @ephem, instant: @instant)
89
250
  end
90
251
 
252
+ # @return [Astronoby::Astrometric] the astrometric reference frame (GCRS)
91
253
  def astrometric
92
254
  @astrometric ||= Astrometric.build_from_geometric(
93
255
  instant: @instant,
94
- earth_geometric: @earth_geometric,
95
- light_time_corrected_position: @light_time_corrected_position,
96
- light_time_corrected_velocity: @light_time_corrected_velocity,
97
- target_body: self
256
+ earth_geometric: earth_geometric,
257
+ light_time_corrected_position: light_time_corrected_position,
258
+ light_time_corrected_velocity: light_time_corrected_velocity,
259
+ target_body: body
98
260
  )
99
261
  end
100
262
 
263
+ # @return [Astronoby::MeanOfDate] the mean-of-date reference frame
101
264
  def mean_of_date
102
265
  @mean_of_date ||= MeanOfDate.build_from_geometric(
103
266
  instant: @instant,
104
- target_geometric: @geometric,
105
- earth_geometric: @earth_geometric,
106
- target_body: self
267
+ target_geometric: geometric,
268
+ earth_geometric: earth_geometric,
269
+ target_body: body
107
270
  )
108
271
  end
109
272
 
273
+ # @return [Astronoby::Apparent] the apparent reference frame
110
274
  def apparent
111
275
  @apparent ||= Apparent.build_from_astrometric(
112
276
  instant: @instant,
113
277
  target_astrometric: astrometric,
114
- earth_geometric: @earth_geometric,
115
- target_body: self
278
+ earth_geometric: earth_geometric,
279
+ target_body: body
116
280
  )
117
281
  end
118
282
 
119
- def observed_by(observer)
120
- Topocentric.build_from_apparent(
121
- apparent: apparent,
122
- observer: observer,
123
- instant: @instant,
124
- target_body: self
125
- )
283
+ # @return [Astronoby::Body] the body definition (the class itself)
284
+ def body
285
+ self.class
126
286
  end
127
287
 
128
288
  # Returns the constellation of the body
@@ -136,6 +296,25 @@ module Astronoby
136
296
  )
137
297
  end
138
298
 
299
+ # Apparent geocentric Sun-Earth-body angle
300
+ # @return [Astronoby::Angle, nil] Elongation of the body
301
+ def elongation
302
+ return unless sun
303
+
304
+ @elongation ||= sun.apparent.separation_from(apparent)
305
+ end
306
+
307
+ # @return [Boolean] true when the body is east of the Sun
308
+ def eastern?
309
+ (apparent.ecliptic.longitude - sun.apparent.ecliptic.longitude)
310
+ .sin.positive?
311
+ end
312
+
313
+ # @return [Boolean] true when the body is west of the Sun
314
+ def western?
315
+ !eastern?
316
+ end
317
+
139
318
  # Source:
140
319
  # Title: Astronomical Algorithms
141
320
  # Author: Jean Meeus
@@ -143,23 +322,12 @@ module Astronoby
143
322
  # Chapter: 48 - Illuminated Fraction of the Moon's Disk
144
323
  # @return [Astronoby::Angle, nil] Phase angle of the body
145
324
  def phase_angle
146
- return unless @sun
325
+ return unless sun
147
326
 
148
327
  @phase_angle ||= begin
149
- geocentric_elongation = Angle.acos(
150
- @sun.apparent.equatorial.declination.sin *
151
- apparent.equatorial.declination.sin +
152
- @sun.apparent.equatorial.declination.cos *
153
- apparent.equatorial.declination.cos *
154
- (
155
- @sun.apparent.equatorial.right_ascension -
156
- apparent.equatorial.right_ascension
157
- ).cos
158
- )
159
-
160
- term1 = @sun.astrometric.distance.km * geocentric_elongation.sin
328
+ term1 = sun.astrometric.distance.km * elongation.sin
161
329
  term2 = astrometric.distance.km -
162
- @sun.astrometric.distance.km * geocentric_elongation.cos
330
+ sun.astrometric.distance.km * elongation.cos
163
331
  angle = Angle.atan(term1 / term2)
164
332
  Astronoby::Util::Trigonometry
165
333
  .adjustement_for_arctangent(term1, term2, angle)
@@ -186,7 +354,7 @@ module Astronoby
186
354
 
187
355
  @apparent_magnitude ||= begin
188
356
  body_sun_distance =
189
- (astrometric.position - @sun.astrometric.position).magnitude
357
+ (astrometric.position - sun.astrometric.position).magnitude
190
358
  self.class.absolute_magnitude +
191
359
  5 * Math.log10(body_sun_distance.au * astrometric.distance.au) +
192
360
  magnitude_correction_term
@@ -206,13 +374,13 @@ module Astronoby
206
374
  end
207
375
  end
208
376
 
209
- # @return [Boolean] True if the body is approaching the primary
210
- # body (Sun), false otherwise.
377
+ # @return [Boolean] True if the body is approaching its primary
378
+ # body, false otherwise.
211
379
  def approaching_primary?
212
380
  relative_position =
213
- (geometric.position - @sun.geometric.position).map(&:m)
381
+ (geometric.position - primary_body_geometric.position).map(&:m)
214
382
  relative_velocity =
215
- (geometric.velocity - @sun.geometric.velocity).map(&:mps)
383
+ (geometric.velocity - primary_body_geometric.velocity).map(&:mps)
216
384
  radial_velocity_component = Astronoby::Util::Maths
217
385
  .dot_product(relative_position, relative_velocity)
218
386
  distance = Math.sqrt(
@@ -221,28 +389,39 @@ module Astronoby
221
389
  radial_velocity_component / distance < 0
222
390
  end
223
391
 
224
- # @return [Boolean] True if the body is receding from the primary
225
- # body (Sun), false otherwise.
392
+ # @return [Boolean] True if the body is receding from its primary
393
+ # body, false otherwise.
226
394
  def receding_from_primary?
227
395
  !approaching_primary?
228
396
  end
229
397
 
230
398
  private
231
399
 
232
- # By default, Solar System bodies expose attributes that are dependent on
233
- # the Sun's position, such as phase angle and illuminated fraction.
234
- # If a body does not require Sun data, it should override this method to
235
- # return false.
236
- def requires_sun_data?
237
- true
400
+ def sun
401
+ @sun ||= Sun.new(instant: @instant, ephem: @ephem)
238
402
  end
239
403
 
240
- def compute_geometric(ephem)
241
- self.class.compute_geometric(ephem: ephem, instant: @instant)
404
+ def primary_body_geometric
405
+ sun.geometric
242
406
  end
243
407
 
244
- def compute_sun(ephem)
245
- @sun ||= Sun.new(instant: @instant, ephem: ephem)
408
+ def light_time_corrected_position
409
+ compute_light_time_correction unless @light_time_corrected_position
410
+ @light_time_corrected_position
411
+ end
412
+
413
+ def light_time_corrected_velocity
414
+ compute_light_time_correction unless @light_time_corrected_velocity
415
+ @light_time_corrected_velocity
416
+ end
417
+
418
+ def compute_light_time_correction
419
+ @light_time_corrected_position, @light_time_corrected_velocity =
420
+ Correction::LightTimeDelay.compute(
421
+ center: earth_geometric,
422
+ target: geometric,
423
+ ephem: @ephem
424
+ )
246
425
  end
247
426
 
248
427
  # Source:
@@ -1,18 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents the Sun. Provides twilight events, equinox/solstice
5
+ # calculations, and equation of time.
4
6
  class Sun < SolarSystemBody
5
7
  EQUATORIAL_RADIUS = Distance.from_meters(695_700_000)
6
8
  ABSOLUTE_MAGNITUDE = -26.74
7
9
 
10
+ # @param _ephem_source [Symbol] the ephemeris source type
11
+ # @return [Array<Array>] ephemeris segment identifiers
8
12
  def self.ephemeris_segments(_ephem_source)
9
13
  [[SOLAR_SYSTEM_BARYCENTER, SUN]]
10
14
  end
11
15
 
16
+ # @return [Float] absolute magnitude
12
17
  def self.absolute_magnitude
13
18
  ABSOLUTE_MAGNITUDE
14
19
  end
15
20
 
21
+ # @param observer [Astronoby::Observer] Observer for whom to calculate
22
+ # twilight events
23
+ # @param ephem [::Ephem::SPK] Ephemeris data source
24
+ # @param date [Date] Date for which to calculate twilight events (optional)
25
+ # @param start_time [Time] Start time for twilight event calculation
26
+ # (optional)
27
+ # @param end_time [Time] End time for twilight event calculation (optional)
28
+ # @param utc_offset [String] UTC offset for the given date (e.g., "+02:00")
29
+ # @return [Astronoby::TwilightEvent, Array<Astronoby::TwilightEvent>]
30
+ # Twilight events for the given date or time range.
31
+ def self.twilight_events(
32
+ observer:,
33
+ ephem:,
34
+ date: nil,
35
+ start_time: nil,
36
+ end_time: nil,
37
+ utc_offset: 0
38
+ )
39
+ calculator = TwilightCalculator.new(observer: observer, ephem: ephem)
40
+ if date
41
+ calculator.event_on(date, utc_offset: utc_offset)
42
+ else
43
+ calculator.events_between(start_time, end_time)
44
+ end
45
+ end
46
+
47
+ # @param year [Integer] Year for which to calculate equinoxes and solstices
48
+ # @param ephem [::Ephem::SPK] Ephemeris data source
49
+ # @return [Time] Time of the March equinox for the given year.
50
+ def self.march_equinox(year, ephem:)
51
+ EquinoxSolstice.march_equinox(year, ephem)
52
+ end
53
+
54
+ # @param year [Integer] Year for which to calculate equinoxes and solstices
55
+ # @param ephem [::Ephem::SPK] Ephemeris data source
56
+ # @return [Time] Time of the June solstice for the given year.
57
+ def self.june_solstice(year, ephem:)
58
+ EquinoxSolstice.june_solstice(year, ephem)
59
+ end
60
+
61
+ # @param year [Integer] Year for which to calculate equinoxes and solstices
62
+ # @param ephem [::Ephem::SPK] Ephemeris data source
63
+ # @return [Time] Time of the September equinox for the given year.
64
+ def self.september_equinox(year, ephem:)
65
+ EquinoxSolstice.september_equinox(year, ephem)
66
+ end
67
+
68
+ # @param year [Integer] Year for which to calculate equinoxes and solstices
69
+ # @param ephem [::Ephem::SPK] Ephemeris data source
70
+ # @return [Time] Time of the December solstice for the given year.
71
+ def self.december_solstice(year, ephem:)
72
+ EquinoxSolstice.december_solstice(year, ephem)
73
+ end
74
+
16
75
  # Source:
17
76
  # Title: Explanatory Supplement to the Astronomical Almanac
18
77
  # Authors: Sean E. Urban and P. Kenneth Seidelmann
@@ -31,7 +90,7 @@ module Astronoby
31
90
  # Edition: 2nd edition
32
91
  # Chapter: 28 - Equation of Time
33
92
 
34
- # @return [Integer] Equation of time in seconds
93
+ # @return [Astronoby::Duration] Equation of time
35
94
  def equation_of_time
36
95
  right_ascension = apparent.equatorial.right_ascension
37
96
  t = (@instant.julian_date - JulianDate::J2000) / Constants::DAYS_PER_JULIAN_MILLENIA
@@ -44,7 +103,7 @@ module Astronoby
44
103
  nutation = Nutation.new(instant: instant).nutation_in_longitude
45
104
  obliquity = TrueObliquity.at(@instant)
46
105
 
47
- (
106
+ seconds = (
48
107
  Angle
49
108
  .from_degrees(
50
109
  l0 -
@@ -53,12 +112,28 @@ module Astronoby
53
112
  nutation.degrees * obliquity.cos
54
113
  ).hours * Constants::SECONDS_PER_HOUR
55
114
  ).round
115
+ Duration.from_seconds(seconds)
56
116
  end
57
117
 
58
- private
118
+ # @return [nil] the Sun has no phase angle as seen from Earth
119
+ def phase_angle
120
+ nil
121
+ end
122
+
123
+ # @return [Boolean] always false; the Sun has no primary body
124
+ def approaching_primary?
125
+ false
126
+ end
59
127
 
60
- def requires_sun_data?
128
+ # @return [Boolean] always false; the Sun has no primary body
129
+ def receding_from_primary?
61
130
  false
62
131
  end
132
+
133
+ private
134
+
135
+ def primary_body_geometric
136
+ nil
137
+ end
63
138
  end
64
139
  end
@@ -1,14 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents Uranus.
4
5
  class Uranus < SolarSystemBody
5
6
  EQUATORIAL_RADIUS = Distance.from_meters(25_559_000)
6
7
  ABSOLUTE_MAGNITUDE = -7.11
8
+ ORBITAL_PERIOD = 30688.5
7
9
 
10
+ # @return [Boolean] true; Uranus is a superior planet
11
+ def self.superior_planet?
12
+ true
13
+ end
14
+
15
+ # @param _ephem_source [Symbol] the ephemeris source type
16
+ # @return [Array<Array>] ephemeris segment identifiers
8
17
  def self.ephemeris_segments(_ephem_source)
9
18
  [[SOLAR_SYSTEM_BARYCENTER, URANUS_BARYCENTER]]
10
19
  end
11
20
 
21
+ # @return [Float] absolute magnitude
12
22
  def self.absolute_magnitude
13
23
  ABSOLUTE_MAGNITUDE
14
24
  end
@@ -1,14 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents Venus.
4
5
  class Venus < SolarSystemBody
5
6
  EQUATORIAL_RADIUS = Distance.from_meters(6_051_800)
6
7
  ABSOLUTE_MAGNITUDE = -4.384
8
+ ORBITAL_PERIOD = 224.701
7
9
 
10
+ # @return [Boolean] true; Venus is an inferior planet
11
+ def self.inferior_planet?
12
+ true
13
+ end
14
+
15
+ # @param _ephem_source [Symbol] the ephemeris source type
16
+ # @return [Array<Array>] ephemeris segment identifiers
8
17
  def self.ephemeris_segments(_ephem_source)
9
18
  [[SOLAR_SYSTEM_BARYCENTER, VENUS_BARYCENTER]]
10
19
  end
11
20
 
21
+ # @return [Float] absolute magnitude
12
22
  def self.absolute_magnitude
13
23
  ABSOLUTE_MAGNITUDE
14
24
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Body
5
+ end
6
+ end
@@ -175,6 +175,19 @@ module Astronoby
175
175
  [type, rounded_tt, *components]
176
176
  end
177
177
 
178
+ # Generate a cache key for a UTC time, without rounding. Sidereal time
179
+ # moves by about 15 arcseconds per second, so unlike nutation it can't
180
+ # share an entry with a nearby instant.
181
+ # @param type [Symbol] The calculation type (e.g., :gmst)
182
+ # @param utc [Time] The UTC time
183
+ # @param components [Array] Additional components for the key
184
+ # @return [Array, nil] The complete cache key, nil if cache is disabled
185
+ def generate_for_utc(type, utc, *components)
186
+ return nil unless Astronoby.configuration.cache_enabled?
187
+
188
+ [type, utc.to_r, *components]
189
+ end
190
+
178
191
  # Round terrestrial time to specified precision
179
192
  # @param tt [Numeric] Terrestrial time value
180
193
  # @param precision [Numeric] Precision in seconds
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class Center
5
+ BARYCENTRIC = :barycentric
6
+ GEOCENTRIC = :geocentric
7
+ TOPOCENTRIC = :topocentric
8
+
9
+ class << self
10
+ # @return [Astronoby::Center] the Solar System barycenter
11
+ def barycentric
12
+ @barycentric ||= new(kind: BARYCENTRIC)
13
+ end
14
+
15
+ # @return [Astronoby::Center] the Earth's center
16
+ def geocentric
17
+ @geocentric ||= new(kind: GEOCENTRIC)
18
+ end
19
+
20
+ # @param observer [Astronoby::Observer] the observer
21
+ # @return [Astronoby::Center] a center at the observer's location
22
+ def topocentric(observer)
23
+ new(kind: TOPOCENTRIC, observer: observer)
24
+ end
25
+ end
26
+
27
+ # @return [Symbol] the kind of center
28
+ attr_reader :kind
29
+
30
+ # @return [Astronoby::Observer, nil] the observer, for topocentric centers
31
+ attr_reader :observer
32
+
33
+ # @param kind [Symbol] one of BARYCENTRIC, GEOCENTRIC, TOPOCENTRIC
34
+ # @param observer [Astronoby::Observer, nil] the observer, for topocentric
35
+ def initialize(kind:, observer: nil)
36
+ @kind = kind
37
+ @observer = observer
38
+ freeze
39
+ end
40
+
41
+ # @return [Boolean] true if the center is the Solar System barycenter
42
+ def barycentric?
43
+ @kind == BARYCENTRIC
44
+ end
45
+
46
+ # @return [Boolean] true if the center is the Earth's center
47
+ def geocentric?
48
+ @kind == GEOCENTRIC
49
+ end
50
+
51
+ # @return [Boolean] true if the center is at an observer's location
52
+ def topocentric?
53
+ @kind == TOPOCENTRIC
54
+ end
55
+
56
+ # @return [Boolean] true if the center depends on a specific observer
57
+ def observer_dependent?
58
+ topocentric?
59
+ end
60
+
61
+ # @param other [Astronoby::Center] center to compare with
62
+ # @return [Boolean] true if both centers are equivalent
63
+ def ==(other)
64
+ other.is_a?(self.class) &&
65
+ kind == other.kind &&
66
+ location_key == other.location_key
67
+ end
68
+ alias_method :eql?, :==
69
+
70
+ # @return [Integer] hash value
71
+ def hash
72
+ [self.class, @kind, location_key].hash
73
+ end
74
+
75
+ protected
76
+
77
+ # @return [Array, nil] the geometric location key, or nil if not topocentric
78
+ def location_key
79
+ return unless @observer
80
+
81
+ [@observer.latitude, @observer.longitude, @observer.elevation]
82
+ end
83
+ end
84
+ end