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,14 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Holds arrays of twilight times over a time range.
|
|
4
5
|
class TwilightEvents
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
:morning_nautical_twilight_times,
|
|
8
|
-
:evening_nautical_twilight_times,
|
|
9
|
-
:morning_astronomical_twilight_times,
|
|
10
|
-
:evening_astronomical_twilight_times
|
|
6
|
+
# @return [Array<Time>] morning civil twilight times
|
|
7
|
+
attr_reader :morning_civil_twilight_times
|
|
11
8
|
|
|
9
|
+
# @return [Array<Time>] evening civil twilight times
|
|
10
|
+
attr_reader :evening_civil_twilight_times
|
|
11
|
+
|
|
12
|
+
# @return [Array<Time>] morning nautical twilight times
|
|
13
|
+
attr_reader :morning_nautical_twilight_times
|
|
14
|
+
|
|
15
|
+
# @return [Array<Time>] evening nautical twilight times
|
|
16
|
+
attr_reader :evening_nautical_twilight_times
|
|
17
|
+
|
|
18
|
+
# @return [Array<Time>] morning astronomical twilight times
|
|
19
|
+
attr_reader :morning_astronomical_twilight_times
|
|
20
|
+
|
|
21
|
+
# @return [Array<Time>] evening astronomical twilight times
|
|
22
|
+
attr_reader :evening_astronomical_twilight_times
|
|
23
|
+
|
|
24
|
+
# @param morning_civil [Array<Time>] morning civil twilight times
|
|
25
|
+
# @param evening_civil [Array<Time>] evening civil twilight times
|
|
26
|
+
# @param morning_nautical [Array<Time>] morning nautical twilight times
|
|
27
|
+
# @param evening_nautical [Array<Time>] evening nautical twilight times
|
|
28
|
+
# @param morning_astronomical [Array<Time>] morning astronomical twilight
|
|
29
|
+
# times
|
|
30
|
+
# @param evening_astronomical [Array<Time>] evening astronomical twilight
|
|
31
|
+
# times
|
|
12
32
|
def initialize(
|
|
13
33
|
morning_civil,
|
|
14
34
|
evening_civil,
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class ExtremumFinder
|
|
5
|
+
PHI = (1 + Math.sqrt(5)) / 2
|
|
6
|
+
INVPHI = 1 / PHI
|
|
7
|
+
GOLDEN_SECTION_TOLERANCE = 1e-5
|
|
8
|
+
|
|
9
|
+
MIN_SAMPLES_PER_PERIOD = 20
|
|
10
|
+
DUPLICATE_THRESHOLD_DAYS = 0.5
|
|
11
|
+
BOUNDARY_BUFFER_DAYS = 0.01
|
|
12
|
+
|
|
13
|
+
# @param value_at [#call] callable mapping a Julian Date (Terrestrial Time)
|
|
14
|
+
# to a comparable value
|
|
15
|
+
# @param period [Float] the characteristic period of the quantity in days,
|
|
16
|
+
# used to scale the sampling density
|
|
17
|
+
# @param samples_per_period [Integer] number of samples per period
|
|
18
|
+
def initialize(value_at:, period:, samples_per_period: 60)
|
|
19
|
+
@value_at = value_at
|
|
20
|
+
@period = period
|
|
21
|
+
@samples_per_period = samples_per_period
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param start_jd [Float] start time in Julian Date (Terrestrial Time)
|
|
25
|
+
# @param end_jd [Float] end time in Julian Date (Terrestrial Time)
|
|
26
|
+
# @param type [Symbol] +:maximum+ or +:minimum+
|
|
27
|
+
# @return [Array<Hash>] extrema as +{jd: Float, value: Comparable}+, sorted
|
|
28
|
+
# by time
|
|
29
|
+
def extrema(start_jd, end_jd, type: :maximum)
|
|
30
|
+
candidates = find_candidates(start_jd, end_jd, type)
|
|
31
|
+
refined = candidates.map { |candidate| refine(candidate, type) }.compact
|
|
32
|
+
refined = remove_duplicates(refined)
|
|
33
|
+
filter_boundary_artifacts(refined, start_jd, end_jd)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def find_candidates(start_jd, end_jd, type)
|
|
39
|
+
samples = collect_samples(start_jd, end_jd)
|
|
40
|
+
find_local_extrema(samples, type)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def collect_samples(start_jd, end_jd)
|
|
44
|
+
duration = end_jd - start_jd
|
|
45
|
+
sample_count = sample_count_for(duration)
|
|
46
|
+
step = duration / sample_count
|
|
47
|
+
|
|
48
|
+
(0..sample_count).map do |i|
|
|
49
|
+
jd = start_jd + (i * step)
|
|
50
|
+
{jd: jd, value: @value_at.call(jd)}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def sample_count_for(duration)
|
|
55
|
+
periods_in_range = duration / @period
|
|
56
|
+
base_samples = (periods_in_range * @samples_per_period).to_i
|
|
57
|
+
[base_samples, MIN_SAMPLES_PER_PERIOD].max
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def find_local_extrema(samples, type)
|
|
61
|
+
candidates = []
|
|
62
|
+
|
|
63
|
+
(1...samples.length - 1).each do |i|
|
|
64
|
+
next unless local_extremum?(samples, i, type)
|
|
65
|
+
|
|
66
|
+
candidates << {
|
|
67
|
+
start_jd: samples[i - 1][:jd],
|
|
68
|
+
end_jd: samples[i + 1][:jd]
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
candidates
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def local_extremum?(samples, index, type)
|
|
76
|
+
current = samples[index][:value]
|
|
77
|
+
previous = samples[index - 1][:value]
|
|
78
|
+
following = samples[index + 1][:value]
|
|
79
|
+
|
|
80
|
+
if type == :maximum
|
|
81
|
+
current > previous && current > following
|
|
82
|
+
else
|
|
83
|
+
current < previous && current < following
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def refine(candidate, type)
|
|
88
|
+
golden_section_search(candidate[:start_jd], candidate[:end_jd], type)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def golden_section_search(a, b, type)
|
|
92
|
+
return nil if b <= a
|
|
93
|
+
|
|
94
|
+
tol = GOLDEN_SECTION_TOLERANCE * (b - a).abs
|
|
95
|
+
|
|
96
|
+
x1 = a + (1 - INVPHI) * (b - a)
|
|
97
|
+
x2 = a + INVPHI * (b - a)
|
|
98
|
+
f1 = @value_at.call(x1)
|
|
99
|
+
f2 = @value_at.call(x2)
|
|
100
|
+
|
|
101
|
+
while (b - a).abs > tol
|
|
102
|
+
keep_left = (type == :maximum) ? (f1 > f2) : (f1 < f2)
|
|
103
|
+
|
|
104
|
+
if keep_left
|
|
105
|
+
b = x2
|
|
106
|
+
x2 = x1
|
|
107
|
+
f2 = f1
|
|
108
|
+
x1 = a + (1 - INVPHI) * (b - a)
|
|
109
|
+
f1 = @value_at.call(x1)
|
|
110
|
+
else
|
|
111
|
+
a = x1
|
|
112
|
+
x1 = x2
|
|
113
|
+
f1 = f2
|
|
114
|
+
x2 = a + INVPHI * (b - a)
|
|
115
|
+
f2 = @value_at.call(x2)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
mid = (a + b) / 2
|
|
120
|
+
{jd: mid, value: @value_at.call(mid)}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def remove_duplicates(extrema)
|
|
124
|
+
return extrema if extrema.length <= 1
|
|
125
|
+
|
|
126
|
+
cleaned = [extrema.first]
|
|
127
|
+
|
|
128
|
+
extrema.each_with_index do |current, i|
|
|
129
|
+
next if i == 0
|
|
130
|
+
|
|
131
|
+
is_duplicate = cleaned.any? do |existing|
|
|
132
|
+
(current[:jd] - existing[:jd]).abs < DUPLICATE_THRESHOLD_DAYS
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
cleaned << current unless is_duplicate
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
cleaned
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def filter_boundary_artifacts(extrema, start_jd, end_jd)
|
|
142
|
+
extrema.reject do |extreme|
|
|
143
|
+
(extreme[:jd] - start_jd).abs < BOUNDARY_BUFFER_DAYS ||
|
|
144
|
+
(extreme[:jd] - end_jd).abs < BOUNDARY_BUFFER_DAYS
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
module Horizon
|
|
5
|
+
# @return [Astronoby::Angle] altitude of the centre of a point-like body at
|
|
6
|
+
# rise and set
|
|
7
|
+
REFRACTION_ANGLE = -Angle.from_dms(0, 34, 0)
|
|
8
|
+
|
|
9
|
+
# @return [Astronoby::Angle] altitude of the Sun's centre at rise and set
|
|
10
|
+
SUN_ANGLE = -Angle.from_dms(0, 50, 0)
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# @param body [Class] the body rising or setting
|
|
14
|
+
# @param distance [Astronoby::Distance] distance to the body, used for
|
|
15
|
+
# the Moon's semidiameter
|
|
16
|
+
# @return [Astronoby::Angle] altitude of the body's centre at rise and set
|
|
17
|
+
def angle_for(body:, distance:)
|
|
18
|
+
if body == Sun
|
|
19
|
+
SUN_ANGLE
|
|
20
|
+
elsif body == Moon
|
|
21
|
+
REFRACTION_ANGLE - moon_semidiameter(distance)
|
|
22
|
+
else
|
|
23
|
+
REFRACTION_ANGLE
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param distance [Astronoby::Distance] distance to the Moon
|
|
28
|
+
# @return [Astronoby::Angle] the Moon's semidiameter
|
|
29
|
+
def moon_semidiameter(distance)
|
|
30
|
+
Angle.from_radians(moon_semidiameter_radians(distance.m))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param distance_in_meters [Numeric] distance to the Moon in meters
|
|
34
|
+
# @return [Float] the Moon's semidiameter in radians
|
|
35
|
+
def moon_semidiameter_radians(distance_in_meters)
|
|
36
|
+
Moon::EQUATORIAL_RADIUS.m.to_f / distance_in_meters
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
# Transforms equatorial coordinates at J2000.0 into horizontal coordinates
|
|
5
|
+
# for one observer at one instant.
|
|
6
|
+
#
|
|
7
|
+
# Everything that depends only on the observer and the instant is computed
|
|
8
|
+
# once at construction: the rotation from the ICRS to the true equator and
|
|
9
|
+
# equinox of date (nutation * precession, with the ICRS frame bias), and the
|
|
10
|
+
# local apparent sidereal time. Converting a point then costs a matrix
|
|
11
|
+
# product and a few trigonometric functions, instead of rebuilding the
|
|
12
|
+
# reference frame chain for each point. It is meant for converting many
|
|
13
|
+
# points for the same observer and instant, such as the stars of a catalogue
|
|
14
|
+
# for a sky chart.
|
|
15
|
+
#
|
|
16
|
+
# It plays the role of the "star-independent astrometry parameters" of SOFA
|
|
17
|
+
# and ERFA, restricted to what Astronoby applies without an ephemeris. The
|
|
18
|
+
# result is the same as
|
|
19
|
+
# `DeepSkyObject#at(instant).observed_by(observer).horizontal`, but the
|
|
20
|
+
# points are treated as fixed directions: no aberration, no proper motion and
|
|
21
|
+
# no parallax. The topocentric correction is not applied either, as it is
|
|
22
|
+
# nil for objects at stellar distances. Atmospheric refraction is optional.
|
|
23
|
+
#
|
|
24
|
+
# @example Convert the stars of a catalogue for one observer and instant
|
|
25
|
+
# transformation = Astronoby::HorizontalTransformation.new(
|
|
26
|
+
# observer: observer,
|
|
27
|
+
# instant: instant
|
|
28
|
+
# )
|
|
29
|
+
# horizontals = catalogue.map { |star| transformation.apply(star) }
|
|
30
|
+
class HorizontalTransformation
|
|
31
|
+
# @return [Astronoby::Observer] the observer
|
|
32
|
+
attr_reader :observer
|
|
33
|
+
|
|
34
|
+
# @return [Astronoby::Instant] the instant
|
|
35
|
+
attr_reader :instant
|
|
36
|
+
|
|
37
|
+
# @param observer [Astronoby::Observer] the observer
|
|
38
|
+
# @param instant [Astronoby::Instant] the instant of the observation
|
|
39
|
+
def initialize(observer:, instant:)
|
|
40
|
+
@observer = observer
|
|
41
|
+
@instant = instant
|
|
42
|
+
@rotation_rows = (
|
|
43
|
+
Nutation.matrix_for(instant) * Precession.matrix_for(instant)
|
|
44
|
+
).to_a.freeze
|
|
45
|
+
@local_apparent_sidereal_time = LocalApparentSiderealTime.from_utc(
|
|
46
|
+
instant.to_time,
|
|
47
|
+
longitude: observer.longitude
|
|
48
|
+
).time
|
|
49
|
+
freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param equatorial_coordinates [Astronoby::Coordinates::Equatorial]
|
|
53
|
+
# equatorial coordinates at epoch J2000.0
|
|
54
|
+
# @param refraction [Boolean] whether to apply atmospheric refraction
|
|
55
|
+
# correction (default: false)
|
|
56
|
+
# @return [Astronoby::Coordinates::Horizontal] horizontal coordinates
|
|
57
|
+
# @raise [Astronoby::IncompatibleArgumentsError] if the coordinates are not
|
|
58
|
+
# referred to epoch J2000.0
|
|
59
|
+
def apply(equatorial_coordinates, refraction: false)
|
|
60
|
+
ensure_j2000!(equatorial_coordinates)
|
|
61
|
+
|
|
62
|
+
horizontal = apparent_equatorial(equatorial_coordinates).to_horizontal(
|
|
63
|
+
time: @instant.to_time,
|
|
64
|
+
observer: @observer
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if refraction
|
|
68
|
+
Refraction.correct_horizontal_coordinates(coordinates: horizontal)
|
|
69
|
+
else
|
|
70
|
+
horizontal
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Equatorial coordinates of date, with the hour angle already known so that
|
|
77
|
+
# the sidereal time is not computed again.
|
|
78
|
+
def apparent_equatorial(coordinates)
|
|
79
|
+
declination = coordinates.declination
|
|
80
|
+
right_ascension = coordinates.right_ascension
|
|
81
|
+
cos_declination = declination.cos
|
|
82
|
+
x = cos_declination * right_ascension.cos
|
|
83
|
+
y = cos_declination * right_ascension.sin
|
|
84
|
+
z = declination.sin
|
|
85
|
+
|
|
86
|
+
row_x, row_y, row_z = @rotation_rows
|
|
87
|
+
apparent_x = row_x[0] * x + row_x[1] * y + row_x[2] * z
|
|
88
|
+
apparent_y = row_y[0] * x + row_y[1] * y + row_y[2] * z
|
|
89
|
+
apparent_z = row_z[0] * x + row_z[1] * y + row_z[2] * z
|
|
90
|
+
|
|
91
|
+
apparent_right_ascension =
|
|
92
|
+
Angle.from_radians(Math.atan2(apparent_y, apparent_x))
|
|
93
|
+
|
|
94
|
+
Coordinates::Equatorial.new(
|
|
95
|
+
right_ascension: apparent_right_ascension,
|
|
96
|
+
declination: Angle.asin(apparent_z.clamp(-1.0, 1.0)),
|
|
97
|
+
hour_angle:
|
|
98
|
+
Angle.from_hours(@local_apparent_sidereal_time) -
|
|
99
|
+
apparent_right_ascension,
|
|
100
|
+
epoch: @instant.tt
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def ensure_j2000!(coordinates)
|
|
105
|
+
return if coordinates.epoch == JulianDate::J2000
|
|
106
|
+
|
|
107
|
+
raise IncompatibleArgumentsError,
|
|
108
|
+
"Expected equatorial coordinates at epoch J2000.0, got epoch " \
|
|
109
|
+
"#{coordinates.epoch}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/astronoby/instant.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "date"
|
|
4
|
+
require "horologium"
|
|
4
5
|
|
|
5
6
|
module Astronoby
|
|
6
7
|
# Represents a specific instant in time using Terrestrial Time (TT) as its
|
|
@@ -8,9 +9,15 @@ module Astronoby
|
|
|
8
9
|
# time scales commonly used in astronomy:
|
|
9
10
|
# - Terrestrial Time (TT)
|
|
10
11
|
# - International Atomic Time (TAI)
|
|
12
|
+
# - Barycentric Dynamical Time (TDB)
|
|
11
13
|
# - Universal Time Coordinated (UTC)
|
|
12
14
|
# - Greenwich Mean Sidereal Time (GMST)
|
|
13
15
|
#
|
|
16
|
+
# TAI and TDB are delegated to the horologium gem, which owns the scales
|
|
17
|
+
# that convert by definition or by model. UT1 is not: it rests on
|
|
18
|
+
# observations of the rotation of the Earth, and comes from
|
|
19
|
+
# {Astronoby::DeltaT}.
|
|
20
|
+
#
|
|
14
21
|
# @example Create an instant from the current time
|
|
15
22
|
# instant = Astronoby::Instant.from_time(Time.now)
|
|
16
23
|
# instant.tai # Get International Atomic Time
|
|
@@ -28,6 +35,16 @@ module Astronoby
|
|
|
28
35
|
# midnight. This constant adds 0.5 days (12 hours) to make the conversion.
|
|
29
36
|
DATETIME_JD_EPOCH_ADJUSTMENT = 0.5
|
|
30
37
|
|
|
38
|
+
# Astronoby pins the precision it asks horologium for rather than reading
|
|
39
|
+
# horologium's global default, so a host application configuring
|
|
40
|
+
# horologium for itself cannot change what Astronoby computes.
|
|
41
|
+
#
|
|
42
|
+
# Exact rather than standard: standard would round the Julian Date into a
|
|
43
|
+
# two-part float on the way in, and TAI is a fixed offset from TT, so
|
|
44
|
+
# there is nothing to round it for. It is also the cheaper of the two
|
|
45
|
+
# here, having no conversion to make in either direction.
|
|
46
|
+
PRECISION = :exact
|
|
47
|
+
|
|
31
48
|
class << self
|
|
32
49
|
# Creates a new Instant from a Terrestrial Time value
|
|
33
50
|
#
|
|
@@ -44,7 +61,7 @@ module Astronoby
|
|
|
44
61
|
# @param time [Time] a Time object to convert
|
|
45
62
|
# @return [Astronoby::Instant] a new Instant object
|
|
46
63
|
def from_time(time)
|
|
47
|
-
delta_t =
|
|
64
|
+
delta_t = DeltaT.at(time)
|
|
48
65
|
terrestrial_time = time.utc.to_datetime.ajd +
|
|
49
66
|
Rational(delta_t, Constants::SECONDS_PER_DAY)
|
|
50
67
|
from_terrestrial_time(terrestrial_time)
|
|
@@ -55,13 +72,14 @@ module Astronoby
|
|
|
55
72
|
# @param julian_date [Numeric] the Julian Date in UTC
|
|
56
73
|
# @return [Astronoby::Instant] a new Instant object
|
|
57
74
|
def from_utc_julian_date(julian_date)
|
|
58
|
-
delta_t =
|
|
75
|
+
delta_t = DeltaT.at(julian_date)
|
|
59
76
|
terrestrial_time = julian_date +
|
|
60
77
|
Rational(delta_t, Constants::SECONDS_PER_DAY)
|
|
61
78
|
from_terrestrial_time(terrestrial_time)
|
|
62
79
|
end
|
|
63
80
|
end
|
|
64
81
|
|
|
82
|
+
# @return [Numeric] the Terrestrial Time as a Julian Date
|
|
65
83
|
attr_reader :terrestrial_time
|
|
66
84
|
alias_method :tt, :terrestrial_time
|
|
67
85
|
alias_method :julian_date, :terrestrial_time
|
|
@@ -77,6 +95,7 @@ module Astronoby
|
|
|
77
95
|
end
|
|
78
96
|
|
|
79
97
|
@terrestrial_time = terrestrial_time
|
|
98
|
+
@memo = {}
|
|
80
99
|
freeze
|
|
81
100
|
end
|
|
82
101
|
|
|
@@ -92,7 +111,7 @@ module Astronoby
|
|
|
92
111
|
#
|
|
93
112
|
# @return [DateTime] the UTC time as DateTime
|
|
94
113
|
def to_datetime
|
|
95
|
-
DateTime.jd(
|
|
114
|
+
@memo[:to_datetime] ||= DateTime.jd(
|
|
96
115
|
@terrestrial_time -
|
|
97
116
|
Rational(delta_t, Constants::SECONDS_PER_DAY) +
|
|
98
117
|
DATETIME_JD_EPOCH_ADJUSTMENT
|
|
@@ -110,7 +129,7 @@ module Astronoby
|
|
|
110
129
|
#
|
|
111
130
|
# @return [Time] the UTC time
|
|
112
131
|
def to_time
|
|
113
|
-
to_datetime.to_time.utc
|
|
132
|
+
@memo[:to_time] ||= to_datetime.to_time.utc
|
|
114
133
|
end
|
|
115
134
|
|
|
116
135
|
# Get the ΔT (Delta T) value for this instant
|
|
@@ -118,21 +137,21 @@ module Astronoby
|
|
|
118
137
|
#
|
|
119
138
|
# @return [Numeric] Delta T in seconds
|
|
120
139
|
def delta_t
|
|
121
|
-
|
|
140
|
+
@memo[:delta_t] ||= DeltaT.at(@terrestrial_time)
|
|
122
141
|
end
|
|
123
142
|
|
|
124
143
|
# Get the Greenwich Mean Sidereal Time
|
|
125
144
|
#
|
|
126
145
|
# @return [Numeric] the sidereal time in hours
|
|
127
146
|
def gmst
|
|
128
|
-
GreenwichMeanSiderealTime.from_utc(to_time).time
|
|
147
|
+
@memo[:gmst] ||= GreenwichMeanSiderealTime.from_utc(to_time).time
|
|
129
148
|
end
|
|
130
149
|
|
|
131
150
|
# Get the Greenwich Apparent Sidereal Time
|
|
132
151
|
#
|
|
133
152
|
# @return [Numeric] the sidereal time in hours
|
|
134
153
|
def gast
|
|
135
|
-
GreenwichApparentSiderealTime.from_utc(to_time).time
|
|
154
|
+
@memo[:gast] ||= GreenwichApparentSiderealTime.from_utc(to_time).time
|
|
136
155
|
end
|
|
137
156
|
|
|
138
157
|
# Get the Local Mean Sidereal Time
|
|
@@ -153,21 +172,26 @@ module Astronoby
|
|
|
153
172
|
|
|
154
173
|
# Get the International Atomic Time (TAI)
|
|
155
174
|
#
|
|
156
|
-
#
|
|
175
|
+
# Read as a Rational rather than a Float. A Julian Date is around 2.46
|
|
176
|
+
# million, so a single Float rounds to about 47 microseconds of the day,
|
|
177
|
+
# which would coarsen an instant that TT carries exactly.
|
|
178
|
+
#
|
|
179
|
+
# @return [Rational] TAI as Julian Date
|
|
157
180
|
def tai
|
|
158
|
-
@
|
|
159
|
-
|
|
181
|
+
@memo[:tai] ||= scale_free_instant
|
|
182
|
+
.as(:julian_date, scale: :tai, as: :rational)
|
|
160
183
|
end
|
|
161
184
|
|
|
162
185
|
# Get the Barycentric Dynamical Time (TDB)
|
|
163
|
-
# Note: Currently approximated as equal to TT
|
|
164
186
|
#
|
|
165
|
-
#
|
|
187
|
+
# TDB - TT comes from the Fairhead and Bretagnon series ERFA uses. It
|
|
188
|
+
# peaks around 1.7 milliseconds, so a Float Julian Date would quantise
|
|
189
|
+
# most of the correction away; see {#tai} on the shape.
|
|
190
|
+
#
|
|
191
|
+
# @return [Rational] TDB as Julian Date
|
|
166
192
|
def tdb
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
# doesn't support it and consider they are the same value.
|
|
170
|
-
@terrestrial_time
|
|
193
|
+
@memo[:tdb] ||= scale_free_instant
|
|
194
|
+
.as(:julian_date, scale: :tdb, as: :rational)
|
|
171
195
|
end
|
|
172
196
|
|
|
173
197
|
# Get the offset between TT and UTC for this instant
|
|
@@ -195,5 +219,21 @@ module Astronoby
|
|
|
195
219
|
@terrestrial_time <=> other.terrestrial_time
|
|
196
220
|
end
|
|
197
221
|
alias_method :eql?, :==
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
# The instant as horologium holds it: a point on the timeline with no
|
|
226
|
+
# scale of its own. Building it costs more than reading a Julian Date back
|
|
227
|
+
# out of it, so it is built on the first scale conversion that needs it
|
|
228
|
+
# and never on the paths that only want TT.
|
|
229
|
+
#
|
|
230
|
+
# @return [Horologium::Instant]
|
|
231
|
+
def scale_free_instant
|
|
232
|
+
@memo[:scale_free_instant] ||= Horologium::Instant.from_julian_date(
|
|
233
|
+
@terrestrial_time,
|
|
234
|
+
scale: :tt,
|
|
235
|
+
precision: PRECISION
|
|
236
|
+
)
|
|
237
|
+
end
|
|
198
238
|
end
|
|
199
239
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
# Geocentric libration of the Moon: the small oscillations that let an
|
|
5
|
+
# observer on Earth see slightly more than half of the lunar surface over
|
|
6
|
+
# time. Holds the total (optical + physical) libration in longitude and
|
|
7
|
+
# latitude.
|
|
8
|
+
class Libration
|
|
9
|
+
# @return [Astronoby::Angle] libration in longitude (l), positive towards
|
|
10
|
+
# the Moon's Mare Crisium (east limb)
|
|
11
|
+
attr_reader :longitude
|
|
12
|
+
|
|
13
|
+
# @return [Astronoby::Angle] libration in latitude (b), positive towards
|
|
14
|
+
# the Moon's north pole
|
|
15
|
+
attr_reader :latitude
|
|
16
|
+
|
|
17
|
+
# @param longitude [Astronoby::Angle] libration in longitude
|
|
18
|
+
# @param latitude [Astronoby::Angle] libration in latitude
|
|
19
|
+
def initialize(longitude:, latitude:)
|
|
20
|
+
@longitude = longitude
|
|
21
|
+
@latitude = latitude
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# as these coefficients work with TT (Terrestrial Time).
|
|
5
5
|
|
|
6
6
|
module Astronoby
|
|
7
|
+
# Computes the mean obliquity of the ecliptic using the IAU 2006 P03 model.
|
|
7
8
|
class MeanObliquity
|
|
8
9
|
# Source:
|
|
9
10
|
# IAU resolution in 2006 in favor of the P03 astronomical model
|
|
@@ -12,6 +13,10 @@ module Astronoby
|
|
|
12
13
|
EPOCH_OF_REFERENCE = JulianDate::DEFAULT_EPOCH
|
|
13
14
|
OBLIQUITY_OF_REFERENCE = 23.4392794
|
|
14
15
|
|
|
16
|
+
# Computes the mean obliquity of the ecliptic at the given instant.
|
|
17
|
+
#
|
|
18
|
+
# @param instant [Astronoby::Instant] the time instant
|
|
19
|
+
# @return [Astronoby::Angle] the mean obliquity
|
|
15
20
|
def self.at(instant)
|
|
16
21
|
return obliquity_of_reference if instant.julian_date == EPOCH_OF_REFERENCE
|
|
17
22
|
|
|
@@ -32,10 +37,13 @@ module Astronoby
|
|
|
32
37
|
)
|
|
33
38
|
end
|
|
34
39
|
|
|
40
|
+
# @return [Astronoby::Angle] the mean obliquity at the reference epoch
|
|
41
|
+
# (J2000.0)
|
|
35
42
|
def self.obliquity_of_reference
|
|
36
43
|
Angle.from_degree_arcseconds(obliquity_of_reference_in_arcseconds)
|
|
37
44
|
end
|
|
38
45
|
|
|
46
|
+
# @return [Float] the mean obliquity at J2000.0 in arcseconds
|
|
39
47
|
def self.obliquity_of_reference_in_arcseconds
|
|
40
48
|
84381.406
|
|
41
49
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
# Computes the Moon's libration and the position angle of its axis from a
|
|
5
|
+
# binary PCK lunar orientation kernel, as the arcsecond-accurate counterpart
|
|
6
|
+
# to the analytic Meeus series in MoonPhysicalEphemeris.
|
|
7
|
+
#
|
|
8
|
+
# The libration is the selenographic longitude and latitude of the sub-Earth
|
|
9
|
+
# point, expressed in the Moon's mean-Earth body-fixed frame.
|
|
10
|
+
class MoonOrientationEphemeris
|
|
11
|
+
# @param moon [Astronoby::Moon] the Moon, carrying an orientation kernel
|
|
12
|
+
def initialize(moon)
|
|
13
|
+
@moon = moon
|
|
14
|
+
@instant = moon.instant
|
|
15
|
+
@orientation = moon.orientation
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [Astronoby::Libration] the libration in longitude and latitude
|
|
19
|
+
def libration
|
|
20
|
+
sub_earth = rotation * moon_to_earth
|
|
21
|
+
Libration.new(
|
|
22
|
+
longitude: Angle.from_radians(Math.atan2(sub_earth[1], sub_earth[0])),
|
|
23
|
+
latitude: Angle.from_radians(
|
|
24
|
+
Math.asin(sub_earth[2] / sub_earth.magnitude)
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Position angle of the Moon's axis of rotation, measured eastward from the
|
|
30
|
+
# north point of the disk.
|
|
31
|
+
# @return [Astronoby::Angle] the position angle of the axis
|
|
32
|
+
def position_angle_of_axis
|
|
33
|
+
@moon.apparent.equatorial.position_angle_to(north_pole)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def rotation
|
|
39
|
+
@rotation ||= @orientation.rotation_for(retarded_instant)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def retarded_instant
|
|
43
|
+
Instant.from_terrestrial_time(
|
|
44
|
+
@instant.terrestrial_time - light_time_in_days
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def light_time_in_days
|
|
49
|
+
@moon.astrometric.distance.km /
|
|
50
|
+
Velocity.light_speed.kmps /
|
|
51
|
+
Constants::SECONDS_PER_DAY
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def moon_to_earth
|
|
55
|
+
x, y, z = @moon.astrometric.position.map(&:m).to_a
|
|
56
|
+
::Vector[-x, -y, -z]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def north_pole
|
|
60
|
+
@north_pole ||= Coordinates::Equatorial.from_position_vector(
|
|
61
|
+
Distance.vector_from_meters(
|
|
62
|
+
Nutation.matrix_for(@instant) *
|
|
63
|
+
Precession.matrix_for(@instant) *
|
|
64
|
+
(rotation.transpose * ::Vector[0, 0, 1])
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|