astronoby 0.8.0 → 0.10.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +159 -0
  4. data/README.md +12 -5
  5. data/UPGRADING.md +109 -0
  6. data/docs/README.md +109 -16
  7. data/docs/angles.md +2 -1
  8. data/docs/configuration.md +20 -17
  9. data/docs/coordinates.md +73 -13
  10. data/docs/deep_sky_bodies.md +101 -0
  11. data/docs/ephem.md +6 -3
  12. data/docs/equinoxes_solstices_times.md +4 -3
  13. data/docs/glossary.md +97 -1
  14. data/docs/iers.md +40 -0
  15. data/docs/instant.md +21 -16
  16. data/docs/lunar_eclipses.md +93 -0
  17. data/docs/lunar_observation.md +87 -0
  18. data/docs/moon_phases.md +5 -2
  19. data/docs/observer.md +21 -7
  20. data/docs/planetary_phenomena.md +78 -0
  21. data/docs/reference_frames.md +193 -35
  22. data/docs/rise_transit_set_times.md +10 -8
  23. data/docs/{celestial_bodies.md → solar_system_bodies.md} +27 -5
  24. data/docs/twilight_times.md +25 -21
  25. data/lib/astronoby/angle.rb +69 -4
  26. data/lib/astronoby/angles/dms.rb +18 -1
  27. data/lib/astronoby/angles/hms.rb +14 -1
  28. data/lib/astronoby/angular_velocity.rb +97 -0
  29. data/lib/astronoby/bodies/deep_sky_object.rb +49 -0
  30. data/lib/astronoby/bodies/deep_sky_object_position.rb +142 -0
  31. data/lib/astronoby/bodies/earth.rb +9 -42
  32. data/lib/astronoby/bodies/jupiter.rb +10 -0
  33. data/lib/astronoby/bodies/mars.rb +10 -0
  34. data/lib/astronoby/bodies/mercury.rb +10 -0
  35. data/lib/astronoby/bodies/moon.rb +162 -15
  36. data/lib/astronoby/bodies/neptune.rb +10 -0
  37. data/lib/astronoby/bodies/saturn.rb +10 -0
  38. data/lib/astronoby/bodies/solar_system_body.rb +257 -53
  39. data/lib/astronoby/bodies/sun.rb +79 -4
  40. data/lib/astronoby/bodies/uranus.rb +10 -0
  41. data/lib/astronoby/bodies/venus.rb +10 -0
  42. data/lib/astronoby/body.rb +6 -0
  43. data/lib/astronoby/cache.rb +1 -0
  44. data/lib/astronoby/center.rb +84 -0
  45. data/lib/astronoby/constants.rb +7 -2
  46. data/lib/astronoby/constellation.rb +9 -1
  47. data/lib/astronoby/coordinates/ecliptic.rb +10 -1
  48. data/lib/astronoby/coordinates/equatorial.rb +66 -13
  49. data/lib/astronoby/coordinates/geodetic.rb +102 -0
  50. data/lib/astronoby/coordinates/horizontal.rb +13 -1
  51. data/lib/astronoby/distance.rb +41 -0
  52. data/lib/astronoby/duration.rb +116 -0
  53. data/lib/astronoby/earth_rotation.rb +70 -0
  54. data/lib/astronoby/equinox_solstice.rb +31 -8
  55. data/lib/astronoby/errors.rb +11 -0
  56. data/lib/astronoby/events/conjunction.rb +51 -0
  57. data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
  58. data/lib/astronoby/events/eclipse_phase.rb +27 -0
  59. data/lib/astronoby/events/extremum_calculator.rb +80 -0
  60. data/lib/astronoby/events/extremum_event.rb +15 -0
  61. data/lib/astronoby/events/greatest_elongation.rb +58 -0
  62. data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
  63. data/lib/astronoby/events/lunar_eclipse.rb +99 -0
  64. data/lib/astronoby/events/lunar_eclipse_calculator.rb +285 -0
  65. data/lib/astronoby/events/opposition.rb +19 -0
  66. data/lib/astronoby/events/rise_transit_set_calculator.rb +9 -6
  67. data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
  68. data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
  69. data/lib/astronoby/events/twilight_calculator.rb +1 -1
  70. data/lib/astronoby/events/twilight_event.rb +24 -6
  71. data/lib/astronoby/events/twilight_events.rb +26 -6
  72. data/lib/astronoby/extremum_finder.rb +148 -0
  73. data/lib/astronoby/instant.rb +35 -9
  74. data/lib/astronoby/libration.rb +25 -0
  75. data/lib/astronoby/mean_obliquity.rb +8 -0
  76. data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
  77. data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
  78. data/lib/astronoby/nutation.rb +10 -20
  79. data/lib/astronoby/observer.rb +67 -49
  80. data/lib/astronoby/orientation.rb +107 -0
  81. data/lib/astronoby/position.rb +16 -0
  82. data/lib/astronoby/precession.rb +61 -60
  83. data/lib/astronoby/reference_frame.rb +73 -7
  84. data/lib/astronoby/reference_frames/apparent.rb +25 -16
  85. data/lib/astronoby/reference_frames/astrometric.rb +14 -1
  86. data/lib/astronoby/reference_frames/geometric.rb +7 -1
  87. data/lib/astronoby/reference_frames/mean_of_date.rb +13 -1
  88. data/lib/astronoby/reference_frames/teme.rb +153 -0
  89. data/lib/astronoby/reference_frames/topocentric.rb +31 -5
  90. data/lib/astronoby/refraction.rb +26 -5
  91. data/lib/astronoby/root_finder.rb +83 -0
  92. data/lib/astronoby/rotation.rb +49 -0
  93. data/lib/astronoby/stellar_propagation.rb +162 -0
  94. data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +31 -0
  95. data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +101 -0
  96. data/lib/astronoby/time/greenwich_sidereal_time.rb +41 -58
  97. data/lib/astronoby/time/local_apparent_sidereal_time.rb +63 -0
  98. data/lib/astronoby/time/local_mean_sidereal_time.rb +63 -0
  99. data/lib/astronoby/time/local_sidereal_time.rb +59 -26
  100. data/lib/astronoby/time/sidereal_time.rb +64 -0
  101. data/lib/astronoby/true_obliquity.rb +4 -0
  102. data/lib/astronoby/util/maths.rb +8 -0
  103. data/lib/astronoby/util/time.rb +10 -467
  104. data/lib/astronoby/vector.rb +10 -0
  105. data/lib/astronoby/velocity.rb +44 -0
  106. data/lib/astronoby/version.rb +1 -1
  107. data/lib/astronoby.rb +33 -0
  108. metadata +58 -6
