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
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # Computes the ephemeris for physical observations of the Moon: the total
5
+ # (optical + physical) libration in longitude and latitude, and the position
6
+ # angle of the Moon's axis of rotation.
7
+ #
8
+ # Source:
9
+ # Title: Astronomical Algorithms
10
+ # Author: Jean Meeus
11
+ # Edition: 2nd edition
12
+ # Chapter: 53 - Ephemeris for Physical Observations of the Moon
13
+ class MoonPhysicalEphemeris
14
+ # Inclination of the mean lunar equator to the ecliptic
15
+ INCLINATION = Angle.from_degrees(1.54242)
16
+
17
+ # @param moon [Astronoby::Moon] the Moon at the desired instant
18
+ def initialize(moon)
19
+ @moon = moon
20
+ @instant = moon.instant
21
+ end
22
+
23
+ # @return [Astronoby::Libration] the total libration in longitude and
24
+ # latitude
25
+ def libration
26
+ Libration.new(
27
+ longitude: Angle.from_degrees(libration_in_longitude),
28
+ latitude: Angle.from_degrees(libration_in_latitude)
29
+ )
30
+ end
31
+
32
+ # @return [Astronoby::Angle] the position angle of the Moon's axis of
33
+ # rotation, measured eastward from the north point of the disk
34
+ def position_angle_of_axis
35
+ i_rho = INCLINATION + Angle.from_degrees(rho)
36
+ v = ascending_node +
37
+ nutation_in_longitude +
38
+ Angle.from_degrees(sigma / INCLINATION.sin)
39
+
40
+ x = i_rho.sin * v.sin
41
+ y = i_rho.sin * v.cos * obliquity.cos - i_rho.cos * obliquity.sin
42
+ omega = Math.atan2(x, y)
43
+
44
+ Angle.from_radians(
45
+ Math.asin(
46
+ Math.sqrt(x * x + y * y) *
47
+ Math.cos(right_ascension.radians - omega) /
48
+ Angle.from_degrees(libration_in_latitude).cos
49
+ )
50
+ )
51
+ end
52
+
53
+ private
54
+
55
+ # @return [Float] total libration in longitude, in degrees, in (-180, 180]
56
+ def libration_in_longitude
57
+ l = optical_longitude + physical_longitude
58
+ ((l + 180) % 360) - 180
59
+ end
60
+
61
+ # @return [Float] total libration in latitude, in degrees
62
+ def libration_in_latitude
63
+ optical_latitude + physical_latitude
64
+ end
65
+
66
+ # Optical libration in longitude, l' = A - F (Meeus 53.1)
67
+ # @return [Float] in degrees
68
+ def optical_longitude
69
+ argument_a.degrees - argument_of_latitude.degrees
70
+ end
71
+
72
+ # Optical libration in latitude (Meeus 53.1)
73
+ # @return [Float] in degrees
74
+ def optical_latitude
75
+ Angle.from_radians(optical_latitude_radians).degrees
76
+ end
77
+
78
+ # Physical libration in longitude (Meeus 53.2)
79
+ # @return [Float] in degrees
80
+ def physical_longitude
81
+ -tau +
82
+ (rho * argument_a.cos + sigma * argument_a.sin) *
83
+ Math.tan(optical_latitude_radians)
84
+ end
85
+
86
+ # Physical libration in latitude (Meeus 53.2)
87
+ # @return [Float] in degrees
88
+ def physical_latitude
89
+ sigma * argument_a.cos - rho * argument_a.sin
90
+ end
91
+
92
+ # The argument "A" of the optical libration (Meeus 53.1)
93
+ def argument_a
94
+ @argument_a ||= Angle.from_radians(
95
+ Math.atan2(
96
+ w.sin * latitude.cos * INCLINATION.cos -
97
+ latitude.sin * INCLINATION.sin,
98
+ w.cos * latitude.cos
99
+ )
100
+ )
101
+ end
102
+
103
+ def optical_latitude_radians
104
+ @optical_latitude_radians ||= Math.asin(
105
+ -w.sin * latitude.cos * INCLINATION.sin - latitude.sin * INCLINATION.cos
106
+ )
107
+ end
108
+
109
+ # W = λ - Δψ - Ω, the Moon's longitude referred to the mean equinox of
110
+ # date, measured from the ascending node of the mean lunar equator
111
+ def w
112
+ @w ||= longitude - nutation_in_longitude - ascending_node
113
+ end
114
+
115
+ # @return [Float] periodic terms ρ, in degrees (Meeus 53)
116
+ def rho
117
+ @rho ||= trig_series([
118
+ [-0.02752, mp.radians, :cos],
119
+ [-0.02245, f.radians, :sin],
120
+ [0.00684, (mp - f - f).radians, :cos],
121
+ [-0.00293, (f + f).radians, :cos],
122
+ [-0.00085, (f + f - d - d).radians, :cos],
123
+ [-0.00054, (mp - d - d).radians, :cos],
124
+ [-0.00020, (mp + f).radians, :sin],
125
+ [-0.00020, (mp + f + f).radians, :cos],
126
+ [-0.00020, (mp - f).radians, :cos],
127
+ [0.00014, (mp + f + f - d - d).radians, :cos]
128
+ ])
129
+ end
130
+
131
+ # @return [Float] periodic terms σ, in degrees (Meeus 53)
132
+ def sigma
133
+ @sigma ||= trig_series([
134
+ [-0.02816, mp.radians, :sin],
135
+ [0.02244, f.radians, :cos],
136
+ [-0.00682, (mp - f - f).radians, :sin],
137
+ [-0.00279, (f + f).radians, :sin],
138
+ [-0.00083, (f + f - d - d).radians, :sin],
139
+ [0.00069, (mp - d - d).radians, :sin],
140
+ [0.00040, (mp + f).radians, :cos],
141
+ [-0.00025, (mp + mp).radians, :sin],
142
+ [-0.00023, (mp + f + f).radians, :sin],
143
+ [0.00020, (mp - f).radians, :cos],
144
+ [0.00019, (mp - f).radians, :sin],
145
+ [0.00013, (mp + f + f - d - d).radians, :sin],
146
+ [-0.00010, (mp - f - f - f).radians, :cos]
147
+ ])
148
+ end
149
+
150
+ # @return [Float] periodic terms τ, in degrees (Meeus 53)
151
+ def tau
152
+ @tau ||= trig_series([
153
+ [0.02520 * eccentricity, m.radians, :sin],
154
+ [0.00473, (mp + mp - f - f).radians, :sin],
155
+ [-0.00467, mp.radians, :sin],
156
+ [0.00396, k1.radians, :sin],
157
+ [0.00276, (mp + mp - d - d).radians, :sin],
158
+ [0.00196, ascending_node.radians, :sin],
159
+ [-0.00183, (mp - f).radians, :cos],
160
+ [0.00115, (mp - d - d).radians, :sin],
161
+ [-0.00096, (mp - d).radians, :sin],
162
+ [0.00046, (f + f - d - d).radians, :sin],
163
+ [-0.00039, (mp - f).radians, :sin],
164
+ [-0.00032, (mp - m - d).radians, :sin],
165
+ [0.00027, (mp + mp - m - d - d).radians, :sin],
166
+ [0.00023, k2.radians, :sin],
167
+ [-0.00014, (d + d).radians, :sin],
168
+ [0.00014, (mp + mp - f - f).radians, :cos],
169
+ [-0.00012, (mp - f - f).radians, :sin],
170
+ [-0.00012, (mp + mp).radians, :sin],
171
+ [0.00011, (mp + mp - m - m - d - d).radians, :sin]
172
+ ])
173
+ end
174
+
175
+ def trig_series(terms)
176
+ terms.sum do |coefficient, argument, fn|
177
+ coefficient * Math.send(fn, argument)
178
+ end
179
+ end
180
+
181
+ # Apparent geocentric ecliptic longitude (true equinox of date, λ)
182
+ def longitude
183
+ @longitude ||= @moon.apparent.ecliptic.longitude
184
+ end
185
+
186
+ # Apparent geocentric ecliptic latitude (β)
187
+ def latitude
188
+ @latitude ||= @moon.apparent.ecliptic.latitude
189
+ end
190
+
191
+ # Apparent geocentric right ascension (α)
192
+ def right_ascension
193
+ @right_ascension ||= @moon.apparent.equatorial.right_ascension
194
+ end
195
+
196
+ def obliquity
197
+ @obliquity ||= TrueObliquity.at(@instant)
198
+ end
199
+
200
+ def nutation_in_longitude
201
+ @nutation_in_longitude ||=
202
+ Nutation.new(instant: @instant).nutation_in_longitude
203
+ end
204
+
205
+ def t
206
+ @t ||=
207
+ (@instant.tt - JulianDate::J2000) / Constants::DAYS_PER_JULIAN_CENTURY
208
+ end
209
+
210
+ # Mean elongation of the Moon (D), Meeus 47.5
211
+ def d
212
+ @d ||= Angle.from_degrees(
213
+ 297.8501921 + 445267.1114034 * t - 0.0018819 * t**2 +
214
+ t**3 / 545868 - t**4 / 113065000
215
+ )
216
+ end
217
+
218
+ # Sun's mean anomaly (M), Meeus 47.3
219
+ def m
220
+ @m ||= Angle.from_degrees(
221
+ 357.5291092 + 35999.0502909 * t - 0.0001536 * t**2 + t**3 / 24490000
222
+ )
223
+ end
224
+
225
+ # Moon's mean anomaly (M'), Meeus 47.4
226
+ def mp
227
+ @mp ||= Angle.from_degrees(
228
+ 134.9633964 + 477198.8675055 * t + 0.0087414 * t**2 +
229
+ t**3 / 69699 - t**4 / 14712000
230
+ )
231
+ end
232
+
233
+ # Moon's argument of latitude (F), Meeus 47.5
234
+ def f
235
+ @f ||= Angle.from_degrees(
236
+ 93.2720950 + 483202.0175233 * t - 0.0036539 * t**2 -
237
+ t**3 / 3526000 + t**4 / 863310000
238
+ )
239
+ end
240
+ alias_method :argument_of_latitude, :f
241
+
242
+ # Longitude of the mean ascending node (Ω), Meeus 47.7
243
+ def ascending_node
244
+ @ascending_node ||= Angle.from_degrees(
245
+ 125.0445479 - 1934.1362891 * t + 0.0020754 * t**2 +
246
+ t**3 / 467441 - t**4 / 60616000
247
+ )
248
+ end
249
+
250
+ # Eccentricity correction (E), Meeus 47.6
251
+ def eccentricity
252
+ @eccentricity ||= 1 - 0.002516 * t - 0.0000074 * t**2
253
+ end
254
+
255
+ def k1
256
+ @k1 ||= Angle.from_degrees(119.75 + 131.849 * t)
257
+ end
258
+
259
+ def k2
260
+ @k2 ||= Angle.from_degrees(72.56 + 20.186 * t)
261
+ end
262
+ end
263
+ end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Computes nutation using the IAU 2000B model (77-term series).
5
+ # Provides both the nutation matrix and the individual nutation angles
6
+ # (in longitude and obliquity).
4
7
  class Nutation
