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
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class ExtremumFinder
5
+ PHI = (1 + Math.sqrt(5)) / 2
6
+ INVPHI = 1 / PHI
7
+ GOLDEN_SECTION_TOLERANCE = 1e-5
8
+
9
+ MIN_SAMPLES_PER_PERIOD = 20
10
+ DUPLICATE_THRESHOLD_DAYS = 0.5
11
+ BOUNDARY_BUFFER_DAYS = 0.01
12
+
13
+ # @param value_at [#call] callable mapping a Julian Date (Terrestrial Time)
14
+ # to a comparable value
15
+ # @param period [Float] the characteristic period of the quantity in days,
16
+ # used to scale the sampling density
17
+ # @param samples_per_period [Integer] number of samples per period
18
+ def initialize(value_at:, period:, samples_per_period: 60)
19
+ @value_at = value_at
20
+ @period = period
21
+ @samples_per_period = samples_per_period
22
+ end
23
+
24
+ # @param start_jd [Float] start time in Julian Date (Terrestrial Time)
25
+ # @param end_jd [Float] end time in Julian Date (Terrestrial Time)
26
+ # @param type [Symbol] +:maximum+ or +:minimum+
27
+ # @return [Array<Hash>] extrema as +{jd: Float, value: Comparable}+, sorted
28
+ # by time
29
+ def extrema(start_jd, end_jd, type: :maximum)
30
+ candidates = find_candidates(start_jd, end_jd, type)
31
+ refined = candidates.map { |candidate| refine(candidate, type) }.compact
32
+ refined = remove_duplicates(refined)
33
+ filter_boundary_artifacts(refined, start_jd, end_jd)
34
+ end
35
+
36
+ private
37
+
38
+ def find_candidates(start_jd, end_jd, type)
39
+ samples = collect_samples(start_jd, end_jd)
40
+ find_local_extrema(samples, type)
41
+ end
42
+
43
+ def collect_samples(start_jd, end_jd)
44
+ duration = end_jd - start_jd
45
+ sample_count = sample_count_for(duration)
46
+ step = duration / sample_count
47
+
48
+ (0..sample_count).map do |i|
49
+ jd = start_jd + (i * step)
50
+ {jd: jd, value: @value_at.call(jd)}
51
+ end
52
+ end
53
+
54
+ def sample_count_for(duration)
55
+ periods_in_range = duration / @period
56
+ base_samples = (periods_in_range * @samples_per_period).to_i
57
+ [base_samples, MIN_SAMPLES_PER_PERIOD].max
58
+ end
59
+
60
+ def find_local_extrema(samples, type)
61
+ candidates = []
62
+
63
+ (1...samples.length - 1).each do |i|
64
+ next unless local_extremum?(samples, i, type)
65
+
66
+ candidates << {
67
+ start_jd: samples[i - 1][:jd],
68
+ end_jd: samples[i + 1][:jd]
69
+ }
70
+ end
71
+
72
+ candidates
73
+ end
74
+
75
+ def local_extremum?(samples, index, type)
76
+ current = samples[index][:value]
77
+ previous = samples[index - 1][:value]
78
+ following = samples[index + 1][:value]
79
+
80
+ if type == :maximum
81
+ current > previous && current > following
82
+ else
83
+ current < previous && current < following
84
+ end
85
+ end
86
+
87
+ def refine(candidate, type)
88
+ golden_section_search(candidate[:start_jd], candidate[:end_jd], type)
89
+ end
90
+
91
+ def golden_section_search(a, b, type)
92
+ return nil if b <= a
93
+
94
+ tol = GOLDEN_SECTION_TOLERANCE * (b - a).abs
95
+
96
+ x1 = a + (1 - INVPHI) * (b - a)
97
+ x2 = a + INVPHI * (b - a)
98
+ f1 = @value_at.call(x1)
99
+ f2 = @value_at.call(x2)
100
+
101
+ while (b - a).abs > tol
102
+ keep_left = (type == :maximum) ? (f1 > f2) : (f1 < f2)
103
+
104
+ if keep_left
105
+ b = x2
106
+ x2 = x1
107
+ f2 = f1
108
+ x1 = a + (1 - INVPHI) * (b - a)
109
+ f1 = @value_at.call(x1)
110
+ else
111
+ a = x1
112
+ x1 = x2
113
+ f1 = f2
114
+ x2 = a + INVPHI * (b - a)
115
+ f2 = @value_at.call(x2)
116
+ end
117
+ end
118
+
119
+ mid = (a + b) / 2
120
+ {jd: mid, value: @value_at.call(mid)}
121
+ end
122
+
123
+ def remove_duplicates(extrema)
124
+ return extrema if extrema.length <= 1
125
+
126
+ cleaned = [extrema.first]
127
+
128
+ extrema.each_with_index do |current, i|
129
+ next if i == 0
130
+
131
+ is_duplicate = cleaned.any? do |existing|
132
+ (current[:jd] - existing[:jd]).abs < DUPLICATE_THRESHOLD_DAYS
133
+ end
134
+
135
+ cleaned << current unless is_duplicate
136
+ end
137
+
138
+ cleaned
139
+ end
140
+
141
+ def filter_boundary_artifacts(extrema, start_jd, end_jd)
142
+ extrema.reject do |extreme|
143
+ (extreme[:jd] - start_jd).abs < BOUNDARY_BUFFER_DAYS ||
144
+ (extreme[:jd] - end_jd).abs < BOUNDARY_BUFFER_DAYS
145
+ end
146
+ end
147
+ end
148
+ end
@@ -62,6 +62,7 @@ module Astronoby
62
62
  end