@@ -1,14 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents Mercury.
4
5
  class Mercury < SolarSystemBody
5
6
  EQUATORIAL_RADIUS = Distance.from_meters(2_439_700)
6
7
  ABSOLUTE_MAGNITUDE = -0.613
8
+ ORBITAL_PERIOD = 87.969
7
9
 
10
+ # @return [Boolean] true; Mercury 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, MERCURY_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,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents the Moon. Provides phase events, apoapsis/periapsis events,
5
+ # and phase fraction.
4
6
  class Moon < SolarSystemBody
5
7
  SEMIDIAMETER_VARIATION = 0.7275
6
8
  EQUATORIAL_RADIUS = Distance.from_meters(1_737_400)
7
9
  ABSOLUTE_MAGNITUDE = 0.28
10
+ ORBITAL_PERIOD = 27.504339
8
11
 
12
+ # @param ephem_source [Symbol] the ephemeris source type
13
+ # @return [Array<Array>] ephemeris segment identifiers
9
14
  def self.ephemeris_segments(ephem_source)
10
15
  if ephem_source == ::Ephem::SPK::JPL_DE
11
16
  [
@@ -33,17 +38,155 @@ module Astronoby
33
38
  Events::MoonPhases.phases_for(year: year, month: month)
34
39
  end
35
40
 
41
+ # @return [Float] absolute magnitude
36
42
  def self.absolute_magnitude
37
43
  ABSOLUTE_MAGNITUDE
38
44
  end
39
45
 
46
+ # Finds all apoapsis events between two times
47
+ # @param ephem [::Ephem::SPK] Ephemeris data source
48
+ # @param start_time [Time] Start time
49
+ # @param end_time [Time] End time
50
+ # @return [Array<Astronoby::ExtremumEvent>] Array of apoapsis events
51
+ def self.apoapsis_events(
52
+ ephem:,
53
+ start_time:,
54
+ end_time:,
55
+ samples_per_period: 60
56
+ )
57
+ ExtremumCalculator.new(
58
+ body: self,
59
+ primary_body: Earth,
60
+ ephem: ephem,
61
+ samples_per_period: samples_per_period
62
+ ).apoapsis_events_between(start_time, end_time)
63
+ end
64
+
65
+ # Finds all periapsis events between two times
66
+ # @param ephem [::Ephem::SPK] Ephemeris data source
67
+ # @param start_time [Time] Start time
68
+ # @param end_time [Time] End time
69
+ # @return [Array<Astronoby::ExtremumEvent>] Array of periapsis events
70
+ def self.periapsis_events(
71
+ ephem:,
72
+ start_time:,
73
+ end_time:,
74
+ samples_per_period: 60
75
+ )
76
+ ExtremumCalculator.new(
77
+ body: self,
78
+ primary_body: Earth,
79
+ ephem: ephem,
80
+ samples_per_period: samples_per_period
81
+ ).periapsis_events_between(start_time, end_time)
82
+ end
83
+
84
+ # Finds all lunar eclipses whose greatest instant falls between two times
85
+ # @param ephem [::Ephem::SPK] Ephemeris data source
86
+ # @param start_time [Time] Start time
87
+ # @param end_time [Time] End time
88
+ # @return [Array<Astronoby::LunarEclipse>] Lunar eclipses in the range
89
+ def self.eclipse_events(ephem:, start_time:, end_time:)
90
+ LunarEclipseCalculator
91
+ .new(ephem: ephem)
92
+ .events_between(start_time, end_time)
93
+ end
94
+
40
95
  # @return [Float] Phase fraction, from 0 to 1
41
96
  def current_phase_fraction
42
97
  mean_elongation.degrees / Constants::DEGREES_PER_CIRCLE
43
98
  end
44
99
 
100
+ # Total geocentric libration of the Moon, in longitude and latitude.
101
+ #
102
+ # With an orientation kernel (see +orientation:+ on the constructor), this
103
+ # is the sub-Earth point from the integrated DE orientation, accurate to
104
+ # better than an arcsecond. Without one, it is the analytic optical plus
105
+ # physical libration (Meeus, Astronomical Algorithms, 2nd ed., chapter 53).
106
+ #
107
+ # @return [Astronoby::Libration] Libration in longitude and latitude
108
+ def libration
109
+ lunar_ephemeris.libration
110
+ end
111
+
112
+ # Position angle of the Moon's axis of rotation, measured eastward from the
113
+ # north point of the disk.
114
+ #
115
+ # With an orientation kernel this comes from the integrated DE orientation;
116
+ # without one, from the analytic series (Meeus, Astronomical Algorithms,
117
+ # 2nd ed., chapter 53).
118
+ #
119
+ # @return [Astronoby::Angle] Position angle of the axis
120
+ def position_angle_of_axis
121
+ lunar_ephemeris.position_angle_of_axis
122
+ end
123
+
124
+ # Position angle of the Moon's bright limb: the position angle of the
125
+ # midpoint of the illuminated limb, measured eastward from the north point
126
+ # of the disk. Geocentric, from the apparent equatorial coordinates of the
127
+ # Moon and the Sun.
128
+ #
129
+ # Source:
130
+ # Title: Astronomical Algorithms
131
+ # Author: Jean Meeus
132
+ # Edition: 2nd edition
133
+ # Chapter: 48 - Illuminated Fraction of the Moon's Disk
134
+ # @return [Astronoby::Angle, nil] Position angle of the bright limb, between
135
+ # 0 and 360 degrees
136
+ def bright_limb_position_angle
137
+ return unless sun
138
+
139
+ @bright_limb_position_angle ||= begin
140
+ angle = apparent.equatorial.position_angle_to(sun.apparent.equatorial)
141
+ Angle.from_degrees(angle.degrees % Constants::DEGREES_PER_CIRCLE)
142
+ end
143
+ end
144
+
145
+ # Parallactic angle of the Moon for a given observer: the angle at the Moon
146
+ # between the direction of the north celestial pole and the direction of
147
+ # the observer's zenith. Computed from the topocentric place, where lunar
148
+ # parallax is significant.
149
+ #
150
+ # Source:
151
+ # Title: Astronomical Algorithms
152
+ # Author: Jean Meeus
153
+ # Edition: 2nd edition
154
+ # Chapter: 14 - The Parallactic Angle
155
+ # @param observer [Astronoby::Observer] Observer for whom to compute the
156
+ # parallactic angle
157
+ # @return [Astronoby::Angle] Parallactic angle
158
+ def parallactic_angle(observer:)
159
+ equatorial = observed_by(observer).equatorial
160
+ declination = equatorial.declination
161
+ hour_angle = equatorial.compute_hour_angle(
162
+ time: @instant.to_time,
163
+ longitude: observer.longitude
164
+ )
165
+
166
+ Angle.from_radians(
167
+ Math.atan2(
168
+ hour_angle.sin,
169
+ observer.latitude.tan * declination.cos -
170
+ declination.sin * hour_angle.cos
171
+ )
172
+ )
173
+ end
174
+
45
175
  private
46
176
 
177
+ def lunar_ephemeris
178
+ @lunar_ephemeris ||=
179
+ if orientation
180
+ MoonOrientationEphemeris.new(self)
181
+ else
182
+ MoonPhysicalEphemeris.new(self)
183
+ end
184
+ end
185
+
186
+ def primary_body_geometric
187
+ earth_geometric
188
+ end
189
+
47
190
  # Source:
48
191
  # Title: Astronomical Algorithms
49
192
  # Author: Jean Meeus
@@ -65,29 +208,33 @@ module Astronoby
65
208
  (@instant.tt - JulianDate::DEFAULT_EPOCH) / Constants::DAYS_PER_JULIAN_CENTURY
66
209
  end
67
210
 
68
- private
69
-
70
211
  # Source:
71
212
  # Title: Computing Apparent Planetary Magnitudes for The Astronomical
72
213
  # Almanac (2018)
73
214
  # Authors: Anthony Mallama and James L. Hilton
74
215
  def magnitude_correction_term
75
216
  phase_angle_degrees = phase_angle.degrees
76
- if phase_angle_degrees <= 150 && current_phase_fraction <= 0.5
77
- 2.9994 * 10**-2 * phase_angle_degrees -
78
- 1.6057 * 10**-4 * phase_angle_degrees**2 +
79
- 3.1543 * 10**-6 * phase_angle_degrees**3 -
80
- 2.0667 * 10**-8 * phase_angle_degrees**4 +
81
- 6.2553 * 10**-11 * phase_angle_degrees**5
82
- elsif phase_angle_degrees <= 150 && current_phase_fraction > 0.5
83
- 3.3234 * 10**-2 * phase_angle_degrees -
84
- 3.0725 * 10**-4 * phase_angle_degrees**2 +
85
- 6.1575 * 10**-6 * phase_angle_degrees**3 -
86
- 4.7723 * 10**-8 * phase_angle_degrees**4 +
87
- 1.4681 * 10**-10 * phase_angle_degrees**5
217
+ if phase_angle_degrees <= 150 && current_phase_fraction > 0.5
218
+ waning_magnitude_correction(phase_angle_degrees)
88
219
  else
89
- super
220
+ waxing_magnitude_correction(phase_angle_degrees)
90
221
  end
91
222
  end
223
+
224
+ def waxing_magnitude_correction(phase_angle_degrees)
225
+ 2.9994 * 10**-2 * phase_angle_degrees -
226
+ 1.6057 * 10**-4 * phase_angle_degrees**2 +
227
+ 3.1543 * 10**-6 * phase_angle_degrees**3 -
228
+ 2.0667 * 10**-8 * phase_angle_degrees**4 +
229
+ 6.2553 * 10**-11 * phase_angle_degrees**5
230
+ end
231
+
232
+ def waning_magnitude_correction(phase_angle_degrees)
233
+ 3.3234 * 10**-2 * phase_angle_degrees -
234
+ 3.0725 * 10**-4 * phase_angle_degrees**2 +
235
+ 6.1575 * 10**-6 * phase_angle_degrees**3 -
236
+ 4.7723 * 10**-8 * phase_angle_degrees**4 +
237
+ 1.4681 * 10**-10 * phase_angle_degrees**5
238
+ end
92
239
  end
93
240
  end
@@ -1,14 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents Neptune.
4
5
  class Neptune < SolarSystemBody
5
6
  EQUATORIAL_RADIUS = Distance.from_meters(24_764_000)
6
7
  ABSOLUTE_MAGNITUDE = -7.0
8
+ ORBITAL_PERIOD = 60182.0
7
9
 
10
+ # @return [Boolean] true; Neptune 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, NEPTUNE_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 Saturn.
4
5
  class Saturn < SolarSystemBody
5
6
  EQUATORIAL_RADIUS = Distance.from_meters(60_268_000)
6
7
  ABSOLUTE_MAGNITUDE = -8.914
8
+ ORBITAL_PERIOD = 10759.22
7
9
 
10
+ # @return [Boolean] true; Saturn 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, SATURN_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,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,12 +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
31
+
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)
43
+ end
21
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
22
50
  def self.geometric(ephem:, instant:)
