astronoby 0.9.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +101 -0
  4. data/README.md +6 -1
  5. data/UPGRADING.md +84 -0
  6. data/docs/README.md +80 -15
  7. data/docs/angles.md +1 -0
  8. data/docs/configuration.md +20 -17
  9. data/docs/coordinates.md +72 -12
  10. data/docs/deep_sky_bodies.md +1 -1
  11. data/docs/ephem.md +5 -2
  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 +20 -15
  16. data/docs/lunar_eclipses.md +93 -0
  17. data/docs/lunar_observation.md +87 -0
  18. data/docs/moon_phases.md +4 -1
  19. data/docs/observer.md +20 -6
  20. data/docs/planetary_phenomena.md +78 -0
  21. data/docs/reference_frames.md +192 -34
  22. data/docs/rise_transit_set_times.md +6 -4
  23. data/docs/solar_system_bodies.md +26 -4
  24. data/docs/twilight_times.md +25 -21
  25. data/lib/astronoby/angle.rb +63 -2
  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 +21 -0
  29. data/lib/astronoby/bodies/deep_sky_object.rb +6 -1
  30. data/lib/astronoby/bodies/deep_sky_object_position.rb +32 -17
  31. data/lib/astronoby/bodies/earth.rb +7 -44
  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 +158 -32
  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 +240 -61
  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/center.rb +84 -0
  44. data/lib/astronoby/constellation.rb +9 -1
  45. data/lib/astronoby/coordinates/ecliptic.rb +10 -1
  46. data/lib/astronoby/coordinates/equatorial.rb +64 -8
  47. data/lib/astronoby/coordinates/geodetic.rb +102 -0
  48. data/lib/astronoby/coordinates/horizontal.rb +13 -1
  49. data/lib/astronoby/distance.rb +35 -0
  50. data/lib/astronoby/duration.rb +116 -0
  51. data/lib/astronoby/earth_rotation.rb +70 -0
  52. data/lib/astronoby/equinox_solstice.rb +31 -8
  53. data/lib/astronoby/errors.rb +11 -0
  54. data/lib/astronoby/events/conjunction.rb +51 -0
  55. data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
  56. data/lib/astronoby/events/eclipse_phase.rb +27 -0
  57. data/lib/astronoby/events/extremum_calculator.rb +23 -176
  58. data/lib/astronoby/events/greatest_elongation.rb +58 -0
  59. data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
  60. data/lib/astronoby/events/lunar_eclipse.rb +99 -0
  61. data/lib/astronoby/events/lunar_eclipse_calculator.rb +285 -0
  62. data/lib/astronoby/events/opposition.rb +19 -0
  63. data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
  64. data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
  65. data/lib/astronoby/events/twilight_event.rb +24 -6
  66. data/lib/astronoby/events/twilight_events.rb +26 -6
  67. data/lib/astronoby/extremum_finder.rb +148 -0
  68. data/lib/astronoby/instant.rb +10 -7
  69. data/lib/astronoby/libration.rb +25 -0
  70. data/lib/astronoby/mean_obliquity.rb +8 -0
  71. data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
  72. data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
  73. data/lib/astronoby/nutation.rb +10 -20
  74. data/lib/astronoby/observer.rb +67 -49
  75. data/lib/astronoby/orientation.rb +107 -0
  76. data/lib/astronoby/position.rb +16 -0
  77. data/lib/astronoby/precession.rb +61 -60
  78. data/lib/astronoby/reference_frame.rb +73 -7
  79. data/lib/astronoby/reference_frames/apparent.rb +26 -7
  80. data/lib/astronoby/reference_frames/astrometric.rb +14 -1
  81. data/lib/astronoby/reference_frames/geometric.rb +7 -1
  82. data/lib/astronoby/reference_frames/mean_of_date.rb +13 -1
  83. data/lib/astronoby/reference_frames/teme.rb +153 -0
  84. data/lib/astronoby/reference_frames/topocentric.rb +30 -4
  85. data/lib/astronoby/refraction.rb +26 -5
  86. data/lib/astronoby/root_finder.rb +83 -0
  87. data/lib/astronoby/rotation.rb +49 -0
  88. data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +9 -0
  89. data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +42 -5
  90. data/lib/astronoby/time/greenwich_sidereal_time.rb +21 -0
  91. data/lib/astronoby/time/local_apparent_sidereal_time.rb +21 -0
  92. data/lib/astronoby/time/local_mean_sidereal_time.rb +21 -0
  93. data/lib/astronoby/time/local_sidereal_time.rb +24 -0
  94. data/lib/astronoby/time/sidereal_time.rb +23 -1
  95. data/lib/astronoby/true_obliquity.rb +4 -0
  96. data/lib/astronoby/util/maths.rb +8 -0
  97. data/lib/astronoby/util/time.rb +10 -485
  98. data/lib/astronoby/vector.rb +10 -0
  99. data/lib/astronoby/velocity.rb +39 -0
  100. data/lib/astronoby/version.rb +1 -1
  101. data/lib/astronoby.rb +22 -0
  102. metadata +45 -5
