lunation 0.1.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 (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