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,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Holds arrays of twilight times over a time range.
4
5
  class TwilightEvents
5
- attr_reader :morning_civil_twilight_times,
6
- :evening_civil_twilight_times,
7
- :morning_nautical_twilight_times,
8
- :evening_nautical_twilight_times,
9
- :morning_astronomical_twilight_times,
10
- :evening_astronomical_twilight_times
6
+ # @return [Array<Time>] morning civil twilight times
7
+ attr_reader :morning_civil_twilight_times
11
8
 
9
+ # @return [Array<Time>] evening civil twilight times
10
+ attr_reader :evening_civil_twilight_times
11
+
12
+ # @return [Array<Time>] morning nautical twilight times
13
+ attr_reader :morning_nautical_twilight_times
14
+
15
+ # @return [Array<Time>] evening nautical twilight times
16
+ attr_reader :evening_nautical_twilight_times
17
+
18
+ # @return [Array<Time>] morning astronomical twilight times
19
+ attr_reader :morning_astronomical_twilight_times
20
+
21
+ # @return [Array<Time>] evening astronomical twilight times
22
+ attr_reader :evening_astronomical_twilight_times
23
+
24
+ # @param morning_civil [Array<Time>] morning civil twilight times
25
+ # @param evening_civil [Array<Time>] evening civil twilight times
26
+ # @param morning_nautical [Array<Time>] morning nautical twilight times
27
+ # @param evening_nautical [Array<Time>] evening nautical twilight times
28
+ # @param morning_astronomical [Array<Time>] morning astronomical twilight
29
+ # times
30
+ # @param evening_astronomical [Array<Time>] evening astronomical twilight
31
+ # times
12
32
  def initialize(
13
33
  morning_civil,
14
34
  evening_civil,
@@ -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,9 +94,9 @@ 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
- Rational(delta_t / Constants::SECONDS_PER_DAY) +
99
+ Rational(delta_t, Constants::SECONDS_PER_DAY) +
98
100
  DATETIME_JD_EPOCH_ADJUSTMENT
99
101
  )
100
102
  end
@@ -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,45 @@ 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
- # @return [Numeric] the sidereal time in radians
129
+ # @return [Numeric] the sidereal time in hours
127
130
  def gmst
128
- GreenwichSiderealTime.from_utc(to_time).time
131
+ @memo[:gmst] ||= GreenwichMeanSiderealTime.from_utc(to_time).time
132
+ end
133
+
134
+ # Get the Greenwich Apparent Sidereal Time
135
+ #
136
+ # @return [Numeric] the sidereal time in hours
137
+ def gast
138
+ @memo[:gast] ||= GreenwichApparentSiderealTime.from_utc(to_time).time
139
+ end
140
+
141
+ # Get the Local Mean Sidereal Time
142
+ #
143
+ # @param longitude [Astronoby::Angle] the observer's longitude
144
+ # @return [Numeric] the sidereal time in hours
145
+ def lmst(longitude:)
146
+ LocalMeanSiderealTime.from_utc(to_time, longitude: longitude).time
147
+ end
148
+
149
+ # Get the Local Apparent Sidereal Time
150
+ #
151
+ # @param longitude [Astronoby::Angle] the observer's longitude
152
+ # @return [Numeric] the sidereal time in hours
153
+ def last(longitude:)
154
+ LocalApparentSiderealTime.from_utc(to_time, longitude: longitude).time
129
155
  end
130
156
 
131
157
  # Get the International Atomic Time (TAI)
132
158
  #
133
159
  # @return [Numeric] TAI as Julian Date
134
160
  def tai
135
- @terrestrial_time -
161
+ @memo[:tai] ||= @terrestrial_time -
136
162
  Rational(Constants::TAI_TT_OFFSET, Constants::SECONDS_PER_DAY)
137
163
  end
138
164
 
@@ -144,14 +170,14 @@ module Astronoby
144
170
  # This is technically false, there is a slight difference between TT and
145
171
  # TDB. However, this difference is so small that currenly Astronoby
146
172
  # doesn't support it and consider they are the same value.
147
- @terrestrial_time
173
+ @memo[:tdb] ||= @terrestrial_time
148
174
  end
149
175
 
150
176
  # Get the offset between TT and UTC for this instant
151
177
  #
152
178
  # @return [Numeric] the offset in days
153
179
  def utc_offset
154
- @terrestrial_time - to_time.utc.to_datetime.ajd
180
+ Rational(delta_t / Constants::SECONDS_PER_DAY)
155
181
  end
156
182
 
157
183
  # Calculate hash value for the 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