5
8
  # IAU 2000B model corrections (in microarcseconds)
6
9
  IAU2000B_DPSI_CORRECTION = -0.000135e7
@@ -139,31 +142,18 @@ module Astronoby
139
142
  dpsi = 0.0
140
143
  deps = 0.0
141
144
 
142
- NUTATION_TERMS.each do |term|
143
- # Extract the fundamental argument coefficients
144
- arg_coef = term[0..4]
145
+ radians = a.map(&:radians)
146
+ jc = julian_centuries
145
147
 
146
- # Calculate the argument
147
- arg = Util::Maths.dot_product(arg_coef, a.map(&:radians))
148
+ NUTATION_TERMS.each do |term|
149
+ arg = term[0] * radians[0] + term[1] * radians[1] +
150
+ term[2] * radians[2] + term[3] * radians[3] + term[4] * radians[4]
148
151
 
149
152
  sin_arg = Math.sin(arg)
150
153
  cos_arg = Math.cos(arg)
151
154
 
152
- # Extract longitude coefficients
153
- long_coef = term[5..7]
154
-
155
- # Extract obliquity coefficients
156
- obl_coef = term[8..10]
157
-
158
- # Update dpsi using longitude coefficients
159
- dpsi += long_coef[0] * sin_arg
160
- dpsi += long_coef[1] * sin_arg * julian_centuries
161
- dpsi += long_coef[2] * cos_arg
162
-
163
- # Update deps using obliquity coefficients
164
- deps += obl_coef[0] * cos_arg
165
- deps += obl_coef[1] * cos_arg * julian_centuries
166
- deps += obl_coef[2] * sin_arg
155
+ dpsi += term[5] * sin_arg + term[6] * sin_arg * jc + term[7] * cos_arg
156
+ deps += term[8] * cos_arg + term[9] * cos_arg * jc + term[10] * sin_arg
167
157
  end
168
158
 
169
159
  [dpsi, deps] # in microarcseconds
@@ -10,15 +10,26 @@ module Astronoby
10
10
  MOLAR_MASS_OF_AIR = 0.0289644
11
11
  UNIVERSAL_GAS_CONSTANT = 8.31432
12
12
 
13
- attr_reader :latitude,
14
- :longitude,
15
- :elevation,
16
- :utc_offset,
17
- :temperature,
18
- :pressure
19
-
20
- # @param latitude [Angle] geographic latitude of the observer
21
- # @param longitude [Angle] geographic longitude of the observer
13
+ # @return [Astronoby::Angle] geographic latitude
14
+ attr_reader :latitude
15
+
16
+ # @return [Astronoby::Angle] geographic longitude
17
+ attr_reader :longitude
18
+
19
+ # @return [Astronoby::Distance] geographic elevation above sea level
20
+ attr_reader :elevation
21
+
22
+ # @return [Numeric, String] offset from Coordinated Universal Time
23
+ attr_reader :utc_offset
24
+
25
+ # @return [Numeric] temperature in kelvins
26
+ attr_reader :temperature
27
+
28
+ # @return [Numeric] atmospheric pressure in millibars
29
+ attr_reader :pressure
30
+
31
+ # @param latitude [Astronoby::Angle] geographic latitude of the observer
32
+ # @param longitude [Astronoby::Angle] geographic longitude of the observer
22
33
  # @param elevation [Astronoby::Distance] geographic elevation (or altitude)
