astronoby 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +235 -0
- data/README.md +19 -1
- data/UPGRADING.md +270 -0
- data/docs/README.md +105 -16
- data/docs/altitude_crossings.md +92 -0
- data/docs/angles.md +1 -0
- data/docs/configuration.md +28 -17
- data/docs/coordinates.md +72 -12
- data/docs/deep_sky_bodies.md +46 -1
- data/docs/ephem.md +5 -2
- data/docs/equinoxes_solstices_times.md +4 -3
- data/docs/glossary.md +99 -1
- data/docs/iers.md +40 -0
- data/docs/instant.md +33 -21
- data/docs/lunar_eclipses.md +208 -0
- data/docs/lunar_observation.md +87 -0
- data/docs/moon_phases.md +4 -1
- data/docs/observer.md +20 -6
- data/docs/planetary_phenomena.md +78 -0
- data/docs/reference_frames.md +192 -34
- data/docs/rise_transit_set_times.md +6 -4
- data/docs/solar_system_bodies.md +26 -4
- data/docs/twilight_times.md +43 -24
- data/lib/astronoby/angle.rb +63 -2
- data/lib/astronoby/angles/dms.rb +18 -1
- data/lib/astronoby/angles/hms.rb +14 -1
- data/lib/astronoby/angular_velocity.rb +21 -0
- data/lib/astronoby/bodies/deep_sky_object.rb +6 -1
- data/lib/astronoby/bodies/deep_sky_object_position.rb +35 -38
- data/lib/astronoby/bodies/earth.rb +7 -44
- data/lib/astronoby/bodies/jupiter.rb +10 -0
- data/lib/astronoby/bodies/mars.rb +10 -0
- data/lib/astronoby/bodies/mercury.rb +10 -0
- data/lib/astronoby/bodies/moon.rb +158 -32
- data/lib/astronoby/bodies/neptune.rb +10 -0
- data/lib/astronoby/bodies/saturn.rb +10 -0
- data/lib/astronoby/bodies/solar_system_body.rb +240 -61
- data/lib/astronoby/bodies/sun.rb +79 -4
- data/lib/astronoby/bodies/uranus.rb +10 -0
- data/lib/astronoby/bodies/venus.rb +10 -0
- data/lib/astronoby/body.rb +6 -0
- data/lib/astronoby/cache.rb +13 -0
- data/lib/astronoby/center.rb +84 -0
- data/lib/astronoby/constants.rb +1 -2
- data/lib/astronoby/constellation.rb +9 -1
- data/lib/astronoby/constellations/data.rb +1 -1
- data/lib/astronoby/coordinates/ecliptic.rb +10 -1
- data/lib/astronoby/coordinates/equatorial.rb +79 -13
- data/lib/astronoby/coordinates/geodetic.rb +102 -0
- data/lib/astronoby/coordinates/horizontal.rb +13 -1
- data/lib/astronoby/delta_t.rb +44 -0
- data/lib/astronoby/distance.rb +35 -0
- data/lib/astronoby/duration.rb +116 -0
- data/lib/astronoby/earth_rotation.rb +70 -0
- data/lib/astronoby/equinox_solstice.rb +31 -8
- data/lib/astronoby/errors.rb +11 -0
- data/lib/astronoby/events/altitude_crossing.rb +29 -0
- data/lib/astronoby/events/altitude_crossing_calculator.rb +511 -0
- data/lib/astronoby/events/altitude_extreme.rb +31 -0
- data/lib/astronoby/events/conjunction.rb +51 -0
- data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
- data/lib/astronoby/events/extremum_calculator.rb +23 -176
- data/lib/astronoby/events/greatest_elongation.rb +58 -0
- data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
- data/lib/astronoby/events/lunar_eclipse.rb +111 -0
- data/lib/astronoby/events/lunar_eclipse_calculator.rb +397 -0
- data/lib/astronoby/events/lunar_eclipse_geometry.rb +115 -0
- data/lib/astronoby/events/lunar_eclipse_phase.rb +45 -0
- data/lib/astronoby/events/lunar_eclipse_visibility.rb +296 -0
- data/lib/astronoby/events/opposition.rb +19 -0
- data/lib/astronoby/events/rise_transit_set_calculator.rb +9 -13
- data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
- data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
- data/lib/astronoby/events/twilight_calculator.rb +37 -147
- data/lib/astronoby/events/twilight_event.rb +24 -6
- data/lib/astronoby/events/twilight_events.rb +26 -6
- data/lib/astronoby/extremum_finder.rb +148 -0
- data/lib/astronoby/horizon.rb +40 -0
- data/lib/astronoby/horizontal_transformation.rb +112 -0
- data/lib/astronoby/instant.rb +56 -16
- data/lib/astronoby/libration.rb +25 -0
- data/lib/astronoby/mean_obliquity.rb +8 -0
- data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
- data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
- data/lib/astronoby/nutation.rb +10 -20
- data/lib/astronoby/observer.rb +67 -49
- data/lib/astronoby/orientation.rb +107 -0
- data/lib/astronoby/position.rb +16 -0
- data/lib/astronoby/precession.rb +62 -61
- data/lib/astronoby/reference_frame.rb +80 -9
- data/lib/astronoby/reference_frames/apparent.rb +43 -12
- data/lib/astronoby/reference_frames/astrometric.rb +14 -1
- data/lib/astronoby/reference_frames/geometric.rb +7 -1
- data/lib/astronoby/reference_frames/mean_of_date.rb +19 -1
- data/lib/astronoby/reference_frames/teme.rb +159 -0
- data/lib/astronoby/reference_frames/topocentric.rb +36 -4
- data/lib/astronoby/refraction.rb +26 -5
- data/lib/astronoby/root_finder.rb +90 -0
- data/lib/astronoby/rotation.rb +49 -0
- data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +19 -8
- data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +44 -5
- data/lib/astronoby/time/greenwich_sidereal_time.rb +21 -0
- data/lib/astronoby/time/local_apparent_sidereal_time.rb +21 -0
- data/lib/astronoby/time/local_mean_sidereal_time.rb +21 -0
- data/lib/astronoby/time/local_sidereal_time.rb +24 -0
- data/lib/astronoby/time/sidereal_time.rb +23 -1
- data/lib/astronoby/true_obliquity.rb +4 -0
- data/lib/astronoby/util/maths.rb +8 -0
- data/lib/astronoby/util/time.rb +0 -497
- data/lib/astronoby/vector.rb +10 -0
- data/lib/astronoby/velocity.rb +39 -0
- data/lib/astronoby/version.rb +1 -1
- data/lib/astronoby.rb +30 -0
- metadata +80 -5
|
@@ -1,49 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
-
# Calculates extrema (minima and maxima) in the distance between two
|
|
5
|
-
# celestial bodies over a given time range using adaptive sampling and golden
|
|
6
|
-
# section search refinement.
|
|
7
4
|
class ExtremumCalculator
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# Algorithm parameters
|
|
14
|
-
MIN_SAMPLES_PER_PERIOD = 20
|
|
15
|
-
DUPLICATE_THRESHOLD_DAYS = 0.5
|
|
16
|
-
BOUNDARY_BUFFER_DAYS = 0.01
|
|
17
|
-
|
|
18
|
-
# Orbital periods
|
|
19
|
-
ORBITAL_PERIODS = {
|
|
20
|
-
"Astronoby::Moon" => 27.504339,
|
|
21
|
-
"Astronoby::Mercury" => 87.969,
|
|
22
|
-
"Astronoby::Venus" => 224.701,
|
|
23
|
-
"Astronoby::Earth" => 365.256,
|
|
24
|
-
"Astronoby::Mars" => 686.98,
|
|
25
|
-
"Astronoby::Jupiter" => 4332.59,
|
|
26
|
-
"Astronoby::Saturn" => 10759.22,
|
|
27
|
-
"Astronoby::Uranus" => 30688.5,
|
|
28
|
-
"Astronoby::Neptune" => 60182.0
|
|
29
|
-
}.freeze
|
|
30
|
-
|
|
31
|
-
# @param ephem [::Ephem::SPK] Ephemeris data source
|
|
32
|
-
# @param body [Astronoby::SolarSystemBody] The celestial body to track
|
|
33
|
-
# @param primary_body [Astronoby::SolarSystemBody] The reference body
|
|
34
|
-
# (e.g., Sun for planetary orbits)
|
|
35
|
-
# @param samples_per_period [Integer] Number of samples to take per orbital
|
|
36
|
-
# period
|
|
5
|
+
# @param body [Astronoby::SolarSystemBody] the celestial body to track
|
|
6
|
+
# @param primary_body [Astronoby::SolarSystemBody] the reference body
|
|
7
|
+
# @param ephem [::Ephem::SPK] ephemeris data source
|
|
8
|
+
# @param samples_per_period [Integer] number of samples per orbital period
|
|
37
9
|
def initialize(
|
|
38
10
|
body:,
|
|
39
11
|
primary_body:,
|
|
40
12
|
ephem:,
|
|
41
13
|
samples_per_period: 60
|
|
42
14
|
)
|
|
43
|
-
@ephem = ephem
|
|
44
15
|
@body = body
|
|
45
16
|
@primary_body = primary_body
|
|
46
|
-
@
|
|
17
|
+
@ephem = ephem
|
|
47
18
|
@samples_per_period = samples_per_period
|
|
48
19
|
end
|
|
49
20
|
|
|
@@ -53,8 +24,8 @@ module Astronoby
|
|
|
53
24
|
# @return [Array<Astronoby::ExtremumEvent>] Array of apoapsis events
|
|
54
25
|
def apoapsis_events_between(start_time, end_time)
|
|
55
26
|
find_extrema(
|
|
56
|
-
|
|
57
|
-
|
|
27
|
+
Instant.from_time(start_time).tt,
|
|
28
|
+
Instant.from_time(end_time).tt,
|
|
58
29
|
type: :maximum
|
|
59
30
|
)
|
|
60
31
|
end
|
|
@@ -65,8 +36,8 @@ module Astronoby
|
|
|
65
36
|
# @return [Array<Astronoby::ExtremumEvent>] Array of periapsis events
|
|
66
37
|
def periapsis_events_between(start_time, end_time)
|
|
67
38
|
find_extrema(
|
|
68
|
-
|
|
69
|
-
|
|
39
|
+
Instant.from_time(start_time).tt,
|
|
40
|
+
Instant.from_time(end_time).tt,
|
|
70
41
|
type: :minimum
|
|
71
42
|
)
|
|
72
43
|
end
|
|
@@ -78,21 +49,24 @@ module Astronoby
|
|
|
78
49
|
# @param type [Symbol] :maximum or :minimum
|
|
79
50
|
# @return [Array<Astronoby::ExtremumEvent>] Array of extrema events
|
|
80
51
|
def find_extrema(start_jd, end_jd, type: :maximum)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
.compact
|
|
88
|
-
|
|
89
|
-
# 3: Remove duplicates and boundary artifacts
|
|
90
|
-
refined_extrema = remove_duplicates(refined_extrema)
|
|
91
|
-
filter_boundary_artifacts(refined_extrema, start_jd, end_jd)
|
|
52
|
+
finder.extrema(start_jd, end_jd, type: type).map do |extremum|
|
|
53
|
+
ExtremumEvent.new(
|
|
54
|
+
Instant.from_terrestrial_time(extremum[:jd]),
|
|
55
|
+
extremum[:value]
|
|
56
|
+
)
|
|
57
|
+
end
|
|
92
58
|
end
|
|
93
59
|
|
|
94
60
|
private
|
|
95
61
|
|
|
62
|
+
def finder
|
|
63
|
+
@finder ||= ExtremumFinder.new(
|
|
64
|
+
value_at: method(:distance_at),
|
|
65
|
+
period: @body::ORBITAL_PERIOD,
|
|
66
|
+
samples_per_period: @samples_per_period
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
96
70
|
def distance_at(jd)
|
|
97
71
|
instant = Instant.from_terrestrial_time(jd)
|
|
98
72
|
body_geometric = @body.geometric(ephem: @ephem, instant: instant)
|
|
@@ -102,132 +76,5 @@ module Astronoby
|
|
|
102
76
|
distance_vector = body_geometric.position - primary_geometric.position
|
|
103
77
|
distance_vector.magnitude
|
|
104
78
|
end
|
|
105
|
-
|
|
106
|
-
def find_extrema_candidates(start_jd, end_jd, type)
|
|
107
|
-
samples = collect_samples(start_jd, end_jd)
|
|
108
|
-
find_local_extrema_in_samples(samples, type)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def collect_samples(start_jd, end_jd)
|
|
112
|
-
duration = end_jd - start_jd
|
|
113
|
-
sample_count = calculate_sample_count(duration)
|
|
114
|
-
step = duration / sample_count
|
|
115
|
-
|
|
116
|
-
(0..sample_count).map do |i|
|
|
117
|
-
jd = start_jd + (i * step)
|
|
118
|
-
{jd: jd, value: distance_at(jd)}
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def calculate_sample_count(duration)
|
|
123
|
-
# Adaptive sampling: scale with duration and orbital period
|
|
124
|
-
periods_in_range = duration / @orbital_period
|
|
125
|
-
base_samples = (periods_in_range * @samples_per_period).to_i
|
|
126
|
-
|
|
127
|
-
# Ensure minimum sample density for short ranges
|
|
128
|
-
[base_samples, MIN_SAMPLES_PER_PERIOD].max
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def find_local_extrema_in_samples(samples, type)
|
|
132
|
-
candidates = []
|
|
133
|
-
|
|
134
|
-
# Check each interior point for local extrema
|
|
135
|
-
(1...samples.length - 1).each do |i|
|
|
136
|
-
if local_extremum?(samples, i, type)
|
|
137
|
-
candidates << {
|
|
138
|
-
start_jd: samples[i - 1][:jd],
|
|
139
|
-
end_jd: samples[i + 1][:jd],
|
|
140
|
-
center_jd: samples[i][:jd]
|
|
141
|
-
}
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
candidates
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def local_extremum?(samples, index, type)
|
|
149
|
-
current_val = samples[index][:value].m
|
|
150
|
-
prev_val = samples[index - 1][:value].m
|
|
151
|
-
next_val = samples[index + 1][:value].m
|
|
152
|
-
|
|
153
|
-
if type == :maximum
|
|
154
|
-
current_val > prev_val && current_val > next_val
|
|
155
|
-
else
|
|
156
|
-
current_val < prev_val && current_val < next_val
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def refine_extremum(candidate, type)
|
|
161
|
-
golden_section_search(candidate[:start_jd], candidate[:end_jd], type)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def golden_section_search(a, b, type)
|
|
165
|
-
return nil if b <= a
|
|
166
|
-
|
|
167
|
-
tol = GOLDEN_SECTION_TOLERANCE * (b - a).abs
|
|
168
|
-
|
|
169
|
-
# Initial points using golden ratio
|
|
170
|
-
x1 = a + (1 - INVPHI) * (b - a)
|
|
171
|
-
x2 = a + INVPHI * (b - a)
|
|
172
|
-
|
|
173
|
-
f1 = distance_at(x1).m
|
|
174
|
-
f2 = distance_at(x2).m
|
|
175
|
-
|
|
176
|
-
while (b - a).abs > tol
|
|
177
|
-
should_keep_left = (type == :maximum) ? (f1 > f2) : (f1 < f2)
|
|
178
|
-
|
|
179
|
-
if should_keep_left
|
|
180
|
-
b = x2
|
|
181
|
-
x2 = x1
|
|
182
|
-
f2 = f1
|
|
183
|
-
x1 = a + (1 - INVPHI) * (b - a)
|
|
184
|
-
f1 = distance_at(x1).m
|
|
185
|
-
else
|
|
186
|
-
a = x1
|
|
187
|
-
x1 = x2
|
|
188
|
-
f1 = f2
|
|
189
|
-
x2 = a + INVPHI * (b - a)
|
|
190
|
-
f2 = distance_at(x2).m
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
mid = (a + b) / 2
|
|
195
|
-
ExtremumEvent.new(
|
|
196
|
-
Instant.from_terrestrial_time(mid),
|
|
197
|
-
distance_at(mid)
|
|
198
|
-
)
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def remove_duplicates(extrema)
|
|
202
|
-
return extrema if extrema.length <= 1
|
|
203
|
-
|
|
204
|
-
cleaned = [extrema.first]
|
|
205
|
-
|
|
206
|
-
extrema.each_with_index do |current, i|
|
|
207
|
-
next if i == 0
|
|
208
|
-
|
|
209
|
-
is_duplicate = cleaned.any? do |existing|
|
|
210
|
-
time_diff = (
|
|
211
|
-
current.instant.tt - existing.instant.tt
|
|
212
|
-
).abs
|
|
213
|
-
time_diff < DUPLICATE_THRESHOLD_DAYS
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
cleaned << current unless is_duplicate
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
cleaned
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def filter_boundary_artifacts(extrema, start_jd, end_jd)
|
|
223
|
-
extrema.reject do |extreme|
|
|
224
|
-
start_diff = (extreme.instant.tt - start_jd).abs
|
|
225
|
-
end_diff = (extreme.instant.tt - end_jd).abs
|
|
226
|
-
|
|
227
|
-
too_close_to_start = start_diff < BOUNDARY_BUFFER_DAYS
|
|
228
|
-
too_close_to_end = end_diff < BOUNDARY_BUFFER_DAYS
|
|
229
|
-
too_close_to_start || too_close_to_end
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
79
|
end
|
|
233
80
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class GreatestElongation
|
|
5
|
+
EASTERN = :eastern
|
|
6
|
+
WESTERN = :western
|
|
7
|
+
|
|
8
|
+
# @param instant [Astronoby::Instant] when the greatest elongation occurs
|
|
9
|
+
# @param body [Astronoby::Body] the body reaching greatest elongation
|
|
10
|
+
# @param angle [Astronoby::Angle] the Sun-Earth-body angle at that instant
|
|
11
|
+
# @return [Astronoby::GreatestElongation] a greatest eastern elongation
|
|
12
|
+
def self.eastern(instant:, body:, angle:)
|
|
13
|
+
new(instant: instant, body: body, angle: angle, direction: EASTERN)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param instant [Astronoby::Instant] when the greatest elongation occurs
|
|
17
|
+
# @param body [Astronoby::Body] the body reaching greatest elongation
|
|
18
|
+
# @param angle [Astronoby::Angle] the Sun-Earth-body angle at that instant
|
|
19
|
+
# @return [Astronoby::GreatestElongation] a greatest western elongation
|
|
20
|
+
def self.western(instant:, body:, angle:)
|
|
21
|
+
new(instant: instant, body: body, angle: angle, direction: WESTERN)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Astronoby::Instant] when the greatest elongation occurs
|
|
25
|
+
attr_reader :instant
|
|
26
|
+
|
|
27
|
+
# @return [Astronoby::Body] the body reaching greatest elongation
|
|
28
|
+
attr_reader :body
|
|
29
|
+
|
|
30
|
+
# @return [Astronoby::Angle] the Sun-Earth-body angle at that instant
|
|
31
|
+
attr_reader :angle
|
|
32
|
+
|
|
33
|
+
# @return [Symbol] +EASTERN+ or +WESTERN+
|
|
34
|
+
attr_reader :direction
|
|
35
|
+
|
|
36
|
+
# @param instant [Astronoby::Instant] when the greatest elongation occurs
|
|
37
|
+
# @param body [Astronoby::Body] the body reaching greatest elongation
|
|
38
|
+
# @param angle [Astronoby::Angle] the Sun-Earth-body angle at that instant
|
|
39
|
+
# @param direction [Symbol] +EASTERN+ or +WESTERN+
|
|
40
|
+
def initialize(instant:, body:, angle:, direction:)
|
|
41
|
+
@instant = instant
|
|
42
|
+
@body = body
|
|
43
|
+
@angle = angle
|
|
44
|
+
@direction = direction
|
|
45
|
+
freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] true for a greatest eastern elongation
|
|
49
|
+
def eastern?
|
|
50
|
+
@direction == EASTERN
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Boolean] true for a greatest western elongation
|
|
54
|
+
def western?
|
|
55
|
+
@direction == WESTERN
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class GreatestElongationCalculator
|
|
5
|
+
# @param body [Astronoby::SolarSystemBody] the planet to track
|
|
6
|
+
# @param ephem [::Ephem::SPK] ephemeris data source
|
|
7
|
+
# @param samples_per_period [Integer] number of samples per synodic period
|
|
8
|
+
def initialize(body:, ephem:, samples_per_period: 60)
|
|
9
|
+
@body = body
|
|
10
|
+
@ephem = ephem
|
|
11
|
+
@samples_per_period = samples_per_period
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param start_time [Time] start time
|
|
15
|
+
# @param end_time [Time] end time
|
|
16
|
+
# @return [Array<Astronoby::GreatestElongation>] greatest elongations in
|
|
17
|
+
# the range
|
|
18
|
+
def greatest_elongation_events_between(start_time, end_time)
|
|
19
|
+
finder.extrema(
|
|
20
|
+
Instant.from_time(start_time).tt,
|
|
21
|
+
Instant.from_time(end_time).tt,
|
|
22
|
+
type: :maximum
|
|
23
|
+
).map { |extremum| build_event(extremum) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def finder
|
|
29
|
+
@finder ||= ExtremumFinder.new(
|
|
30
|
+
value_at: ->(jd) { planet_at(jd).elongation },
|
|
31
|
+
period: synodic_period,
|
|
32
|
+
samples_per_period: @samples_per_period
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_event(extremum)
|
|
37
|
+
instant = Instant.from_terrestrial_time(extremum[:jd])
|
|
38
|
+
angle = extremum[:value]
|
|
39
|
+
|
|
40
|
+
if planet_at(instant.tt).eastern?
|
|
41
|
+
GreatestElongation.eastern(instant: instant, body: @body, angle: angle)
|
|
42
|
+
else
|
|
43
|
+
GreatestElongation.western(instant: instant, body: @body, angle: angle)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def planet_at(jd)
|
|
48
|
+
@body.new(instant: Instant.from_terrestrial_time(jd), ephem: @ephem)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def synodic_period
|
|
52
|
+
@synodic_period ||=
|
|
53
|
+
1.0 / ((1.0 / @body::ORBITAL_PERIOD) - (1.0 / Earth::ORBITAL_PERIOD)).abs
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
# A lunar eclipse: a geocentric passage of the Moon through Earth's shadow.
|
|
5
|
+
# Immutable; built by LunarEclipseCalculator.
|
|
6
|
+
#
|
|
7
|
+
# The penumbral phase is always present. The partial phase is present for
|
|
8
|
+
# partial and total eclipses, and the total phase only for total eclipses.
|
|
9
|
+
class LunarEclipse
|
|
10
|
+
PENUMBRAL = :penumbral
|
|
11
|
+
PARTIAL = :partial
|
|
12
|
+
TOTAL = :total
|
|
13
|
+
|
|
14
|
+
# @return [Astronoby::Instant] greatest eclipse, when the Moon's centre is
|
|
15
|
+
# least distant from the axis of Earth's shadow
|
|
16
|
+
attr_reader :instant
|
|
17
|
+
alias_method :greatest_eclipse_instant, :instant
|
|
18
|
+
|
|
19
|
+
# @return [Symbol] +PENUMBRAL+, +PARTIAL+ or +TOTAL+
|
|
20
|
+
attr_reader :kind
|
|
21
|
+
|
|
22
|
+
# @return [Astronoby::LunarEclipseGeometry] the geometry of Earth's shadow
|
|
23
|
+
# and the Moon's place in it at greatest eclipse. The geometry at each
|
|
24
|
+
# contact is on the phases.
|
|
25
|
+
attr_reader :geometry
|
|
26
|
+
|
|
27
|
+
# @return [Astronoby::LunarEclipsePhase] the penumbral phase (always present)
|
|
28
|
+
attr_reader :penumbral
|
|
29
|
+
|
|
30
|
+
# @return [Astronoby::LunarEclipsePhase, nil] the partial phase, present for
|
|
31
|
+
# partial and total eclipses
|
|
32
|
+
attr_reader :partial
|
|
33
|
+
|
|
34
|
+
# @return [Astronoby::LunarEclipsePhase, nil] the total phase (totality),
|
|
35
|
+
# present only for total eclipses
|
|
36
|
+
attr_reader :total
|
|
37
|
+
|
|
38
|
+
# @param instant [Astronoby::Instant] greatest eclipse
|
|
39
|
+
# @param kind [Symbol] +PENUMBRAL+, +PARTIAL+ or +TOTAL+
|
|
40
|
+
# @param geometry [Astronoby::LunarEclipseGeometry] the shadow geometry at
|
|
41
|
+
# greatest eclipse
|
|
42
|
+
# @param penumbral [Astronoby::LunarEclipsePhase] the penumbral phase
|
|
43
|
+
# @param partial [Astronoby::LunarEclipsePhase, nil] the partial phase
|
|
44
|
+
# @param total [Astronoby::LunarEclipsePhase, nil] the total phase
|
|
45
|
+
def initialize(
|
|
46
|
+
instant:,
|
|
47
|
+
kind:,
|
|
48
|
+
geometry:,
|
|
49
|
+
penumbral:,
|
|
50
|
+
partial: nil,
|
|
51
|
+
total: nil
|
|
52
|
+
)
|
|
53
|
+
@instant = instant
|
|
54
|
+
@kind = kind
|
|
55
|
+
@geometry = geometry
|
|
56
|
+
@penumbral = penumbral
|
|
57
|
+
@partial = partial
|
|
58
|
+
@total = total
|
|
59
|
+
freeze
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Float] least distance of the Moon's centre from the axis of
|
|
63
|
+
# Earth's shadow at greatest eclipse, in Earth radii, positive when the
|
|
64
|
+
# Moon passes north of the axis
|
|
65
|
+
def gamma
|
|
66
|
+
sign = @geometry.north_of_axis? ? 1 : -1
|
|
67
|
+
sign * @geometry.axis_distance.m /
|
|
68
|
+
Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [Float] fraction of the Moon's diameter immersed in the umbra at
|
|
72
|
+
# greatest eclipse (negative when the Moon misses the umbra)
|
|
73
|
+
def umbral_magnitude
|
|
74
|
+
@geometry.umbral_magnitude
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [Float] fraction of the Moon's diameter immersed in the penumbra
|
|
78
|
+
# at greatest eclipse
|
|
79
|
+
def penumbral_magnitude
|
|
80
|
+
@geometry.penumbral_magnitude
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Astronoby::Distance] least distance of the Moon's centre from the
|
|
84
|
+
# axis of Earth's shadow at greatest eclipse. This is the unsigned length
|
|
85
|
+
# of which gamma is the value in Earth radii.
|
|
86
|
+
def shadow_axis_distance
|
|
87
|
+
@geometry.axis_distance
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Boolean] true for a penumbral eclipse (the Moon misses the umbra)
|
|
91
|
+
def penumbral?
|
|
92
|
+
@kind == PENUMBRAL
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Boolean] true for a partial eclipse
|
|
96
|
+
def partial?
|
|
97
|
+
@kind == PARTIAL
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Boolean] true for a total eclipse
|
|
101
|
+
def total?
|
|
102
|
+
@kind == TOTAL
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @param observer [Astronoby::Observer] the observer
|
|
106
|
+
# @return [Astronoby::LunarEclipseVisibility] the local circumstances
|
|
107
|
+
def visibility_from(observer)
|
|
108
|
+
LunarEclipseVisibility.new(eclipse: self, observer: observer)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|