@@ -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,38 +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
 
45
- # @return [Boolean] True if the body is approaching the primary
46
- # body (Earth), false otherwise.
47
- def approaching_primary?
48
- relative_position =
49
- (geometric.position - @earth_geometric.position).map(&:m)
50
- relative_velocity =
51
- (geometric.velocity - @earth_geometric.velocity).map(&:mps)
52
- radial_velocity_component = Astronoby::Util::Maths
53
- .dot_product(relative_position, relative_velocity)
54
- distance = Math.sqrt(
55
- Astronoby::Util::Maths.dot_product(relative_position, relative_position)
56
- )
57
- radial_velocity_component / distance < 0
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
58
122
  end
59
123
 
60
- # @return [Boolean] True if the body is receding from the primary
61
- # body (Earth), false otherwise.
62
- def receding_from_primary?
63
- !approaching_primary?
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
+ )
64
173
  end
65
174
 
66
175
  private
67
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
+
68
190
  # Source:
69
191
  # Title: Astronomical Algorithms
70
192
  # Author: Jean Meeus
@@ -86,29 +208,33 @@ module Astronoby
86
208
  (@instant.tt - JulianDate::DEFAULT_EPOCH) / Constants::DAYS_PER_JULIAN_CENTURY
87
209
  end
88
210
 
89
- private
90
-
91
211
  # Source:
92
212
  # Title: Computing Apparent Planetary Magnitudes for The Astronomical
93
213
  # Almanac (2018)
94
214
  # Authors: Anthony Mallama and James L. Hilton
95
215
  def magnitude_correction_term
96
216
  phase_angle_degrees = phase_angle.degrees
97
- if phase_angle_degrees <= 150 && current_phase_fraction <= 0.5
98
- 2.9994 * 10**-2 * phase_angle_degrees -
99
- 1.6057 * 10**-4 * phase_angle_degrees**2 +
100
- 3.1543 * 10**-6 * phase_angle_degrees**3 -
101
- 2.0667 * 10**-8 * phase_angle_degrees**4 +
102
- 6.2553 * 10**-11 * phase_angle_degrees**5
103
- elsif phase_angle_degrees <= 150 && current_phase_fraction > 0.5
104
- 3.3234 * 10**-2 * phase_angle_degrees -
105
- 3.0725 * 10**-4 * phase_angle_degrees**2 +
106
- 6.1575 * 10**-6 * phase_angle_degrees**3 -
107
- 4.7723 * 10**-8 * phase_angle_degrees**4 +
108
- 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)
109
219
  else
110
- super
220
+ waxing_magnitude_correction(phase_angle_degrees)
111
221
  end
112
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
113
239
  end
114
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,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: