astronoby 0.8.0 → 0.9.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/CHANGELOG.md +58 -0
- data/README.md +6 -4
- data/UPGRADING.md +25 -0
- data/docs/README.md +30 -2
- data/docs/angles.md +1 -1
- data/docs/coordinates.md +1 -1
- data/docs/deep_sky_bodies.md +101 -0
- data/docs/ephem.md +1 -1
- data/docs/instant.md +1 -1
- data/docs/moon_phases.md +1 -1
- data/docs/observer.md +1 -1
- data/docs/reference_frames.md +1 -1
- data/docs/rise_transit_set_times.md +5 -5
- data/docs/{celestial_bodies.md → solar_system_bodies.md} +1 -1
- data/lib/astronoby/angle.rb +6 -2
- data/lib/astronoby/angular_velocity.rb +76 -0
- data/lib/astronoby/bodies/deep_sky_object.rb +44 -0
- data/lib/astronoby/bodies/deep_sky_object_position.rb +127 -0
- data/lib/astronoby/bodies/earth.rb +5 -1
- data/lib/astronoby/bodies/moon.rb +21 -0
- data/lib/astronoby/bodies/solar_system_body.rb +25 -0
- data/lib/astronoby/cache.rb +1 -0
- data/lib/astronoby/constants.rb +7 -2
- data/lib/astronoby/coordinates/equatorial.rb +2 -5
- data/lib/astronoby/distance.rb +6 -0
- data/lib/astronoby/events/extremum_calculator.rb +233 -0
- data/lib/astronoby/events/extremum_event.rb +15 -0
- data/lib/astronoby/events/rise_transit_set_calculator.rb +9 -6
- data/lib/astronoby/events/twilight_calculator.rb +1 -1
- data/lib/astronoby/instant.rb +27 -4
- data/lib/astronoby/reference_frames/apparent.rb +0 -10
- data/lib/astronoby/reference_frames/topocentric.rb +1 -1
- data/lib/astronoby/stellar_propagation.rb +162 -0
- data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +22 -0
- data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +64 -0
- data/lib/astronoby/time/greenwich_sidereal_time.rb +20 -58
- data/lib/astronoby/time/local_apparent_sidereal_time.rb +42 -0
- data/lib/astronoby/time/local_mean_sidereal_time.rb +42 -0
- data/lib/astronoby/time/local_sidereal_time.rb +35 -26
- data/lib/astronoby/time/sidereal_time.rb +42 -0
- data/lib/astronoby/util/time.rb +61 -43
- data/lib/astronoby/velocity.rb +5 -0
- data/lib/astronoby/version.rb +1 -1
- data/lib/astronoby.rb +11 -0
- metadata +14 -2
|
@@ -15,12 +15,16 @@ module Astronoby
|
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def phase_angle
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
18
22
|
private
|
|
19
23
|
|
|
20
24
|
# Attributes that require Sun data like phase angle or magnitude are not
|
|
21
25
|
# applicable for Earth.
|
|
22
26
|
def requires_sun_data?
|
|
23
|
-
|
|
27
|
+
true
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def compute_astrometric(_ephem)
|
|
@@ -42,6 +42,27 @@ module Astronoby
|
|
|
42
42
|
mean_elongation.degrees / Constants::DEGREES_PER_CIRCLE
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# @return [Boolean] True if the body is approaching the primary
|
|
46
|
+
# body (Earth), false otherwise.
|
|
47
|
+
def approaching_primary?
|
|
48
|
+
relative_position =
|
|
49
|
+
(geometric.position - @earth_geometric.position).map(&:m)
|
|
50
|
+
relative_velocity =
|
|
51
|
+
(geometric.velocity - @earth_geometric.velocity).map(&:mps)
|
|
52
|
+
radial_velocity_component = Astronoby::Util::Maths
|
|
53
|
+
.dot_product(relative_position, relative_velocity)
|
|
54
|
+
distance = Math.sqrt(
|
|
55
|
+
Astronoby::Util::Maths.dot_product(relative_position, relative_position)
|
|
56
|
+
)
|
|
57
|
+
radial_velocity_component / distance < 0
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Boolean] True if the body is receding from the primary
|
|
61
|
+
# body (Earth), false otherwise.
|
|
62
|
+
def receding_from_primary?
|
|
63
|
+
!approaching_primary?
|
|
64
|
+
end
|
|
65
|
+
|
|
45
66
|
private
|
|
46
67
|
|
|
47
68
|
# Source:
|
|
@@ -19,6 +19,10 @@ module Astronoby
|
|
|
19
19
|
|
|
20
20
|
attr_reader :geometric, :instant
|
|
21
21
|
|
|
22
|
+
def self.at(instant, ephem:)
|
|
23
|
+
new(ephem: ephem, instant: instant)
|
|
24
|
+
end
|
|
25
|
+
|
|
22
26
|
def self.geometric(ephem:, instant:)
|
|
23
27
|
compute_geometric(ephem: ephem, instant: instant)
|
|
24
28
|
end
|
|
@@ -202,6 +206,27 @@ module Astronoby
|
|
|
202
206
|
end
|
|
203
207
|
end
|
|
204
208
|
|
|
209
|
+
# @return [Boolean] True if the body is approaching the primary
|
|
210
|
+
# body (Sun), false otherwise.
|
|
211
|
+
def approaching_primary?
|
|
212
|
+
relative_position =
|
|
213
|
+
(geometric.position - @sun.geometric.position).map(&:m)
|
|
214
|
+
relative_velocity =
|
|
215
|
+
(geometric.velocity - @sun.geometric.velocity).map(&:mps)
|
|
216
|
+
radial_velocity_component = Astronoby::Util::Maths
|
|
217
|
+
.dot_product(relative_position, relative_velocity)
|
|
218
|
+
distance = Math.sqrt(
|
|
219
|
+
Astronoby::Util::Maths.dot_product(relative_position, relative_position)
|
|
220
|
+
)
|
|
221
|
+
radial_velocity_component / distance < 0
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# @return [Boolean] True if the body is receding from the primary
|
|
225
|
+
# body (Sun), false otherwise.
|
|
226
|
+
def receding_from_primary?
|
|
227
|
+
!approaching_primary?
|
|
228
|
+
end
|
|
229
|
+
|
|
205
230
|
private
|
|
206
231
|
|
|
207
232
|
# By default, Solar System bodies expose attributes that are dependent on
|
data/lib/astronoby/cache.rb
CHANGED
data/lib/astronoby/constants.rb
CHANGED
|
@@ -14,19 +14,24 @@ module Astronoby
|
|
|
14
14
|
|
|
15
15
|
SECONDS_PER_MINUTE = 60.0
|
|
16
16
|
MINUTES_PER_HOUR = 60.0
|
|
17
|
-
MINUTES_PER_DEGREE = 60.0
|
|
18
|
-
|
|
19
17
|
SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR
|
|
20
18
|
SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY
|
|
19
|
+
SECONDS_PER_JULIAN_YEAR = SECONDS_PER_DAY * DAYS_PER_JULIAN_YEAR
|
|
21
20
|
RADIAN_PER_HOUR = Math::PI / 12.0
|
|
22
21
|
MICROSECOND_IN_DAYS = 1.0 / SECONDS_PER_DAY / 1e6
|
|
23
22
|
|
|
23
|
+
ARCMINUTES_PER_DEGREE = 60.0
|
|
24
|
+
ARC_SECONDS_PER_ARCMINUTE = 60.0
|
|
25
|
+
ARCSECONDS_PER_DEGREE = ARC_SECONDS_PER_ARCMINUTE * ARCMINUTES_PER_DEGREE
|
|
26
|
+
MILLIARCSECONDS_PER_DEGREE = ARCSECONDS_PER_DEGREE * 1000
|
|
27
|
+
|
|
24
28
|
PI_IN_DEGREES = 180.0
|
|
25
29
|
|
|
26
30
|
EQUATION_OF_TIME_CONSTANT = 0.0057183
|
|
27
31
|
|
|
28
32
|
KILOMETER_IN_METERS = 1_000
|
|
29
33
|
ASTRONOMICAL_UNIT_IN_METERS = 149_597_870_700
|
|
34
|
+
PARSEC_IN_METERS = 3.0856775814913673e16
|
|
30
35
|
EARTH_EQUATORIAL_RADIUS_IN_METERS = 6378140
|
|
31
36
|
|
|
32
37
|
# WGS84 Earth Constants
|
|
@@ -42,11 +42,8 @@ module Astronoby
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def compute_hour_angle(time:, longitude:)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.to_lst(longitude: longitude)
|
|
48
|
-
|
|
49
|
-
ha = (lst.time - @right_ascension.hours)
|
|
45
|
+
last = LocalApparentSiderealTime.from_utc(time.utc, longitude: longitude)
|
|
46
|
+
ha = (last.time - @right_ascension.hours)
|
|
50
47
|
ha += Constants::HOURS_PER_DAY if ha.negative?
|
|
51
48
|
|
|
52
49
|
Angle.from_hours(ha)
|
data/lib/astronoby/distance.rb
CHANGED
|
@@ -26,6 +26,12 @@ module Astronoby
|
|
|
26
26
|
end
|
|
27
27
|
alias_method :from_au, :from_astronomical_units
|
|
28
28
|
|
|
29
|
+
def from_parsecs(parsecs)
|
|
30
|
+
meters = parsecs * Constants::PARSEC_IN_METERS
|
|
31
|
+
from_meters(meters)
|
|
32
|
+
end
|
|
33
|
+
alias_method :from_pc, :from_parsecs
|
|
34
|
+
|
|
29
35
|
def vector_from_meters(array)
|
|
30
36
|
Vector.elements(array.map { from_meters(_1) })
|
|
31
37
|
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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
|
+
class ExtremumCalculator
|
|
8
|
+
# Mathematical constants
|
|
9
|
+
PHI = (1 + Math.sqrt(5)) / 2
|
|
10
|
+
INVPHI = 1 / PHI
|
|
11
|
+
GOLDEN_SECTION_TOLERANCE = 1e-5
|
|
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
|
|
37
|
+
def initialize(
|
|
38
|
+
body:,
|
|
39
|
+
primary_body:,
|
|
40
|
+
ephem:,
|
|
41
|
+
samples_per_period: 60
|
|
42
|
+
)
|
|
43
|
+
@ephem = ephem
|
|
44
|
+
@body = body
|
|
45
|
+
@primary_body = primary_body
|
|
46
|
+
@orbital_period = ORBITAL_PERIODS.fetch(body.name)
|
|
47
|
+
@samples_per_period = samples_per_period
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Finds all apoapsis events between two times
|
|
51
|
+
# @param start_time [Time] Start time
|
|
52
|
+
# @param end_time [Time] End time
|
|
53
|
+
# @return [Array<Astronoby::ExtremumEvent>] Array of apoapsis events
|
|
54
|
+
def apoapsis_events_between(start_time, end_time)
|
|
55
|
+
find_extrema(
|
|
56
|
+
Astronoby::Instant.from_time(start_time).tt,
|
|
57
|
+
Astronoby::Instant.from_time(end_time).tt,
|
|
58
|
+
type: :maximum
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Finds all periapsis events between two times
|
|
63
|
+
# @param start_time [Time] Start time
|
|
64
|
+
# @param end_time [Time] End time
|
|
65
|
+
# @return [Array<Astronoby::ExtremumEvent>] Array of periapsis events
|
|
66
|
+
def periapsis_events_between(start_time, end_time)
|
|
67
|
+
find_extrema(
|
|
68
|
+
Astronoby::Instant.from_time(start_time).tt,
|
|
69
|
+
Astronoby::Instant.from_time(end_time).tt,
|
|
70
|
+
type: :minimum
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Finds extrema (minima or maxima) in the distance between bodies within
|
|
75
|
+
# a time range
|
|
76
|
+
# @param start_jd [Float] Start time in Julian Date (Terrestrial Time)
|
|
77
|
+
# @param end_jd [Float] End time in Julian Date (Terrestrial Time)
|
|
78
|
+
# @param type [Symbol] :maximum or :minimum
|
|
79
|
+
# @return [Array<Astronoby::ExtremumEvent>] Array of extrema events
|
|
80
|
+
def find_extrema(start_jd, end_jd, type: :maximum)
|
|
81
|
+
# 1: Find extrema candidates through adaptive sampling
|
|
82
|
+
candidates = find_extrema_candidates(start_jd, end_jd, type)
|
|
83
|
+
|
|
84
|
+
# 2: Refine each candidate using golden section search
|
|
85
|
+
refined_extrema = candidates
|
|
86
|
+
.map { |candidate| refine_extremum(candidate, type) }
|
|
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)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def distance_at(jd)
|
|
97
|
+
instant = Instant.from_terrestrial_time(jd)
|
|
98
|
+
body_geometric = @body.geometric(ephem: @ephem, instant: instant)
|
|
99
|
+
primary_geometric = @primary_body
|
|
100
|
+
.geometric(ephem: @ephem, instant: instant)
|
|
101
|
+
|
|
102
|
+
distance_vector = body_geometric.position - primary_geometric.position
|
|
103
|
+
distance_vector.magnitude
|
|
104
|
+
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
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
# Represents an extremum event with its timing and value
|
|
5
|
+
class ExtremumEvent
|
|
6
|
+
attr_reader :instant, :value
|
|
7
|
+
|
|
8
|
+
# @param instant [Astronoby::Instant] When the event occurs
|
|
9
|
+
# @param value [Object] The extreme value
|
|
10
|
+
def initialize(instant, value)
|
|
11
|
+
@instant = instant
|
|
12
|
+
@value = value
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -23,7 +23,11 @@ module Astronoby
|
|
|
23
23
|
SUN_REFRACTION_ANGLE = -Angle.from_dms(0, 50, 0)
|
|
24
24
|
EVENT_TYPES = [:rising, :transit, :setting].freeze
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# @param body [Astronoby::SolarSystemBody, Astronoby::DeepSkyObject]
|
|
27
|
+
# Celestial body for which to calculate events
|
|
28
|
+
# @param observer [Astronoby::Observer] Observer location
|
|
29
|
+
# @param ephem [::Ephem::SPK, nil] Ephemeris data source
|
|
30
|
+
def initialize(body:, observer:, ephem: nil)
|
|
27
31
|
@body = body
|
|
28
32
|
@observer = observer
|
|
29
33
|
@ephem = ephem
|
|
@@ -280,7 +284,7 @@ module Astronoby
|
|
|
280
284
|
)
|
|
281
285
|
Astronoby.cache.fetch(cache_key) do
|
|
282
286
|
@body
|
|
283
|
-
.
|
|
287
|
+
.at(instant, ephem: @ephem)
|
|
284
288
|
.observed_by(@observer)
|
|
285
289
|
end
|
|
286
290
|
end
|
|
@@ -292,7 +296,7 @@ module Astronoby
|
|
|
292
296
|
instant.tt.round(precision)
|
|
293
297
|
)
|
|
294
298
|
@positions_cache[rounded_instant] ||= @body
|
|
295
|
-
.
|
|
299
|
+
.at(rounded_instant, ephem: @ephem)
|
|
296
300
|
.observed_by(@observer)
|
|
297
301
|
end
|
|
298
302
|
end
|
|
@@ -352,10 +356,9 @@ module Astronoby
|
|
|
352
356
|
end
|
|
353
357
|
|
|
354
358
|
def horizon_angle(distance)
|
|
355
|
-
|
|
356
|
-
when "Astronoby::Sun"
|
|
359
|
+
if @body == Astronoby::Sun
|
|
357
360
|
SUN_REFRACTION_ANGLE
|
|
358
|
-
|
|
361
|
+
elsif @body == Astronoby::Moon
|
|
359
362
|
STANDARD_REFRACTION_ANGLE -
|
|
360
363
|
Angle.from_radians(Moon::EQUATORIAL_RADIUS.m / distance.m)
|
|
361
364
|
else
|
|
@@ -210,7 +210,7 @@ module Astronoby
|
|
|
210
210
|
|
|
211
211
|
twilight_in_hours =
|
|
212
212
|
time_sign * (hour_angle_at_twilight - hour_angle_at_period).hours *
|
|
213
|
-
|
|
213
|
+
GreenwichMeanSiderealTime::SIDEREAL_MINUTE_IN_UT_MINUTE
|
|
214
214
|
twilight_in_seconds = time_sign *
|
|
215
215
|
twilight_in_hours *
|
|
216
216
|
Constants::SECONDS_PER_HOUR
|
data/lib/astronoby/instant.rb
CHANGED
|
@@ -94,7 +94,7 @@ module Astronoby
|
|
|
94
94
|
def to_datetime
|
|
95
95
|
DateTime.jd(
|
|
96
96
|
@terrestrial_time -
|
|
97
|
-
Rational(delta_t
|
|
97
|
+
Rational(delta_t, Constants::SECONDS_PER_DAY) +
|
|
98
98
|
DATETIME_JD_EPOCH_ADJUSTMENT
|
|
99
99
|
)
|
|
100
100
|
end
|
|
@@ -123,9 +123,32 @@ module Astronoby
|
|
|
123
123
|
|
|
124
124
|
# Get the Greenwich Mean Sidereal Time
|
|
125
125
|
#
|
|
126
|
-
# @return [Numeric] the sidereal time in
|
|
126
|
+
# @return [Numeric] the sidereal time in hours
|
|
127
127
|
def gmst
|
|
128
|
-
|
|
128
|
+
GreenwichMeanSiderealTime.from_utc(to_time).time
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Get the Greenwich Apparent Sidereal Time
|
|
132
|
+
#
|
|
133
|
+
# @return [Numeric] the sidereal time in hours
|
|
134
|
+
def gast
|
|
135
|
+
GreenwichApparentSiderealTime.from_utc(to_time).time
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Get the Local Mean Sidereal Time
|
|
139
|
+
#
|
|
140
|
+
# @param longitude [Astronoby::Angle] the observer's longitude
|
|
141
|
+
# @return [Numeric] the sidereal time in hours
|
|
142
|
+
def lmst(longitude:)
|
|
143
|
+
LocalMeanSiderealTime.from_utc(to_time, longitude: longitude).time
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Get the Local Apparent Sidereal Time
|
|
147
|
+
#
|
|
148
|
+
# @param longitude [Astronoby::Angle] the observer's longitude
|
|
149
|
+
# @return [Numeric] the sidereal time in hours
|
|
150
|
+
def last(longitude:)
|
|
151
|
+
LocalApparentSiderealTime.from_utc(to_time, longitude: longitude).time
|
|
129
152
|
end
|
|
130
153
|
|
|
131
154
|
# Get the International Atomic Time (TAI)
|
|
@@ -151,7 +174,7 @@ module Astronoby
|
|
|
151
174
|
#
|
|
152
175
|
# @return [Numeric] the offset in days
|
|
153
176
|
def utc_offset
|
|
154
|
-
|
|
177
|
+
Rational(delta_t / Constants::SECONDS_PER_DAY)
|
|
155
178
|
end
|
|
156
179
|
|
|
157
180
|
# Calculate hash value for the instant
|
|
@@ -46,15 +46,5 @@ module Astronoby
|
|
|
46
46
|
equatorial.to_ecliptic(instant: @instant)
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
|
-
|
|
50
|
-
def angular_diameter
|
|
51
|
-
@angular_radius ||= begin
|
|
52
|
-
return Angle.zero if @position.zero?
|
|
53
|
-
|
|
54
|
-
Angle.from_radians(
|
|
55
|
-
Math.atan(@target_body.class::EQUATORIAL_RADIUS.m / distance.m) * 2
|
|
56
|
-
)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
49
|
end
|
|
60
50
|
end
|
|
@@ -14,7 +14,7 @@ module Astronoby
|
|
|
14
14
|
matrix * observer.geocentric_position.map(&:m)
|
|
15
15
|
)
|
|
16
16
|
observer_velocity = Velocity.vector_from_mps(
|
|
17
|
-
matrix * observer.geocentric_velocity.map(&:
|
|
17
|
+
matrix * observer.geocentric_velocity.map(&:mps)
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
position = apparent.position - observer_position
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class StellarPropagation
|
|
5
|
+
# @return [Astronoby::Vector] Propagated position vector of
|
|
6
|
+
# Astronoby::Distance components
|
|
7
|
+
def self.position_for(**kwargs)
|
|
8
|
+
new(**kwargs).position
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @return [Astronoby::Vector] Propagated position vector of
|
|
12
|
+
# Astronoby::Velocity components
|
|
13
|
+
def self.velocity_vector_for(**kwargs)
|
|
14
|
+
new(**kwargs).velocity_vector
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [Astronoby::Coordinates::Equatorial] Propagated equatorial
|
|
18
|
+
# coordinates
|
|
19
|
+
def self.equatorial_coordinates_for(**kwargs)
|
|
20
|
+
new(**kwargs).equatorial_coordinates
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param instant [Astronoby::Instant] Instant of the observation
|
|
24
|
+
# @param equatorial_coordinates [Astronoby::Coordinates::Equatorial]
|
|
25
|
+
# Equatorial coordinates at epoch J2000.0
|
|
26
|
+
# @param proper_motion_ra [Astronoby::AngularVelocity] Proper motion in
|
|
27
|
+
# right ascension
|
|
28
|
+
# @param proper_motion_dec [Astronoby::AngularVelocity] Proper motion in
|
|
29
|
+
# declination
|
|
30
|
+
# @param parallax [Astronoby::Angle] Parallax angle
|
|
31
|
+
# @param radial_velocity [Astronoby::Velocity] Radial velocity
|
|
32
|
+
# @param earth_geometric [Astronoby::ReferenceFrame::Geometric, nil]
|
|
33
|
+
# Geometric reference frame of the Earth
|
|
34
|
+
def initialize(
|
|
35
|
+
instant:,
|
|
36
|
+
equatorial_coordinates:,
|
|
37
|
+
proper_motion_ra:,
|
|
38
|
+
proper_motion_dec:,
|
|
39
|
+
parallax:,
|
|
40
|
+
radial_velocity:,
|
|
41
|
+
earth_geometric: nil
|
|
42
|
+
)
|
|
43
|
+
@instant = instant
|
|
44
|
+
@right_ascension = equatorial_coordinates.right_ascension
|
|
45
|
+
@declination = equatorial_coordinates.declination
|
|
46
|
+
@initial_epoch = equatorial_coordinates.epoch
|
|
47
|
+
@proper_motion_ra = proper_motion_ra
|
|
48
|
+
@proper_motion_dec = proper_motion_dec
|
|
49
|
+
@parallax = parallax
|
|
50
|
+
@radial_velocity = radial_velocity
|
|
51
|
+
@earth_geometric = earth_geometric
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Astronoby::Vector] Propagated position vector of
|
|
55
|
+
# Astronoby::Distance components
|
|
56
|
+
def position
|
|
57
|
+
@position ||= Distance.vector_from_meters(
|
|
58
|
+
initial_position_vector +
|
|
59
|
+
tangential_velocity.map(&:mps) * time_elapsed_seconds
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Astronoby::Vector] Propagated position vector of
|
|
64
|
+
# Astronoby::Velocity components
|
|
65
|
+
def velocity_vector
|
|
66
|
+
@velocity_vector ||= if @earth_geometric
|
|
67
|
+
@earth_geometric.velocity - tangential_velocity
|
|
68
|
+
else
|
|
69
|
+
tangential_velocity
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Astronoby::Coordinates::Equatorial] Propagated equatorial
|
|
74
|
+
# coordinates
|
|
75
|
+
def equatorial_coordinates
|
|
76
|
+
@equatorial_coordinates ||= begin
|
|
77
|
+
right_ascension = Util::Trigonometry.adjustement_for_arctangent(
|
|
78
|
+
position.y.m,
|
|
79
|
+
position.x.m,
|
|
80
|
+
Angle.atan(position.y.m / position.x.m)
|
|
81
|
+
)
|
|
82
|
+
declination = Angle.asin(position.z.m / position.magnitude.m)
|
|
83
|
+
|
|
84
|
+
Coordinates::Equatorial.new(
|
|
85
|
+
right_ascension: right_ascension,
|
|
86
|
+
declination: declination,
|
|
87
|
+
epoch: @instant.tt
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def distance
|
|
95
|
+
@distance ||= Distance.from_parsecs(
|
|
96
|
+
1 / (@parallax.degrees * Constants::ARCSECONDS_PER_DEGREE)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def unit_position_vector
|
|
101
|
+
@unit_position_vector ||= Vector[
|
|
102
|
+
@right_ascension.cos * @declination.cos,
|
|
103
|
+
@right_ascension.sin * @declination.cos,
|
|
104
|
+
@declination.sin
|
|
105
|
+
]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def right_ascension_unit_vector
|
|
109
|
+
@right_ascension_unit_vector ||= Vector[
|
|
110
|
+
-@right_ascension.sin,
|
|
111
|
+
@right_ascension.cos,
|
|
112
|
+
0.0
|
|
113
|
+
]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def declination_unit_vector
|
|
117
|
+
@declination_unit_vector ||= Vector[
|
|
118
|
+
-@right_ascension.cos * @declination.sin,
|
|
119
|
+
-@right_ascension.sin * @declination.sin,
|
|
120
|
+
@declination.cos
|
|
121
|
+
]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def initial_position_vector
|
|
125
|
+
@initial_position_vector ||= unit_position_vector * distance.meters
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def tangential_velocity
|
|
129
|
+
@tangential_velocity ||= begin
|
|
130
|
+
# Doppler factor for light travel time correction
|
|
131
|
+
k = 1.0 / (1.0 - @radial_velocity.kmps / Velocity.light_speed.kmps)
|
|
132
|
+
|
|
133
|
+
proper_motion_ra_component =
|
|
134
|
+
@proper_motion_ra.mas_per_year / (
|
|
135
|
+
@parallax.degree_milliarcseconds * Constants::DAYS_PER_JULIAN_YEAR
|
|
136
|
+
) * k
|
|
137
|
+
proper_motion_dec_component =
|
|
138
|
+
@proper_motion_dec.mas_per_year / (
|
|
139
|
+
@parallax.degree_milliarcseconds * Constants::DAYS_PER_JULIAN_YEAR
|
|
140
|
+
) * k
|
|
141
|
+
radial_velocity_component = Velocity
|
|
142
|
+
.from_kmps(@radial_velocity.kmps * k)
|
|
143
|
+
|
|
144
|
+
Velocity.vector_from_astronomical_units_per_day([
|
|
145
|
+
-proper_motion_ra_component * @right_ascension.sin -
|
|
146
|
+
proper_motion_dec_component * @declination.sin * @right_ascension.cos +
|
|
147
|
+
radial_velocity_component.aupd * @declination.cos * @right_ascension.cos,
|
|
148
|
+
proper_motion_ra_component * @right_ascension.cos -
|
|
149
|
+
proper_motion_dec_component * @declination.sin * @right_ascension.sin +
|
|
150
|
+
radial_velocity_component.aupd * @declination.cos * @right_ascension.sin,
|
|
151
|
+
proper_motion_dec_component * @declination.cos +
|
|
152
|
+
radial_velocity_component.aupd * @declination.sin
|
|
153
|
+
])
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def time_elapsed_seconds
|
|
158
|
+
@time_elapsed_seconds ||=
|
|
159
|
+
(@instant.tt - @initial_epoch) * Constants::SECONDS_PER_DAY
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|