23
34
  # of the observer above sea level
24
35
  # @param utc_offset [Numeric, String] offset from Coordinated Universal Time
@@ -42,45 +53,44 @@ module Astronoby
42
53
  @pressure = pressure || compute_pressure
43
54
  end
44
55
 
56
+ # Returns the observer's ECEF position vector (WGS-84 geodetic to ECEF).
57
+ #
58
+ # @return [Astronoby::Vector<Astronoby::Distance>] geocentric position
45
59
  def geocentric_position
46
- n = earth_prime_vertical_radius_of_curvature
47
- x = (n + @elevation.m) * @latitude.cos * @longitude.cos
48
- y = (n + @elevation.m) * @latitude.cos * @longitude.sin
49
- z = (n * (1 - Constants::WGS84_ECCENTICITY_SQUARED) + @elevation.m) *
50
- @latitude.sin
51
- Distance.vector_from_meters([x, y, z])
60
+ @geocentric_position ||= begin
61
+ n = earth_prime_vertical_radius_of_curvature
62
+ x = (n + @elevation.m) * @latitude.cos * @longitude.cos
63
+ y = (n + @elevation.m) * @latitude.cos * @longitude.sin
64
+ z = (n * (1 - Constants::WGS84_ECCENTICITY_SQUARED) + @elevation.m) *
65
+ @latitude.sin
66
+ Distance.vector_from_meters([x, y, z])
67
+ end
52
68
  end
53
69
 
70
+ # Returns the observer's ECEF velocity vector due to Earth rotation.
71
+ #
72
+ # @return [Astronoby::Vector<Astronoby::Velocity>] geocentric velocity
54
73
  def geocentric_velocity
55
- r = projected_radius
56
- vx = -Constants::EARTH_ANGULAR_VELOCITY_RAD_PER_S * r * @longitude.sin
57
- vy = Constants::EARTH_ANGULAR_VELOCITY_RAD_PER_S * r * @longitude.cos
58
- vz = 0.0
59
- Velocity.vector_from_mps([vx, vy, vz])
74
+ @geocentric_velocity ||= begin
75
+ r = projected_radius
76
+ vx = -Constants::EARTH_ANGULAR_VELOCITY_RAD_PER_S * r * @longitude.sin
77
+ vy = Constants::EARTH_ANGULAR_VELOCITY_RAD_PER_S * r * @longitude.cos
78
+ vz = 0.0
79
+ Velocity.vector_from_mps([vx, vy, vz])
80
+ end
60
81
  end
