lunation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +37 -0
  4. data/CHANGELOG.md +3 -0
  5. data/CODE_OF_CONDUCT.md +84 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +114 -0
  8. data/Rakefile +12 -0
  9. data/config/jpl_horizons_lunar_ephemeris_1950_2050.csv +3654 -0
  10. data/config/periodic_terms_earth_nutation.yml +567 -0
  11. data/config/periodic_terms_earth_position_b0.yml +5 -0
  12. data/config/periodic_terms_earth_position_b1.yml +2 -0
  13. data/config/periodic_terms_earth_position_l0.yml +64 -0
  14. data/config/periodic_terms_earth_position_l1.yml +34 -0
  15. data/config/periodic_terms_earth_position_l2.yml +20 -0
  16. data/config/periodic_terms_earth_position_l3.yml +7 -0
  17. data/config/periodic_terms_earth_position_l4.yml +3 -0
  18. data/config/periodic_terms_earth_position_l5.yml +1 -0
  19. data/config/periodic_terms_earth_position_r0.yml +40 -0
  20. data/config/periodic_terms_earth_position_r1.yml +10 -0
  21. data/config/periodic_terms_earth_position_r2.yml +6 -0
  22. data/config/periodic_terms_earth_position_r3.yml +2 -0
  23. data/config/periodic_terms_earth_position_r4.yml +1 -0
  24. data/config/periodic_terms_moon_latitude.yml +300 -0
  25. data/config/periodic_terms_moon_longitude_distance.yml +360 -0
  26. data/lib/lunation/calculation/angle.rb +174 -0
  27. data/lib/lunation/calculation/earth_position_vsop87.rb +80 -0
  28. data/lib/lunation/calculation/horner.rb +26 -0
  29. data/lib/lunation/calculation/moon_illuminated_fraction.rb +51 -0
  30. data/lib/lunation/calculation/moon_position.rb +227 -0
  31. data/lib/lunation/calculation/nutation_and_obliquity.rb +121 -0
  32. data/lib/lunation/calculation/sun_position.rb +124 -0
  33. data/lib/lunation/calculation/timekeeping.rb +99 -0
  34. data/lib/lunation/calculation.rb +170 -0
  35. data/lib/lunation/version.rb +5 -0
  36. data/lib/lunation.rb +11 -0
  37. data/sig/lunation.rbs +4 -0
  38. metadata +82 -0