23
51
  compute_geometric(ephem: ephem, instant: instant)
24
52
  end
25
53
 
54
+ # @param ephem [::Ephem::SPK] ephemeris data source
55
+ # @param instant [Astronoby::Instant] the time instant
56
+ # @return [Astronoby::Geometric] the geometric frame
26
57
  def self.compute_geometric(ephem:, instant:)
27
58
  segments = ephemeris_segments(ephem.type)
28
59
  segment1 = segments[0]
@@ -62,63 +93,196 @@ module Astronoby
62
93
  end
63
94
  end
64
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
65
99
  def self.ephemeris_segments(_ephem_source)
66
100
  raise NotImplementedError
67
101
  end
68
102
 
103
+ # @return [Float, nil] absolute magnitude of the body
69
104
  def self.absolute_magnitude
70
105
  nil
71
106
  end
72
107
 
73
- 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
74
235
  @instant = instant
75
- @geometric = compute_geometric(ephem)
76
- @earth_geometric = Earth.geometric(ephem: ephem, instant: instant)
77
- @light_time_corrected_position,
78
- @light_time_corrected_velocity =
79
- Correction::LightTimeDelay.compute(
80
- center: @earth_geometric,
81
- target: @geometric,
82
- ephem: ephem
83
- )
84
- 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
+ )
85
245
  end