61
82
 
83
+ # Computes the Earth-fixed rotation matrix R₃(GAST) * W (polar motion)
84
+ # for a given instant.
85
+ #
86
+ # @param instant [Astronoby::Instant] the time instant
87
+ # @return [Matrix] 3x3 rotation matrix
62
88
  def earth_fixed_rotation_matrix_for(instant)
63
- dpsi = Nutation.new(instant: instant).nutation_in_longitude
64
-
65
- mean_obliquity = MeanObliquity.at(instant)
66
-
67
- gast = Angle.from_radians(
68
- Angle.from_hours(instant.gmst).radians +
69
- dpsi.radians * mean_obliquity.cos
70
- )
71
-
72
- earth_rotation_matrix = Matrix[
73
- [gast.cos, -gast.sin, 0],
74
- [gast.sin, gast.cos, 0],
75
- [0, 0, 1]
76
- ]
77
-
78
- nutation_matrix = Nutation.matrix_for(instant)
79
- precession_matrix = Precession.matrix_for(instant)
80
-
81
- earth_rotation_matrix * nutation_matrix * precession_matrix
89
+ EarthRotation.matrix_for(instant) * polar_motion_matrix_for(instant)
82
90
  end
83
91
 
92
+ # @param other [Astronoby::Observer] observer to compare with
93
+ # @return [Boolean] true if all attributes are equal
84
94
  def ==(other)
85
95
  return false unless other.is_a?(self.class)
86
96
 
@@ -93,6 +103,7 @@ module Astronoby
93
103
  end
94
104
  alias_method :eql?, :==
95
105
 
106
+ # @return [Integer] hash value
96
107
  def hash