@@ -0,0 +1,174 @@
1
+ module Lunation
2
+ class Calculation
3
+ class Angle
4
+ attr_reader :radians, :decimal_degrees
5
+
6
+ def initialize(
7
+ radians: nil,
8
+ decimal_degrees: nil,
9
+ decimal_arcseconds: nil,
10
+ arcminutes: nil,
11
+ degrees: nil,
12
+ hours: nil,
13
+ minutes: nil,
14
+ decimal_seconds: nil
15
+ )
16
+ @radians = radians
17
+ @decimal_degrees = decimal_degrees
18
+ @decimal_arcseconds = decimal_arcseconds
19
+ @arcminutes = arcminutes
20
+ @degrees = degrees
21
+ @hours = hours
22
+ @minutes = minutes
23
+ @decimal_seconds = decimal_seconds
24
+ end
25
+
26
+ def decimal_arcminutes
27
+ @decimal_arcminutes ||= (decimal_degrees - degrees) * 60
28
+ end
29
+
30
+ def decimal_arcseconds
31
+ @decimal_arcseconds ||= ((decimal_arcminutes - arcminutes) * 60).round(6)
32
+ end
33
+
34
+ def degrees
35
+ @degrees ||= decimal_degrees.to_i
36
+ end
37
+
38
+ def arcminutes
39
+ @arcminutes ||= decimal_arcminutes.to_i
40
+ end
41
+
42
+ def decimal_hours
43
+ @decimal_hours ||= decimal_degrees / 15.0
44
+ end
45
+
46
+ def decimal_minutes
47
+ @decimal_minutes ||= (decimal_hours - hours) * 60
48
+ end
49
+
50
+ def decimal_seconds
51
+ @decimal_seconds ||= ((decimal_minutes - minutes) * 60).round(6)
52
+ end
53
+
54
+ def hours
55
+ @hours ||= decimal_hours.to_i
56
+ end
57
+
58
+ def minutes
59
+ @minutes ||= decimal_minutes.to_i
60
+ end
61
+
62
+ def +(other)
63
+ self.class.from_decimal_degrees(
64
+ decimal_degrees + other.decimal_degrees,
65
+ normalize: false
66
+ )
67
+ end
68
+
69
+ def -(other)
70
+ self.class.from_decimal_degrees(
71
+ decimal_degrees - other.decimal_degrees,
72
+ normalize: false
73
+ )
74
+ end
75
+
76
+ class << self
77
+ def from_radians(radians, normalize: true)
78
+ new(
79
+ radians: radians,
80
+ decimal_degrees: radians_to_decimal_degrees(radians, normalize: normalize)
81
+ )
82
+ end
83
+
84
+ def from_decimal_degrees(decimal_degrees, normalize: true)
85
+ new(
86
+ decimal_degrees: (normalize ? decimal_degrees % 360 : decimal_degrees).round(9),
87
+ radians: decimal_degrees_to_radians(decimal_degrees, normalize: normalize)
88
+ )
89
+ end
90
+
91
+ def from_degrees(degrees:, arcminutes:, decimal_arcseconds:, normalize: true)
92
+ decimal_degrees = degrees_arcminutes_decimal_arcseconds_to_decimal_degrees(
93
+ degrees,
94
+ arcminutes,
95
+ decimal_arcseconds,
96
+ normalize: normalize
97
+ )
98
+ new(
99
+ decimal_degrees: decimal_degrees,
100
+ degrees: degrees,
101
+ arcminutes: arcminutes,
102
+ decimal_arcseconds: decimal_arcseconds
103
+ )
104
+ end
105
+
106
+ def from_hours_minutes_decimal_seconds(
107
+ hours:,
108
+ minutes:,
109
+ decimal_seconds:,
110
+ normalize: true
111
+ )
112
+ decimal_degrees = hours_minutes_decimal_seconds_to_decimal_degrees(
113
+ hours,
114
+ minutes,
115
+ decimal_seconds,
116
+ normalize: normalize
117
+ )
118
+ new(
119
+ decimal_degrees: decimal_degrees,
120
+ hours: hours,
121
+ minutes: minutes,
122
+ decimal_seconds: decimal_seconds
123
+ )
124
+ end
125
+
126
+ def from_decimal_arcseconds(decimal_arcseconds, normalize: true)
127
+ decimal_degrees = decimal_arcseconds_to_decimal_degrees(
128
+ decimal_arcseconds,
129
+ normalize: normalize
130
+ )
131
+ new(decimal_degrees: decimal_degrees, decimal_arcseconds: decimal_arcseconds)
132
+ end
133
+
134
+ private def radians_to_decimal_degrees(radians, normalize: true)
135
+ result = radians / Math::PI * 180
136
+ (normalize ? result % 360 : result).round(9)
137
+ end
138
+
139
+ private def decimal_degrees_to_radians(decimal_degrees, normalize: true)
140
+ result = (normalize ? decimal_degrees % 360 : decimal_degrees) * Math::PI / 180.0
141
+ result.round(9)
142
+ end
143
+
144
+ private def degrees_arcminutes_decimal_arcseconds_to_decimal_degrees(
145
+ degrees,
146
+ arcminutes,
147
+ decimal_arcseconds,
148
+ normalize: true
149
+ )
150
+ result = degrees + arcminutes / 60.0 + decimal_arcseconds / 3600.0
151
+ (normalize ? result % 360 : result).round(9)
152
+ end
153
+
154
+ private def hours_minutes_decimal_seconds_to_decimal_degrees(
155
+ hours,
156
+ minutes,
157
+ decimal_seconds,
158
+ normalize: true
159
+ )
160
+ result = hours * 15.0 + minutes / 4.0 + decimal_seconds / 240.0
161
+ (normalize ? result % 360 : result).round(9)
162
+ end
163
+
164
+ private def decimal_arcseconds_to_decimal_degrees(
165
+ decimal_arcseconds,
166
+ normalize: true
167
+ )
168
+ result = decimal_arcseconds / 3600.0
169
+ (normalize ? result % 360 : result).round(9)
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,80 @@
1
+ module Lunation
2
+ class Calculation
3
+ module EarthPositionVSOP87
4
+ # rubocop:disable Layout/LineLength
5
+ PERIODIC_TERMS_B0 = YAML.load_file("config/periodic_terms_earth_position_b0.yml").freeze
6
+ PERIODIC_TERMS_B1 = YAML.load_file("config/periodic_terms_earth_position_b1.yml").freeze
7
+ PERIODIC_TERMS_L0 = YAML.load_file("config/periodic_terms_earth_position_l0.yml").freeze
8
+ PERIODIC_TERMS_L1 = YAML.load_file("config/periodic_terms_earth_position_l1.yml").freeze
9
+ PERIODIC_TERMS_L2 = YAML.load_file("config/periodic_terms_earth_position_l2.yml").freeze
10
+ PERIODIC_TERMS_L3 = YAML.load_file("config/periodic_terms_earth_position_l3.yml").freeze
11
+ PERIODIC_TERMS_L4 = YAML.load_file("config/periodic_terms_earth_position_l4.yml").freeze
12
+ PERIODIC_TERMS_L5 = YAML.load_file("config/periodic_terms_earth_position_l5.yml").freeze
13
+ PERIODIC_TERMS_R0 = YAML.load_file("config/periodic_terms_earth_position_r0.yml").freeze
14
+ PERIODIC_TERMS_R1 = YAML.load_file("config/periodic_terms_earth_position_r1.yml").freeze
15
+ PERIODIC_TERMS_R2 = YAML.load_file("config/periodic_terms_earth_position_r2.yml").freeze
16
+ PERIODIC_TERMS_R3 = YAML.load_file("config/periodic_terms_earth_position_r3.yml").freeze
17
+ PERIODIC_TERMS_R4 = YAML.load_file("config/periodic_terms_earth_position_r4.yml").freeze
18
+ # rubocop:enable Layout/LineLength
19
+
20
+ # (L) Ecliptical longitude of the earth (A.A. p. 219, 32.2)
21
+ # UNIT: Angle
22
+ def ecliptic_longitude_of_earth_using_vsop87
23
+ @ecliptic_longitude_of_earth_using_vsop87 ||= begin
24
+ result = Horner.compute(
25
+ time_millennia,
26
+ [
27
+ reduce_periodic_terms(PERIODIC_TERMS_L0),
28
+ reduce_periodic_terms(PERIODIC_TERMS_L1),
29
+ reduce_periodic_terms(PERIODIC_TERMS_L2),
30
+ reduce_periodic_terms(PERIODIC_TERMS_L3),
31
+ reduce_periodic_terms(PERIODIC_TERMS_L4),
32
+ reduce_periodic_terms(PERIODIC_TERMS_L5)
33
+ ]
34
+ )
35
+
36
+ Angle.from_radians(result / 100_000_000.0)
37
+ end
38
+ end
39
+
40
+ # (B) Ecliptical latitude of the earth (A.A. p. 219, 32.2)
41
+ # UNIT: Angle
42
+ def ecliptic_latitude_of_earth_using_vsop87
43
+ @ecliptic_latitude_of_earth_using_vsop87 ||= begin
44
+ first_series = reduce_periodic_terms(PERIODIC_TERMS_B0)
45
+ second_series = reduce_periodic_terms(PERIODIC_TERMS_B1)
46
+
47
+ Angle.from_radians(
48
+ (first_series + second_series * time_millennia) / 100_000_000.0,
49
+ normalize: false
50
+ )
51
+ end
52
+ end
53
+
54
+ # (R) Radius vector (distance to sun) of the earth (A.A. p. 219, 32.2)
55
+ # UNIT: Astronomical Units (AU)
56
+ def radius_vector_of_earth_using_vsop87
57
+ @radius_vector_of_earth_using_vsop87 ||= begin
58
+ result = Horner.compute(
59
+ time_millennia,
60
+ [
61
+ reduce_periodic_terms(PERIODIC_TERMS_R0),
62
+ reduce_periodic_terms(PERIODIC_TERMS_R1),
63
+ reduce_periodic_terms(PERIODIC_TERMS_R2),
64
+ reduce_periodic_terms(PERIODIC_TERMS_R3),
65
+ reduce_periodic_terms(PERIODIC_TERMS_R4)
66
+ ]
67
+ )
68
+
69
+ (result / 100_000_000.0).round(9)
70
+ end
71
+ end
72
+
73
+ private def reduce_periodic_terms(periodic_terms)
74
+ periodic_terms.inject(0.0) do |sum, terms|
75
+ sum + terms[0] * Math.cos(terms[1] + terms[2] * time_millennia)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,26 @@
1
+ module Lunation
2
+ class Calculation
3
+ class Horner
4
+ def initialize(variable, constants)
5
+ @variable = variable
6
+ @constants = constants
7
+ end
8
+
9
+ def compute
10
+ @constants.reverse_each.each_with_index.inject(0.0) do |sum, (constant, index)|
11
+ if index.zero?
12
+ constant
13
+ else
14
+ constant + @variable * sum
15
+ end
16
+ end
17
+ end
18
+
19
+ class << self
20
+ def compute(variable, constants)
21
+ new(variable, constants).compute
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,51 @@
1
+ module Lunation
2
+ class Calculation
3
+ module MoonIlluminatedFraction
4
+ # (k) illuminated fraction of the moon (48.1, A.A. p. 345)
5
+ # UNIT: fraction (decimal)
6
+ def moon_illuminated_fraction
7
+ @moon_illuminated_fraction ||=
8
+ ((1 + Math.cos(moon_phase_angle.radians)) / 2.0).round(4)
9
+ end
10
+
11
+ # (i) phase angle of the moon (48.3, A.A. p. 346)
12
+ # UNIT: Angle
13
+ def moon_phase_angle
14
+ @moon_phase_angle ||= begin
15
+ numerator = distance_between_earth_and_sun_in_kilometers * Math.sin(moon_elongation_from_sun.radians)
16
+ denominator = distance_between_earth_and_moon -
17
+ distance_between_earth_and_sun_in_kilometers * Math.cos(moon_elongation_from_sun.radians)
18
+ Angle.from_radians(Math.atan2(numerator, denominator))
19
+ end
20
+ end
21
+
22
+ # (ψ) geocentric elongation of the moon (48.2, A.A. p. 345)
23
+ # UNIT: Angle
24
+ def moon_elongation_from_sun
25
+ @moon_elongation_from_sun ||= begin
26
+ result = Math.sin(sun_declination.radians) *
27
+ Math.sin(moon_declination.radians) +
28
+ Math.cos(sun_declination.radians) *
29
+ Math.cos(moon_declination.radians) *
30
+ Math.cos((sun_right_ascension - moon_right_ascension).radians)
31
+ Angle.from_radians(Math.acos(result))
32
+ end
33
+ end
34
+
35
+ # (χ) Position angle of the moon's bright limb (48.5, A.A. p. 346)
36
+ # UNIT: Angle
37
+ def moon_position_angle_of_bright_limb
38
+ @moon_position_angle_of_bright_limb ||= begin
39
+ numerator = Math.cos(sun_declination.radians) *
40
+ Math.sin((sun_right_ascension - moon_right_ascension).radians)
41
+ denominator = Math.sin(sun_declination.radians) *
42
+ Math.cos(moon_declination.radians) -
43
+ Math.cos(sun_declination.radians) *
44
+ Math.sin(moon_declination.radians) *
45
+ Math.cos((sun_right_ascension - moon_right_ascension).radians)
46
+ Angle.from_radians(Math.atan2(numerator, denominator))
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,227 @@
1
+ require_relative "nutation_and_obliquity"
2
+
3
+ module Lunation
4
+ class Calculation
5
+ module MoonPosition
6
+ # rubocop:disable Layout/LineLength
7
+ LONGITUDE_AND_DISTANCE_OF_MOON_PERIODIC_TERMS = YAML.load_file("config/periodic_terms_moon_longitude_distance.yml").freeze
8
+ LATITUDE_OF_MOON_PERIODIC_TERMS = YAML.load_file("config/periodic_terms_moon_latitude.yml").freeze
9
+ MOON_MEAN_LONGITUDE_CONSTANTS = [218.3164477, 481_267.88123421, -0.0015786, 1 / 538_841.0, -1 / 65_194_000.0].freeze
10
+ MOON_MEAN_ELONGATION_CONSTANTS = [297.8501921, 445_267.1114034, -0.0018819, 1 / 545_868.0, -1 / 113_065_000.0].freeze
11
+ MOON_MEAN_ANOMALY_CONSTANTS = [134.9633964, 477_198.8675055, 0.0087414, 1 / 69_699.0, -1 / 14_712_000.0].freeze
12
+ MOON_ARGUMENT_OF_LATITUDE_CONSTANTS = [93.2720950, 483_202.0175233, -0.0036539, -1 / 3_526_000.0, 1 / 863_310_000.0].freeze
13
+ CORRECTION_ECCENTRICITY_OF_EARTH_CONSTANTS = [1, -0.002516, -0.0000074].freeze
14
+ FIXED_DISTANCE_BETWEEN_EARTH_AND_MOON = 385_000.56
15
+ RADIUS_OF_EARTH = 6378.14
16
+ # rubocop:enable Layout/LineLength
17
+
18
+ # (L') Moon mean_longitude (47.1, A.A. p. 338)
19
+ # UNIT: Angle
20
+ def moon_mean_longitude
21
+ @moon_mean_longitude ||=
22
+ Angle.from_decimal_degrees(Horner.compute(time, MOON_MEAN_LONGITUDE_CONSTANTS))
23
+ end
24
+
25
+ # (D) Moon mean_elongation (47.2, A.A. p. 338)
26
+ # UNIT: Angle
27
+ def moon_mean_elongation_from_sun_high_precision
28
+ @moon_mean_elongation_from_sun_high_precision ||=
29
+ Angle.from_decimal_degrees(Horner.compute(time, MOON_MEAN_ELONGATION_CONSTANTS))
30
+ end
31
+
32
+ # (M') Moon mean_anomaly (47.4, A.A. p. 338)
33
+ # UNIT: Angle
34
+ def moon_mean_anomaly_high_precision
35
+ @moon_mean_anomaly_high_precision ||=
36
+ Angle.from_decimal_degrees(Horner.compute(time, MOON_MEAN_ANOMALY_CONSTANTS))
37
+ end
38
+
39
+ # (F) Moon argument_of_latitude (47.5, A.A. p. 338)
40
+ # UNIT: Angle
41
+ def moon_argument_of_latitude_high_precision
42
+ @moon_argument_of_latitude_high_precision ||= begin
43
+ result = Horner.compute(time, MOON_ARGUMENT_OF_LATITUDE_CONSTANTS)
44
+ Angle.from_decimal_degrees(result)
45
+ end
46
+ end
47
+
48
+ # (A1) Venus correction (A.A. p. 338)
49
+ # UNIT: Angle
50
+ def correction_venus
51
+ @correction_venus ||= Angle.from_decimal_degrees(119.75 + 131.849 * time)
52
+ end
53
+
54
+ # (A2) Jupiter correction (A.A. p. 338)
55
+ # UNIT: Angle
56
+ def correction_jupiter
57
+ @correction_jupiter ||= Angle.from_decimal_degrees(53.09 + 4_792_64.29 * time)
58
+ end
59
+
60
+ # (A3) latitude correction (A.A. p. 338)
61
+ # UNIT: Angle
62
+ def correction_latitude
63
+ @correction_latitude ||= Angle.from_decimal_degrees(313.45 + 481_266.484 * time)
64
+ end
65
+
66
+ # (E) Earth eccentricity (47.6 A.A. p. 338)
67
+ def correction_eccentricity_of_earth
68
+ @correction_eccentricity_of_earth ||= begin
69
+ result = Horner.compute(time, CORRECTION_ECCENTRICITY_OF_EARTH_CONSTANTS)
70
+ result.round(6)
71
+ end
72
+ end
73
+
74
+ # (Σl) Moon longitude (A.A. p. 338)
75
+ # UNIT: decimal degrees
76
+ def moon_heliocentric_longitude
77
+ @moon_heliocentric_longitude ||= begin
78
+ result = LONGITUDE_AND_DISTANCE_OF_MOON_PERIODIC_TERMS.inject(0.0) do |acc, elem|
79
+ sine_argument = Angle.from_decimal_degrees(
80
+ elem["moon_mean_elongation"] * moon_mean_elongation_from_sun_high_precision.decimal_degrees +
81
+ elem["sun_mean_anomaly"] * sun_mean_anomaly2.decimal_degrees +
82
+ elem["moon_mean_anomaly"] * moon_mean_anomaly_high_precision.decimal_degrees +
83
+ elem["moon_argument_of_latitude"] * moon_argument_of_latitude_high_precision.decimal_degrees
84
+ )
85
+
86
+ if elem["sine_coefficient"].nil?
87
+ next acc
88
+ elsif [1, -1].include?(elem["sun_mean_anomaly"])
89
+ acc + elem["sine_coefficient"] * correction_eccentricity_of_earth * Math.sin(sine_argument.radians)
90
+ elsif [-2, 2].include?(elem["sun_mean_anomaly"])
91
+ acc + elem["sine_coefficient"] * correction_eccentricity_of_earth**2 * Math.sin(sine_argument.radians)
92
+ else
93
+ acc + elem["sine_coefficient"] * Math.sin(sine_argument.radians)
94
+ end
95
+ end + 3958 * Math.sin(correction_venus.radians) +
96
+ 1962 * Math.sin((moon_mean_longitude - moon_argument_of_latitude_high_precision).radians) +
97
+ 318 * Math.sin(correction_jupiter.radians)
98
+ result.round
99
+ end
100
+ end
101
+
102
+ # (Σb) Moon latitude (A.A. p. 338)
103
+ # UNIT: decimal degrees
104
+ def moon_heliocentric_latitude
105
+ @moon_heliocentric_latitude ||= begin
106
+ result = LATITUDE_OF_MOON_PERIODIC_TERMS.inject(0.0) do |acc, elem|
107
+ sine_argument = Angle.from_decimal_degrees(
108
+ elem["moon_mean_elongation"] * moon_mean_elongation_from_sun_high_precision.decimal_degrees +
109
+ elem["sun_mean_anomaly"] * sun_mean_anomaly2.decimal_degrees +
110
+ elem["moon_mean_anomaly"] * moon_mean_anomaly_high_precision.decimal_degrees +
111
+ elem["moon_argument_of_latitude"] * moon_argument_of_latitude_high_precision.decimal_degrees
112
+ )
113
+
114
+ if elem["sine_coefficient"].nil?
115
+ next acc
116
+ elsif [1, -1].include?(elem["sun_mean_anomaly"])
117
+ acc + elem["sine_coefficient"] * correction_eccentricity_of_earth * Math.sin(sine_argument.radians)
118
+ elsif [-2, 2].include?(elem["sun_mean_anomaly"])
119
+ acc + elem["sine_coefficient"] * correction_eccentricity_of_earth**2 * Math.sin(sine_argument.radians)
120
+ else
121
+ acc + elem["sine_coefficient"] * Math.sin(sine_argument.radians)
122
+ end
123
+ end - 2235 * Math.sin(moon_mean_longitude.radians) +
124
+ 382 * Math.sin(correction_latitude.radians) +
125
+ 175 * Math.sin((correction_venus - moon_argument_of_latitude_high_precision).radians) +
126
+ 175 * Math.sin((correction_venus + moon_argument_of_latitude_high_precision).radians) +
127
+ 127 * Math.sin((moon_mean_longitude - moon_mean_anomaly_high_precision).radians) +
128
+ -115 * Math.sin((moon_mean_longitude + moon_mean_anomaly_high_precision).radians)
129
+ result.round
130
+ end
131
+ end
132
+
133
+ # (Σr) Moon distance (A.A. p. 338)
134
+ # UNIT: 1000 km
135
+ def moon_heliocentric_distance
136
+ @moon_heliocentric_distance ||= begin
137
+ result = LONGITUDE_AND_DISTANCE_OF_MOON_PERIODIC_TERMS.inject(0.0) do |acc, elem|
138
+ cosine_argument = Angle.from_decimal_degrees(
139
+ elem["moon_mean_elongation"] * moon_mean_elongation_from_sun_high_precision.decimal_degrees +
140
+ elem["sun_mean_anomaly"] * sun_mean_anomaly2.decimal_degrees +
141
+ elem["moon_mean_anomaly"] * moon_mean_anomaly_high_precision.decimal_degrees +
142
+ elem["moon_argument_of_latitude"] * moon_argument_of_latitude_high_precision.decimal_degrees
143
+ )
144
+
145
+ if elem["cosine_coefficient"].nil?
146
+ next acc
147
+ elsif [1, -1].include?(elem["sun_mean_anomaly"])
148
+ acc + elem["cosine_coefficient"] * correction_eccentricity_of_earth * Math.cos(cosine_argument.radians)
149
+ elsif [-2, 2].include?(elem["sun_mean_anomaly"])
150
+ acc + elem["cosine_coefficient"] * correction_eccentricity_of_earth**2 * Math.cos(cosine_argument.radians)
151
+ else
152
+ acc + elem["cosine_coefficient"] * Math.cos(cosine_argument.radians)
153
+ end
154
+ end
155
+ result.round
156
+ end
157
+ end
158
+
159
+ # (λ) ecliptical longitude (A.A. p. 342)
160
+ # UNIT: Angle
161
+ def moon_ecliptic_longitude
162
+ @moon_ecliptic_longitude ||= Angle.from_decimal_degrees(
163
+ moon_mean_longitude.decimal_degrees + moon_heliocentric_longitude / 1_000_000.0
164
+ )
165
+ end
166
+
167
+ # (β) ecliptical latitude (A.A. p. 342)
168
+ # UNIT: Angle
169
+ def moon_ecliptic_latitude
170
+ @moon_ecliptic_latitude ||= Angle.from_decimal_degrees(
171
+ moon_heliocentric_latitude / 1_000_000.0,
172
+ normalize: false
173
+ )
174
+ end
175
+
176
+ # (Δ) Earth-moon distance (in kilometers) (A.A. p. 342)
177
+ # UNIT: kilometers (km)
178
+ def distance_between_earth_and_moon
179
+ @distance_between_earth_and_moon ||= begin
180
+ result = FIXED_DISTANCE_BETWEEN_EARTH_AND_MOON +
181
+ (moon_heliocentric_distance / 1_000.0)
182
+ result.round(1)
183
+ end
184
+ end
185
+
186
+ # (π) Moon equitorial horizontal parallax (A.A. p. 337)
187
+ # UNIT: Angle
188
+ def equatorial_horizontal_parallax
189
+ @equatorial_horizontal_parallax ||= Angle.from_radians(
190
+ Math.asin(RADIUS_OF_EARTH / distance_between_earth_and_moon)
191
+ )
192
+ end
193
+
194
+ # (apparent λ) Moon apparent longitude (A.A. p. 343)
195
+ # UNIT: Angle
196
+ def moon_apparent_ecliptic_longitude
197
+ @moon_apparent_ecliptic_longitude ||= moon_ecliptic_longitude + nutation_in_longitude
198
+ end
199
+
200
+ # (α) geocentric (apparent) right ascension of the moon (13.3 A.A. p. 93)
201
+ # UNIT: Angle
202
+ def moon_right_ascension
203
+ @moon_right_ascension ||= begin
204
+ numerator = Math.sin(moon_apparent_ecliptic_longitude.radians) *
205
+ Math.cos(obliquity_of_ecliptic.radians) -
206
+ Math.tan(moon_ecliptic_latitude.radians) *
207
+ Math.sin(obliquity_of_ecliptic.radians)
208
+ denominator = Math.cos(moon_apparent_ecliptic_longitude.radians)
209
+ Angle.from_radians(Math.atan2(numerator, denominator))
210
+ end
211
+ end
212
+
213
+ # (δ) geocentric (apparent) declination of the moon (13.4) A.A. p. 93
214
+ # UNIT: Angle
215
+ def moon_declination
216
+ @moon_declination ||= begin
217
+ result = Math.sin(moon_ecliptic_latitude.radians) *
218
+ Math.cos(obliquity_of_ecliptic.radians) +
219
+ Math.cos(moon_ecliptic_latitude.radians) *
220
+ Math.sin(obliquity_of_ecliptic.radians) *
221
+ Math.sin(moon_apparent_ecliptic_longitude.radians)
222
+ Angle.from_radians(Math.asin(result))
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,121 @@
1
+ module Lunation
2
+ class Calculation
3
+ module NutationAndObliquity
4
+ # rubocop:disable Layout/LineLength
5
+ NUTATION_IN_OBLIQUITY_PERIODIC_TERMS = YAML.load_file("config/periodic_terms_earth_nutation.yml").freeze
6
+ ECLIPTIC_MEAN_OBLIQUITY_CONSTANTS = [21.448, -4680.93, -1.55, 1_999.25, -51.38, -249.67, -39.05, 7.12, 27.87, 5.79, 2.45].freeze
7
+ MOON_MEAN_ELONGATION_FROM_SUN_CONSTANTS = [297.85036, 445_267.111480, -0.0019142, 1 / 189_474.0].freeze
8
+ SUN_MEAN_ANOMALY_CONSTANTS = [357.52772, 35_999.050340, -0.0001603, -1 / 300_000.0].freeze
9
+ MOON_MEAN_ANOMALY_CONSTANTS = [134.96298, 477_198.867398, 0.0086972, 1 / 56_250.0].freeze
10
+ MOON_ARGUMENT_OF_LATITUDE_CONSTANTS = [93.27191, 483_202.017538, -0.0036825, 1 / 327_270.0].freeze
11
+ LONGITUDE_OF_THE_ASCENDING_NODE_CONSTANTS = [125.04452, -1934.136261, 0.0020708, 1 / 450_000.0].freeze
12
+ # rubocop:enable Layout/LineLength
13
+
14
+ # (D) Mean elongation of the moon from the sun (A.A. p. 144)
15
+ # UNIT: Angle
16
+ def moon_mean_elongation_from_sun
17
+ @moon_mean_elongation_from_sun ||= begin
18
+ result = Horner.compute(time, MOON_MEAN_ELONGATION_FROM_SUN_CONSTANTS)
19
+ Angle.from_decimal_degrees(result)
20
+ end
21
+ end
22
+
23
+ # (M) Sun mean_anomaly (A.A. p. 144)
24
+ # UNIT: Angle
25
+ def sun_mean_anomaly
26
+ @sun_mean_anomaly ||= begin
27
+ result = Horner.compute(time, SUN_MEAN_ANOMALY_CONSTANTS)
28
+ Angle.from_decimal_degrees(result)
29
+ end
30
+ end
31
+
32
+ # (M') Moon mean_anomaly (A.A. p. 149)
33
+ # UNIT: Angle
34
+ def moon_mean_anomaly
35
+ @moon_mean_anomaly ||= begin
36
+ result = Horner.compute(time, MOON_MEAN_ANOMALY_CONSTANTS)
37
+ Angle.from_decimal_degrees(result)
38
+ end
39
+ end
40
+
41
+ # (F) Moon argument_of_latitude (A.A. p. 144)
42
+ # UNIT: Angle
43
+ def moon_argument_of_latitude
44
+ @moon_argument_of_latitude ||= begin
45
+ result = Horner.compute(time, MOON_ARGUMENT_OF_LATITUDE_CONSTANTS)
46
+ Angle.from_decimal_degrees(result)
47
+ end
48
+ end
49
+
50
+ # (Ω) Longitude of the ascending node of the Moon's mean orbit on the ecliptic,
51
+ # measured from the mean equinox of the date (A.A. p. 144)
52
+ # UNIT: Angle
53
+ def longitude_of_ascending_node
54
+ @longitude_of_ascending_node ||= begin
55
+ result = Horner.compute(time, LONGITUDE_OF_THE_ASCENDING_NODE_CONSTANTS)
56
+ Angle.from_decimal_degrees(result)
57
+ end
58
+ end
59
+
60
+ # (Δψ) nutation in longitude (A.A. p. 144)
61
+ # UNIT: Angle
62
+ def nutation_in_longitude
63
+ @nutation_in_longitude ||= begin
64
+ result = NUTATION_IN_OBLIQUITY_PERIODIC_TERMS.inject(0.0) do |acc, elem|
65
+ argument = Angle.from_decimal_degrees(
66
+ elem["moon_mean_elongation"] * moon_mean_elongation_from_sun.decimal_degrees +
67
+ elem["sun_mean_anomaly"] * sun_mean_anomaly.decimal_degrees +
68
+ elem["moon_mean_anomaly"] * moon_mean_anomaly.decimal_degrees +
69
+ elem["moon_argument_of_latitude"] * moon_argument_of_latitude.decimal_degrees +
70
+ elem["longitude_of_ascending_node"] * longitude_of_ascending_node.decimal_degrees
71
+ )
72
+ coefficient = elem["sine_coefficient_first_term"] + elem["sine_coefficient_second_term"] * time
73
+ acc + coefficient * Math.sin(argument.radians)
74
+ end / 10_000.0
75
+ Angle.from_decimal_arcseconds(result)
76
+ end
77
+ end
78
+
79
+ # (Δε) nutation in obliquity (A.A. p. 144)
80
+ # UNIT: Angle
81
+ def nutation_in_obliquity
82
+ @nutation_in_obliquity ||= begin
83
+ result = NUTATION_IN_OBLIQUITY_PERIODIC_TERMS.inject(0.0) do |acc, elem|
84
+ argument = Angle.from_decimal_degrees(
85
+ elem["moon_mean_elongation"] * moon_mean_elongation_from_sun.decimal_degrees +
86
+ elem["sun_mean_anomaly"] * sun_mean_anomaly.decimal_degrees +
87
+ elem["moon_mean_anomaly"] * moon_mean_anomaly.decimal_degrees +
88
+ elem["moon_argument_of_latitude"] * moon_argument_of_latitude.decimal_degrees +
89
+ elem["longitude_of_ascending_node"] * longitude_of_ascending_node.decimal_degrees
90
+ )
91
+ coefficient = elem["cosine_coefficient_first_term"] + elem["cosine_coefficient_second_term"] * time
92
+ acc + coefficient * Math.cos(argument.radians)
93
+ end / 10_000.0
94
+ Angle.from_decimal_arcseconds(result)
95
+ end
96
+ end
97
+
98
+ # (ε0) mean obliquity of the ecliptic (22.3, A.A. p. 147)
99
+ # UNIT: Angle
100
+ def mean_obliquity_of_ecliptic
101
+ @mean_obliquity_of_ecliptic ||= begin
102
+ decimal_arcseconds = Horner.compute(
103
+ time_myriads,
104
+ ECLIPTIC_MEAN_OBLIQUITY_CONSTANTS
105
+ )
106
+ Angle.from_degrees(
107
+ degrees: 23.0,
108
+ arcminutes: 26.0,
109
+ decimal_arcseconds: decimal_arcseconds
110
+ )
111
+ end
112
+ end
113
+
114
+ # (ε) true obliquity of the ecliptic (A.A. p. 147)
115
+ # UNIT: Angle
116
+ def obliquity_of_ecliptic
117
+ @obliquity_of_ecliptic ||= mean_obliquity_of_ecliptic + nutation_in_obliquity
118
+ end
119
+ end
120
+ end
121
+ end