86
246
 
247
+ # @return [Astronoby::Geometric] Earth's geometric reference frame
248
+ def earth_geometric
249
+ @earth_geometric ||= Earth.geometric(ephem: @ephem, instant: @instant)
250
+ end
251
+
252
+ # @return [Astronoby::Astrometric] the astrometric reference frame (GCRS)
87
253
  def astrometric
88
254
  @astrometric ||= Astrometric.build_from_geometric(
89
255
  instant: @instant,
90
- earth_geometric: @earth_geometric,
91
- light_time_corrected_position: @light_time_corrected_position,
92
- light_time_corrected_velocity: @light_time_corrected_velocity,
93
- 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
94
260
  )
95
261
  end
96
262
 
263
+ # @return [Astronoby::MeanOfDate] the mean-of-date reference frame
97
264
  def mean_of_date
98
265
  @mean_of_date ||= MeanOfDate.build_from_geometric(
99
266
  instant: @instant,
100
- target_geometric: @geometric,
101
- earth_geometric: @earth_geometric,
102
- target_body: self
267
+ target_geometric: geometric,
268
+ earth_geometric: earth_geometric,
269
+ target_body: body
103
270
  )
104
271
  end
105
272
 
273
+ # @return [Astronoby::Apparent] the apparent reference frame
106
274
  def apparent