97
108
  [
98
109
  self.class,
@@ -107,7 +118,6 @@ module Astronoby
107
118
 
108
119
  private
109
120
 
110
- # @return [Float] the atmospheric pressure in millibars.
111
121
  def compute_pressure
112
122
  @pressure ||= PRESSURE_AT_SEA_LEVEL * pressure_ratio
113
123
  end
@@ -124,20 +134,28 @@ module Astronoby
124
134
  Math.exp(-term1 / term2)
125
135
  end
126
136
 
137
+ def polar_motion_matrix_for(instant)
138
+ rows = IERS::PolarMotion.rotation_matrix_at(instant.to_time)
139
+ Matrix[*rows]
140
+ rescue IERS::OutOfRangeError
141
+ Matrix.identity(3)
142
+ end
143
+
127
144
  def earth_prime_vertical_radius_of_curvature
128
- Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS./(
129
- Math.sqrt(
130
- 1 -
131
- Constants::WGS84_ECCENTICITY_SQUARED * @latitude.sin * @latitude.sin
145
+ @earth_prime_vertical_radius_of_curvature ||=
146
+ Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS./(
147
+ Math.sqrt(
148
+ 1 -
149
+ Constants::WGS84_ECCENTICITY_SQUARED * @latitude.sin * @latitude.sin
150
+ )
132
151
  )
133
- )
134
152
  end
135
153
 
136
154
  def projected_radius
137
- Math.sqrt(
138
- geocentric_position.x.m * geocentric_position.x.m +
139
- geocentric_position.y.m * geocentric_position.y.m
140
- )
155
+ @projected_radius ||= begin
156
+ pos = geocentric_position
157
+ Math.sqrt(pos.x.m * pos.x.m + pos.y.m * pos.y.m)
158
+ end
141
159
  end
142
160
  end
143
161
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ephem"
4
+ require "matrix"
5
+
6
+ module Astronoby
7
+ # Wraps a binary PCK lunar orientation kernel (for example
8
+ # +moon_pa_de440_200625.bpc+) and exposes the rotation from the inertial frame
9
+ # (J2000/ICRF) to the Moon's mean-Earth (ME) body-fixed frame at a given
10
+ # instant.
11
+ #
12
+ # A binary kernel only provides the principal-axis (PA) orientation, and which
13
+ # PA frame it describes is read from the kernel itself. JPL Horizons and IMCCE
14
+ # report selenographic positions in the mean-Earth frame, whose small fixed
15
+ # offset from the principal axes is published in the NAIF lunar frame kernels
16
+ # (the +moon_*.tf+ files) rather than in the binary kernel. That alignment is
17
+ # essentially constant across ephemeris versions (below 0.15 arcsecond), so
18
+ # the DE440 values are applied here.
19
+ class Orientation
20
+ # Principal-axis to mean-Earth alignment from the NAIF DE440 lunar frame
21
+ # kernel (moon_de440_250416.tf): the rotation angles about the z, y and x
22
+ # axes that align the principal-axis frame with the mean-Earth frame.
23
+ MEAN_EARTH_ALIGNMENT = [
24
+ Angle.from_degree_arcseconds(67.8526),
25
+ Angle.from_degree_arcseconds(78.6944),
26
+ Angle.from_degree_arcseconds(0.2785)
27
+ ].freeze
28
+
29
+ # Download a binary PCK orientation kernel.
30
+ #
31
+ # @param name [String] kernel name supported by the Ephem gem
32
+ # @param target [String] destination path
33
+ # @return [Boolean] true if the download was successful
34
+ def self.download(name:, target:)
35
+ ::Ephem::Download.call(name: name, target: target)
36
+ end
37
+
38
+ # Load a binary PCK orientation kernel.
39
+ #
40
+ # @param target [String] path to the +.bpc+ file
41
+ # @return [Astronoby::Orientation]
42
+ # @raise [Astronoby::OrientationError] if the kernel has no orientation data
43
+ def self.load(target)
44
+ new(::Ephem::PCK.open(target))
45
+ end
46
+
47
+ # @param pck [::Ephem::PCK] an opened binary PCK
48
+ def initialize(pck)
49
+ @pck = pck
50
+ @source = orientation_source
51
+ @mean_earth_rotation = mean_earth_rotation
52
+ @start_jd = @pck.segments.map(&:start_jd).min
53
+ @end_jd = @pck.segments.map(&:end_jd).max
54
+ end
55
+
56
+ # Rotation from the inertial frame (J2000/ICRF) to the Moon's mean-Earth
57
+ # body-fixed frame.
58
+ #
59
+ # @param instant [Astronoby::Instant] the time instant
60
+ # @return [Matrix] a 3x3 rotation matrix
61
+ # @raise [Astronoby::OrientationOutOfRangeError] if the kernel does not
62
+ # cover the instant
63
+ def rotation_for(instant)
64
+ @mean_earth_rotation * principal_axis_rotation(instant)
65
+ end
66
+
67
+ # @return [void]
68
+ def close
69
+ @pck.close
70
+ end
71
+
72
+ private
73
+
74
+ # The orientation source for the body frame the kernel describes, read from
75
+ # the kernel rather than assumed.
76
+ def orientation_source
77
+ segment = @pck.segments.first
78
+ raise OrientationError, "Orientation kernel has no segments" unless segment
79
+
80
+ @pck[segment.target]
81
+ end
82
+
83
+ # Inertial -> principal-axis rotation from the binary kernel's Euler angles.
84
+ def principal_axis_rotation(instant)
85
+ terrestrial_time = instant.terrestrial_time
86
+ unless terrestrial_time.between?(@start_jd, @end_jd)
87
+ raise OrientationOutOfRangeError,
88
+ "Orientation kernel covers #{date(@start_jd)} to #{date(@end_jd)}; " \
89
+ "requested #{date(terrestrial_time)}"
90
+ end
91
+
92
+ Matrix[*@source.matrix_at(terrestrial_time)]
93
+ end
94
+
95
+ # Principal-axis -> mean-Earth rotation (constant), about the x, y, z axes.
96
+ def mean_earth_rotation
97
+ about_z, about_y, about_x = MEAN_EARTH_ALIGNMENT
98
+ Rotation.about_x(-about_x) *
99
+ Rotation.about_y(-about_y) *
100
+ Rotation.about_z(-about_z)
101
+ end
102
+
103
+ def date(terrestrial_time)
104
+ Instant.from_terrestrial_time(terrestrial_time).to_time.utc.to_date
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Position
5
+ # @param observer [Astronoby::Observer] the observer
6
+ # @return [Astronoby::Topocentric] the topocentric reference frame
7
+ def observed_by(observer)
8
+ Topocentric.build_from_apparent(
9
+ apparent: apparent,
10
+ observer: observer,
11
+ instant: instant,
12
+ target_body: body
13
+ )
14
+ end
15
+ end
16
+ end