63
63
  end
64
64
 
65
+ # @return [Numeric] the Terrestrial Time as a Julian Date
65
66
  attr_reader :terrestrial_time
66
67
  alias_method :tt, :terrestrial_time
67
68
  alias_method :julian_date, :terrestrial_time
@@ -77,6 +78,7 @@ module Astronoby
77
78
  end
78
79
 
79
80
  @terrestrial_time = terrestrial_time
81
+ @memo = {}
80
82
  freeze
81
83
  end
82
84
 
@@ -92,7 +94,7 @@ module Astronoby
92
94
  #
93
95
  # @return [DateTime] the UTC time as DateTime
94
96
  def to_datetime
95
- DateTime.jd(
97
+ @memo[:to_datetime] ||= DateTime.jd(
96
98
  @terrestrial_time -
97
99
  Rational(delta_t, Constants::SECONDS_PER_DAY) +
98
100
  DATETIME_JD_EPOCH_ADJUSTMENT
@@ -110,7 +112,7 @@ module Astronoby
110
112
  #
111
113
  # @return [Time] the UTC time
112
114
  def to_time
113
- to_datetime.to_time.utc
115
+ @memo[:to_time] ||= to_datetime.to_time.utc
114
116
  end
115
117
 
116
118
  # Get the ΔT (Delta T) value for this instant
@@ -118,21 +120,22 @@ module Astronoby
118
120
  #
119
121
  # @return [Numeric] Delta T in seconds
120
122
  def delta_t
121
- Util::Time.terrestrial_universal_time_delta(@terrestrial_time)
123
+ @memo[:delta_t] ||=
124
+ Util::Time.terrestrial_universal_time_delta(@terrestrial_time)
122
125
  end
123
126
 
124
127
  # Get the Greenwich Mean Sidereal Time
125
128
  #
126
129
  # @return [Numeric] the sidereal time in hours
127
130
  def gmst
128
- GreenwichMeanSiderealTime.from_utc(to_time).time
131
+ @memo[:gmst] ||= GreenwichMeanSiderealTime.from_utc(to_time).time
129
132
  end
130
133
 
131
134
  # Get the Greenwich Apparent Sidereal Time
132
135
  #
133
136
  # @return [Numeric] the sidereal time in hours
134
137
  def gast
135
- GreenwichApparentSiderealTime.from_utc(to_time).time
138
+ @memo[:gast] ||= GreenwichApparentSiderealTime.from_utc(to_time).time
136
139
  end
137
140
 
138
141
  # Get the Local Mean Sidereal Time
@@ -155,7 +158,7 @@ module Astronoby
155
158
  #
156
159
  # @return [Numeric] TAI as Julian Date
157
160
  def tai
158
- @terrestrial_time -
161
+ @memo[:tai] ||= @terrestrial_time -
159
162
  Rational(Constants::TAI_TT_OFFSET, Constants::SECONDS_PER_DAY)
160
163
  end
161
164
 
@@ -167,7 +170,7 @@ module Astronoby
167
170
  # This is technically false, there is a slight difference between TT and
168
171
  # TDB. However, this difference is so small that currenly Astronoby
169
172
  # doesn't support it and consider they are the same value.
170
- @terrestrial_time
173
+ @memo[:tdb] ||= @terrestrial_time
171
174
  end
172
175
 
173
176
  # Get the offset between TT and UTC for this instant
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # Geocentric libration of the Moon: the small oscillations that let an
5
+ # observer on Earth see slightly more than half of the lunar surface over
6
+ # time. Holds the total (optical + physical) libration in longitude and
7
+ # latitude.
8
+ class Libration
9
+ # @return [Astronoby::Angle] libration in longitude (l), positive towards
10
+ # the Moon's Mare Crisium (east limb)
11
+ attr_reader :longitude
12
+
13
+ # @return [Astronoby::Angle] libration in latitude (b), positive towards
14
+ # the Moon's north pole
15
+ attr_reader :latitude
16
+
17
+ # @param longitude [Astronoby::Angle] libration in longitude
18
+ # @param latitude [Astronoby::Angle] libration in latitude
19
+ def initialize(longitude:, latitude:)
20
+ @longitude = longitude
21
+ @latitude = latitude
22
+ freeze
23
+ end
24
+ end
25
+ end
@@ -4,6 +4,7 @@
4
4
  # as these coefficients work with TT (Terrestrial Time).
5
5
 
6
6
  module Astronoby
7
+ # Computes the mean obliquity of the ecliptic using the IAU 2006 P03 model.
7
8
  class MeanObliquity
8
9
  # Source:
9
10
  # IAU resolution in 2006 in favor of the P03 astronomical model
@@ -12,6 +13,10 @@ module Astronoby
12
13
  EPOCH_OF_REFERENCE = JulianDate::DEFAULT_EPOCH
13
14
  OBLIQUITY_OF_REFERENCE = 23.4392794
14
15
 
16
+ # Computes the mean obliquity of the ecliptic at the given instant.
17
+ #
18
+ # @param instant [Astronoby::Instant] the time instant
19
+ # @return [Astronoby::Angle] the mean obliquity
15
20
  def self.at(instant)
16
21
  return obliquity_of_reference if instant.julian_date == EPOCH_OF_REFERENCE
17
22
 
@@ -32,10 +37,13 @@ module Astronoby
32
37
  )
33
38
  end
34
39
 
40
+ # @return [Astronoby::Angle] the mean obliquity at the reference epoch
41
+ # (J2000.0)
35
42
  def self.obliquity_of_reference
36
43
  Angle.from_degree_arcseconds(obliquity_of_reference_in_arcseconds)
37
44
  end
38
45
 
46
+ # @return [Float] the mean obliquity at J2000.0 in arcseconds
39
47
  def self.obliquity_of_reference_in_arcseconds
40
48
  84381.406
41
49
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # Computes the Moon's libration and the position angle of its axis from a
5
+ # binary PCK lunar orientation kernel, as the arcsecond-accurate counterpart
6
+ # to the analytic Meeus series in MoonPhysicalEphemeris.
7
+ #
8
+ # The libration is the selenographic longitude and latitude of the sub-Earth
9
+ # point, expressed in the Moon's mean-Earth body-fixed frame.
10
+ class MoonOrientationEphemeris
11
+ # @param moon [Astronoby::Moon] the Moon, carrying an orientation kernel
12
+ def initialize(moon)
13
+ @moon = moon
14
+ @instant = moon.instant
15
+ @orientation = moon.orientation
16
+ end
17
+
18
+ # @return [Astronoby::Libration] the libration in longitude and latitude
19
+ def libration
20
+ sub_earth = rotation * moon_to_earth
21
+ Libration.new(
22
+ longitude: Angle.from_radians(Math.atan2(sub_earth[1], sub_earth[0])),
23
+ latitude: Angle.from_radians(
24
+ Math.asin(sub_earth[2] / sub_earth.magnitude)
25
+ )
26
+ )
27
+ end
28
+
29
+ # Position angle of the Moon's axis of rotation, measured eastward from the
30
+ # north point of the disk.
31
+ # @return [Astronoby::Angle] the position angle of the axis
32
+ def position_angle_of_axis
33
+ @moon.apparent.equatorial.position_angle_to(north_pole)
34
+ end
35
+
36
+ private
37
+
38
+ def rotation
39
+ @rotation ||= @orientation.rotation_for(retarded_instant)
40
+ end
41
+
42
+ def retarded_instant
43
+ Instant.from_terrestrial_time(
44
+ @instant.terrestrial_time - light_time_in_days
45
+ )
46
+ end
47
+
48
+ def light_time_in_days
49
+ @moon.astrometric.distance.km /
50
+ Velocity.light_speed.kmps /
51
+ Constants::SECONDS_PER_DAY
52
+ end
53
+
54
+ def moon_to_earth
55
+ x, y, z = @moon.astrometric.position.map(&:m).to_a
56
+ ::Vector[-x, -y, -z]
57
+ end
58
+
59
+ def north_pole
60
+ @north_pole ||= Coordinates::Equatorial.from_position_vector(
61
+ Distance.vector_from_meters(
62
+ Nutation.matrix_for(@instant) *
63
+ Precession.matrix_for(@instant) *
64
+ (rotation.transpose * ::Vector[0, 0, 1])
65
+ )
66
+ )
67
+ end
68
+ end
69
+ end
@@ -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