107
275
  @apparent ||= Apparent.build_from_astrometric(
108
276
  instant: @instant,
109
277
  target_astrometric: astrometric,
110
- earth_geometric: @earth_geometric,
111
- target_body: self
278
+ earth_geometric: earth_geometric,
279
+ target_body: body
112
280
  )
113
281
  end
114
282
 
115
- def observed_by(observer)
116
- Topocentric.build_from_apparent(
117
- apparent: apparent,
118
- observer: observer,
119
- instant: @instant,
120
- target_body: self
121
- )
283
+ # @return [Astronoby::Body] the body definition (the class itself)
284
+ def body
285
+ self.class
122
286
  end
123
287
 
124
288
  # Returns the constellation of the body
@@ -132,6 +296,25 @@ module Astronoby
132
296
  )
133
297
  end
134
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
+
135
318
  # Source:
136
319
  # Title: Astronomical Algorithms
137
320
  # Author: Jean Meeus
@@ -139,23 +322,12 @@ module Astronoby
139
322
  # Chapter: 48 - Illuminated Fraction of the Moon's Disk
140
323
  # @return [Astronoby::Angle, nil] Phase angle of the body
141
324
  def phase_angle
142
- return unless @sun
325
+ return unless sun
143
326
 
144
327
  @phase_angle ||= begin
145
- geocentric_elongation = Angle.acos(
146
- @sun.apparent.equatorial.declination.sin *
147
- apparent.equatorial.declination.sin +
148
- @sun.apparent.equatorial.declination.cos *
149
- apparent.equatorial.declination.cos *
150
- (
151
- @sun.apparent.equatorial.right_ascension -
152
- apparent.equatorial.right_ascension
153
- ).cos
154
- )
155
-
156
- term1 = @sun.astrometric.distance.km * geocentric_elongation.sin
328
+ term1 = sun.astrometric.distance.km * elongation.sin
157
329
  term2 = astrometric.distance.km -
158
- @sun.astrometric.distance.km * geocentric_elongation.cos
330
+ sun.astrometric.distance.km * elongation.cos
159
331
  angle = Angle.atan(term1 / term2)
160
332
  Astronoby::Util::Trigonometry
161
333
  .adjustement_for_arctangent(term1, term2, angle)
@@ -182,7 +354,7 @@ module Astronoby
182
354
 
183
355
  @apparent_magnitude ||= begin
184
356
  body_sun_distance =
185
- (astrometric.position - @sun.astrometric.position).magnitude
357
+ (astrometric.position - sun.astrometric.position).magnitude
186
358
  self.class.absolute_magnitude +
187
359
  5 * Math.log10(body_sun_distance.au * astrometric.distance.au) +
188
360
  magnitude_correction_term
@@ -202,22 +374,54 @@ module Astronoby
202
374
  end
203
375
  end
204
376
 
377
+ # @return [Boolean] True if the body is approaching its primary
378
+ # body, false otherwise.
379
+ def approaching_primary?
380
+ relative_position =
381
+ (geometric.position - primary_body_geometric.position).map(&:m)
382
+ relative_velocity =
383
+ (geometric.velocity - primary_body_geometric.velocity).map(&:mps)
384
+ radial_velocity_component = Astronoby::Util::Maths
385
+ .dot_product(relative_position, relative_velocity)
386
+ distance = Math.sqrt(
387
+ Astronoby::Util::Maths.dot_product(relative_position, relative_position)
388
+ )
389
+ radial_velocity_component / distance < 0
390
+ end
391
+
392
+ # @return [Boolean] True if the body is receding from its primary
393
+ # body, false otherwise.
394
+ def receding_from_primary?
395
+ !approaching_primary?
396
+ end
397
+
205
398
  private
206
399
 
207
- # By default, Solar System bodies expose attributes that are dependent on
208
- # the Sun's position, such as phase angle and illuminated fraction.
209
- # If a body does not require Sun data, it should override this method to
210
- # return false.
211
- def requires_sun_data?
212
- true
400
+ def sun
401
+ @sun ||= Sun.new(instant: @instant, ephem: @ephem)
402
+ end
403
+
404
+ def primary_body_geometric
405
+ sun.geometric
213
406
  end
214
407
 
215
- def compute_geometric(ephem)
216
- self.class.compute_geometric(ephem: ephem, instant: @instant)
408
+ def light_time_corrected_position
409
+ compute_light_time_correction unless @light_time_corrected_position
410
+ @light_time_corrected_position
217
411
  end
218
412
 
219
- def compute_sun(ephem)
220
- @sun ||= Sun.new(instant: @instant, ephem: ephem)
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
+ )
221
425
  end
222
426
 
223
427
  # Source: