astronoby 0.9.0 → 0.10.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 +101 -0
- data/README.md +6 -1
- data/UPGRADING.md +84 -0
- data/docs/README.md +80 -15
- data/docs/angles.md +1 -0
- data/docs/configuration.md +20 -17
- data/docs/coordinates.md +72 -12
- data/docs/deep_sky_bodies.md +1 -1
- data/docs/ephem.md +5 -2
- data/docs/equinoxes_solstices_times.md +4 -3
- data/docs/glossary.md +97 -1
- data/docs/iers.md +40 -0
- data/docs/instant.md +20 -15
- data/docs/lunar_eclipses.md +93 -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 +25 -21
- 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 +32 -17
- 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/center.rb +84 -0
- data/lib/astronoby/constellation.rb +9 -1
- data/lib/astronoby/coordinates/ecliptic.rb +10 -1
- data/lib/astronoby/coordinates/equatorial.rb +64 -8
- data/lib/astronoby/coordinates/geodetic.rb +102 -0
- data/lib/astronoby/coordinates/horizontal.rb +13 -1
- 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/conjunction.rb +51 -0
- data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
- data/lib/astronoby/events/eclipse_phase.rb +27 -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 +99 -0
- data/lib/astronoby/events/lunar_eclipse_calculator.rb +285 -0
- data/lib/astronoby/events/opposition.rb +19 -0
- 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_event.rb +24 -6
- data/lib/astronoby/events/twilight_events.rb +26 -6
- data/lib/astronoby/extremum_finder.rb +148 -0
- data/lib/astronoby/instant.rb +10 -7
- 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 +61 -60
- data/lib/astronoby/reference_frame.rb +73 -7
- data/lib/astronoby/reference_frames/apparent.rb +26 -7
- 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 +13 -1
- data/lib/astronoby/reference_frames/teme.rb +153 -0
- data/lib/astronoby/reference_frames/topocentric.rb +30 -4
- data/lib/astronoby/refraction.rb +26 -5
- data/lib/astronoby/root_finder.rb +83 -0
- data/lib/astronoby/rotation.rb +49 -0
- data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +9 -0
- data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +42 -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 +10 -485
- 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 +22 -0
- metadata +45 -5
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Geometric reference frame (BCRS). Represents a body's position relative
|
|
5
|
+
# to the Solar System Barycenter, without any corrections applied.
|
|
4
6
|
class Geometric < ReferenceFrame
|
|
7
|
+
# @param position [Astronoby::Vector<Astronoby::Distance>] position vector
|
|
8
|
+
# @param velocity [Astronoby::Vector<Astronoby::Velocity>] velocity vector
|
|
9
|
+
# @param instant [Astronoby::Instant] the time instant
|
|
10
|
+
# @param target_body [Astronoby::Body, nil] the target body
|
|
5
11
|
def initialize(
|
|
6
12
|
position:,
|
|
7
13
|
velocity:,
|
|
@@ -12,7 +18,7 @@ module Astronoby
|
|
|
12
18
|
position: position,
|
|
13
19
|
velocity: velocity,
|
|
14
20
|
instant: instant,
|
|
15
|
-
|
|
21
|
+
center: Center.barycentric,
|
|
16
22
|
target_body: target_body
|
|
17
23
|
)
|
|
18
24
|
end
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Mean-of-date reference frame. Represents a body's geocentric position
|
|
5
|
+
# corrected for precession only (no nutation or aberration).
|
|
4
6
|
class MeanOfDate < ReferenceFrame
|
|
7
|
+
# Builds a mean-of-date frame from geometric frames by applying
|
|
8
|
+
# precession.
|
|
9
|
+
#
|
|
10
|
+
# @param instant [Astronoby::Instant] the time instant
|
|
11
|
+
# @param target_geometric [Astronoby::Geometric] target's geometric frame
|
|
12
|
+
# @param earth_geometric [Astronoby::Geometric] Earth's geometric frame
|
|
13
|
+
# @param target_body [Astronoby::Body, nil] the target body
|
|
14
|
+
# @return [Astronoby::MeanOfDate] a new mean-of-date frame
|
|
5
15
|
def self.build_from_geometric(
|
|
6
16
|
instant:,
|
|
7
17
|
target_geometric:,
|
|
@@ -22,11 +32,13 @@ module Astronoby
|
|
|
22
32
|
position: corrected_position,
|
|
23
33
|
velocity: corrected_velocity,
|
|
24
34
|
instant: instant,
|
|
25
|
-
|
|
35
|
+
center: Center.geocentric,
|
|
26
36
|
target_body: target_body
|
|
27
37
|
)
|
|
28
38
|
end
|
|
29
39
|
|
|
40
|
+
# @return [Astronoby::Coordinates::Ecliptic] ecliptic coordinates at the
|
|
41
|
+
# current instant (mean equinox of date)
|
|
30
42
|
def ecliptic
|
|
31
43
|
@ecliptic ||= begin
|
|
32
44
|
return Coordinates::Ecliptic.zero if distance.zero?
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
# TEME (True Equator, Mean Equinox) reference frame. This is the output
|
|
5
|
+
# frame of the SGP4/SDP4 satellite orbit propagators. Provides conversions
|
|
6
|
+
# to ECEF, GCRS, and topocentric frames.
|
|
7
|
+
class Teme < ReferenceFrame
|
|
8
|
+
# ECEF position and velocity vectors.
|
|
9
|
+
class EcefCoordinates
|
|
10
|
+
# @return [Astronoby::Vector<Astronoby::Distance>] ECEF position
|
|
11
|
+
attr_reader :position
|
|
12
|
+
|
|
13
|
+
# @return [Astronoby::Vector<Astronoby::Velocity>] ECEF velocity
|
|
14
|
+
attr_reader :velocity
|
|
15
|
+
|
|
16
|
+
# @param position [Astronoby::Vector<Astronoby::Distance>] ECEF position
|
|
17
|
+
# @param velocity [Astronoby::Vector<Astronoby::Velocity>] ECEF velocity
|
|
18
|
+
def initialize(position:, velocity:)
|
|
19
|
+
@position = position
|
|
20
|
+
@velocity = velocity
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Converts the ECEF position to WGS-84 geodetic coordinates using
|
|
25
|
+
# Bowring's iterative method.
|
|
26
|
+
#
|
|
27
|
+
# @return [Astronoby::Coordinates::Geodetic] geodetic coordinates
|
|
28
|
+
def geodetic
|
|
29
|
+
Coordinates::Geodetic.from_ecef(@position)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param position [Astronoby::Vector<Astronoby::Distance>] TEME position
|
|
34
|
+
# @param velocity [Astronoby::Vector<Astronoby::Velocity>] TEME velocity
|
|
35
|
+
# @param instant [Astronoby::Instant] the time instant
|
|
36
|
+
def initialize(position:, velocity:, instant:)
|
|
37
|
+
super(
|
|
38
|
+
position: position,
|
|
39
|
+
velocity: velocity,
|
|
40
|
+
instant: instant,
|
|
41
|
+
center: Center.geocentric,
|
|
42
|
+
target_body: nil
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Converts TEME position and velocity to ECEF using the canonical
|
|
47
|
+
# Vallado formulation with R₃(GMST).
|
|
48
|
+
#
|
|
49
|
+
# Velocity includes the ω×r transport term to account for Earth
|
|
50
|
+
# rotation.
|
|
51
|
+
#
|
|
52
|
+
# @return [Astronoby::Teme::EcefCoordinates] ECEF position and velocity
|
|
53
|
+
def to_ecef
|
|
54
|
+
mean_rotation_matrix = EarthRotation.mean_matrix_for(@instant).transpose
|
|
55
|
+
|
|
56
|
+
position = mean_rotation_matrix * @position.map(&:m)
|
|
57
|
+
velocity_mps = mean_rotation_matrix * @velocity.map(&:mps)
|
|
58
|
+
|
|
59
|
+
omega = Constants::EARTH_ANGULAR_VELOCITY_RAD_PER_S
|
|
60
|
+
corrected_vel = ::Vector[
|
|
61
|
+
velocity_mps[0] + omega * position[1],
|
|
62
|
+
velocity_mps[1] - omega * position[0],
|
|
63
|
+
velocity_mps[2]
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
EcefCoordinates.new(
|
|
67
|
+
position: Distance.vector_from_meters(position),
|
|
68
|
+
velocity: Velocity.vector_from_mps(corrected_vel)
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Converts TEME to GCRS using the pragmatic approach: reuse existing
|
|
73
|
+
# IAU 2006/2000B precession and nutation matrices transposed.
|
|
74
|
+
#
|
|
75
|
+
# The transformation chain is: r_GCRS = PBᵀ * Nᵀ * R₃(EoE) * r_TEME
|
|
76
|
+
#
|
|
77
|
+
# @return [Astronoby::Astrometric] GCRS Earth-centered frame
|
|
78
|
+
def to_gcrs
|
|
79
|
+
Astrometric.new(
|
|
80
|
+
position: Distance.vector_from_meters(
|
|
81
|
+
gcrs_rotation_matrix * @position.map(&:m)
|
|
82
|
+
),
|
|
83
|
+
velocity: Velocity.vector_from_mps(
|
|
84
|
+
gcrs_rotation_matrix * @velocity.map(&:mps)
|
|
85
|
+
),
|
|
86
|
+
instant: @instant,
|
|
87
|
+
center: Center.geocentric,
|
|
88
|
+
target_body: nil
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Converts TEME to topocentric coordinates as seen from an observer.
|
|
93
|
+
#
|
|
94
|
+
# Both satellite (via equation of equinoxes) and observer (via
|
|
95
|
+
# R₃(GAST) * W) are placed in the TOD frame, then subtracted.
|
|
96
|
+
#
|
|
97
|
+
# @param observer [Astronoby::Observer] the observer
|
|
98
|
+
# @return [Astronoby::Topocentric] topocentric frame
|
|
99
|
+
def observed_by(observer)
|
|
100
|
+
satellite_position = Distance.vector_from_meters(
|
|
101
|
+
equation_of_equinoxes_matrix * @position.map(&:m)
|
|
102
|
+
)
|
|
103
|
+
satellite_velocity = Velocity.vector_from_mps(
|
|
104
|
+
equation_of_equinoxes_matrix * @velocity.map(&:mps)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
matrix = observer.earth_fixed_rotation_matrix_for(@instant)
|
|
108
|
+
observer_position = Distance.vector_from_meters(
|
|
109
|
+
matrix * observer.geocentric_position.map(&:m)
|
|
110
|
+
)
|
|
111
|
+
observer_velocity = Velocity.vector_from_mps(
|
|
112
|
+
matrix * observer.geocentric_velocity.map(&:mps)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
Topocentric.new(
|
|
116
|
+
position: satellite_position - observer_position,
|
|
117
|
+
velocity: satellite_velocity - observer_velocity,
|
|
118
|
+
instant: @instant,
|
|
119
|
+
center: Center.topocentric(observer),
|
|
120
|
+
target_body: nil,
|
|
121
|
+
observer: observer
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def gcrs_rotation_matrix
|
|
128
|
+
@gcrs_rotation_matrix ||= begin
|
|
129
|
+
precession_matrix = Precession.matrix_for(@instant)
|
|
130
|
+
nutation_matrix = Nutation.matrix_for(@instant)
|
|
131
|
+
precession_matrix.transpose *
|
|
132
|
+
nutation_matrix.transpose *
|
|
133
|
+
equation_of_equinoxes_matrix
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def equation_of_equinoxes_matrix
|
|
138
|
+
@equation_of_equinoxes_matrix ||= begin
|
|
139
|
+
nutation = Nutation.new(instant: @instant)
|
|
140
|
+
dpsi = nutation.nutation_in_longitude
|
|
141
|
+
mean_obliquity = MeanObliquity.at(@instant)
|
|
142
|
+
eoe = dpsi.radians * mean_obliquity.cos
|
|
143
|
+
|
|
144
|
+
c, s = Math.cos(eoe), Math.sin(eoe)
|
|
145
|
+
Matrix[
|
|
146
|
+
[c, -s, 0],
|
|
147
|
+
[s, c, 0],
|
|
148
|
+
[0, 0, 1]
|
|
149
|
+
]
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Topocentric reference frame. Represents a body's position as seen from a
|
|
5
|
+
# specific observer on Earth's surface, accounting for the observer's
|
|
6
|
+
# geocentric position and Earth rotation.
|
|
4
7
|
class Topocentric < ReferenceFrame
|
|
8
|
+
# Builds a topocentric frame from an apparent frame and observer.
|
|
9
|
+
#
|
|
10
|
+
# @param apparent [Astronoby::Apparent] the apparent frame
|
|
11
|
+
# @param observer [Astronoby::Observer] the observer
|
|
12
|
+
# @param instant [Astronoby::Instant] the time instant
|
|
13
|
+
# @param target_body [Astronoby::Body, nil] the target body
|
|
14
|
+
# @return [Astronoby::Topocentric] a new topocentric frame
|
|
5
15
|
def self.build_from_apparent(
|
|
6
16
|
apparent:,
|
|
7
17
|
observer:,
|
|
@@ -24,17 +34,23 @@ module Astronoby
|
|
|
24
34
|
position: position,
|
|
25
35
|
velocity: velocity,
|
|
26
36
|
instant: instant,
|
|
27
|
-
|
|
37
|
+
center: Center.topocentric(observer),
|
|
28
38
|
target_body: target_body,
|
|
29
39
|
observer: observer
|
|
30
40
|
)
|
|
31
41
|
end
|
|
32
42
|
|
|
43
|
+
# @param position [Astronoby::Vector<Astronoby::Distance>] position vector
|
|
44
|
+
# @param velocity [Astronoby::Vector<Astronoby::Velocity>] velocity vector
|
|
45
|
+
# @param instant [Astronoby::Instant] the time instant
|
|
46
|
+
# @param center [Astronoby::Center] the center of the frame
|
|
47
|
+
# @param target_body [Astronoby::Body, nil] the target body
|
|
48
|
+
# @param observer [Astronoby::Observer] the observer
|
|
33
49
|
def initialize(
|
|
34
50
|
position:,
|
|
35
51
|
velocity:,
|
|
36
52
|
instant:,
|
|
37
|
-
|
|
53
|
+
center:,
|
|
38
54
|
target_body:,
|
|
39
55
|
observer:
|
|
40
56
|
)
|
|
@@ -42,20 +58,30 @@ module Astronoby
|
|
|
42
58
|
position: position,
|
|
43
59
|
velocity: velocity,
|
|
44
60
|
instant: instant,
|
|
45
|
-
|
|
61
|
+
center: center,
|
|
46
62
|
target_body: target_body
|
|
47
63
|
)
|
|
48
64
|
@observer = observer
|
|
49
65
|
end
|
|
50
66
|
|
|
67
|
+
# @return [Astronoby::Coordinates::Ecliptic] ecliptic coordinates at the
|
|
68
|
+
# current instant (true ecliptic and equinox of date)
|
|
51
69
|
def ecliptic
|
|
52
70
|
@ecliptic ||= begin
|
|
53
71
|
return Coordinates::Ecliptic.zero if distance.zero?
|
|
54
72
|
|
|
55
|
-
equatorial.to_ecliptic(
|
|
73
|
+
equatorial.to_ecliptic(
|
|
74
|
+
instant: @instant,
|
|
75
|
+
obliquity: TrueObliquity.at(@instant)
|
|
76
|
+
)
|
|
56
77
|
end
|
|
57
78
|
end
|
|
58
79
|
|
|
80
|
+
# Converts to horizontal coordinates (azimuth/altitude).
|
|
81
|
+
#
|
|
82
|
+
# @param refraction [Boolean] whether to apply atmospheric refraction
|
|
83
|
+
# correction (default: false)
|
|
84
|
+
# @return [Astronoby::Coordinates::Horizontal] horizontal coordinates
|
|
59
85
|
def horizontal(refraction: false)
|
|
60
86
|
horizontal = equatorial.to_horizontal(
|
|
61
87
|
time: @instant.to_time,
|
data/lib/astronoby/refraction.rb
CHANGED
|
@@ -1,27 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Computes atmospheric refraction corrections for horizontal coordinates.
|
|
5
|
+
#
|
|
6
|
+
# Source:
|
|
7
|
+
# Title: Practical Astronomy with your Calculator or Spreadsheet
|
|
8
|
+
# Authors: Peter Duffett-Smith and Jonathan Zwart
|
|
9
|
+
# Edition: Cambridge University Press
|
|
10
|
+
# Chapter: 37 - Refraction
|
|
4
11
|
class Refraction
|
|
5
12
|
LOW_ALTITUDE_BODY_ANGLE = Angle.from_degrees(15)
|
|
6
13
|
ZENITH = Angle.from_degrees(90)
|
|
7
14
|
|
|
15
|
+
# Computes the refraction angle for the given horizontal coordinates.
|
|
16
|
+
#
|
|
17
|
+
# @param coordinates [Astronoby::Coordinates::Horizontal] horizontal
|
|
18
|
+
# coordinates
|
|
19
|
+
# @return [Astronoby::Angle] the refraction angle
|
|
8
20
|
def self.angle(coordinates:)
|
|
9
21
|
new(coordinates).refraction_angle
|
|
10
22
|
end
|
|
11
23
|
|
|
24
|
+
# Returns horizontal coordinates corrected for atmospheric refraction.
|
|
25
|
+
#
|
|
26
|
+
# @param coordinates [Astronoby::Coordinates::Horizontal] horizontal
|
|
27
|
+
# coordinates
|
|
28
|
+
# @return [Astronoby::Coordinates::Horizontal] corrected coordinates
|
|
12
29
|
def self.correct_horizontal_coordinates(coordinates:)
|
|
13
30
|
new(coordinates).refract
|
|
14
31
|
end
|
|
15
32
|
|
|
33
|
+
# @param coordinates [Astronoby::Coordinates::Horizontal] horizontal
|
|
34
|
+
# coordinates
|
|
16
35
|
def initialize(coordinates)
|
|
17
36
|
@coordinates = coordinates
|
|
18
37
|
end
|
|
19
38
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# Edition: Cambridge University Press
|
|
24
|
-
# Chapter: 37 - Refraction
|
|
39
|
+
# Returns horizontal coordinates with refraction applied to the altitude.
|
|
40
|
+
#
|
|
41
|
+
# @return [Astronoby::Coordinates::Horizontal] corrected coordinates
|
|
25
42
|
def refract
|
|
26
43
|
Coordinates::Horizontal.new(
|
|
27
44
|
azimuth: @coordinates.azimuth,
|
|
@@ -30,6 +47,10 @@ module Astronoby
|
|
|
30
47
|
)
|
|
31
48
|
end
|
|
32
49
|
|
|
50
|
+
# Computes the refraction angle based on the observer's atmospheric
|
|
51
|
+
# conditions and the body's altitude.
|
|
52
|
+
#
|
|
53
|
+
# @return [Astronoby::Angle] the refraction angle
|
|
33
54
|
def refraction_angle
|
|
34
55
|
if @coordinates.altitude > LOW_ALTITUDE_BODY_ANGLE
|
|
35
56
|
high_altitude_angle
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class RootFinder
|
|
5
|
+
MIN_SAMPLES_PER_PERIOD = 20
|
|
6
|
+
BISECTION_TOLERANCE_DAYS = 1e-5
|
|
7
|
+
|
|
8
|
+
# @param value_at [#call] callable mapping a Julian Date (Terrestrial Time)
|
|
9
|
+
# to a Float
|
|
10
|
+
# @param period [Float] the characteristic period of the quantity in days
|
|
11
|
+
# @param samples_per_period [Integer] number of samples per period
|
|
12
|
+
def initialize(value_at:, period:, samples_per_period: 60)
|
|
13
|
+
@value_at = value_at
|
|
14
|
+
@period = period
|
|
15
|
+
@samples_per_period = samples_per_period
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param start_jd [Float] start time in Julian Date (Terrestrial Time)
|
|
19
|
+
# @param end_jd [Float] end time in Julian Date (Terrestrial Time)
|
|
20
|
+
# @param accept [#call, nil] optional predicate on a located root (a Julian
|
|
21
|
+
# Date)
|
|
22
|
+
# @return [Array<Float>] root times in Julian Date (Terrestrial Time),
|
|
23
|
+
# sorted by time
|
|
24
|
+
def roots(start_jd, end_jd, accept: nil)
|
|
25
|
+
brackets = find_brackets(start_jd, end_jd)
|
|
26
|
+
located = brackets.map { |a, b| bisect(a, b) }
|
|
27
|
+
located.select { |jd| accept.nil? || accept.call(jd) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def find_brackets(start_jd, end_jd)
|
|
33
|
+
samples = collect_samples(start_jd, end_jd)
|
|
34
|
+
|
|
35
|
+
(0...samples.length - 1).filter_map do |i|
|
|
36
|
+
current = samples[i]
|
|
37
|
+
following = samples[i + 1]
|
|
38
|
+
|
|
39
|
+
[current[:jd], following[:jd]] if sign_change?(current, following)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def sign_change?(current, following)
|
|
44
|
+
current[:value] * following[:value] < 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def collect_samples(start_jd, end_jd)
|
|
48
|
+
duration = end_jd - start_jd
|
|
49
|
+
sample_count = sample_count_for(duration)
|
|
50
|
+
step = duration / sample_count
|
|
51
|
+
|
|
52
|
+
(0..sample_count).map do |i|
|
|
53
|
+
jd = start_jd + (i * step)
|
|
54
|
+
{jd: jd, value: @value_at.call(jd)}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sample_count_for(duration)
|
|
59
|
+
periods_in_range = duration / @period
|
|
60
|
+
base_samples = (periods_in_range * @samples_per_period).to_i
|
|
61
|
+
[base_samples, MIN_SAMPLES_PER_PERIOD].max
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def bisect(a, b)
|
|
65
|
+
f_a = @value_at.call(a)
|
|
66
|
+
|
|
67
|
+
while (b - a).abs > BISECTION_TOLERANCE_DAYS
|
|
68
|
+
midpoint = (a + b) / 2
|
|
69
|
+
f_midpoint = @value_at.call(midpoint)
|
|
70
|
+
return midpoint if f_midpoint.zero?
|
|
71
|
+
|
|
72
|
+
if f_a.negative? == f_midpoint.negative?
|
|
73
|
+
a = midpoint
|
|
74
|
+
f_a = f_midpoint
|
|
75
|
+
else
|
|
76
|
+
b = midpoint
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
(a + b) / 2
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "matrix"
|
|
4
|
+
|
|
5
|
+
module Astronoby
|
|
6
|
+
# Builds elementary rotation matrices about the coordinate axes from an
|
|
7
|
+
# +Astronoby::Angle+.
|
|
8
|
+
#
|
|
9
|
+
# These use the passive (frame) convention: the matrix expresses a fixed
|
|
10
|
+
# vector in a frame rotated by +angle+ about the axis. This is the convention
|
|
11
|
+
# used throughout the reference-frame chain (precession, nutation, body
|
|
12
|
+
# orientation).
|
|
13
|
+
module Rotation
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# @param angle [Astronoby::Angle] the rotation angle
|
|
17
|
+
# @return [Matrix] rotation about the x-axis
|
|
18
|
+
def about_x(angle)
|
|
19
|
+
cosine, sine = angle.cos, angle.sin
|
|
20
|
+
Matrix[
|
|
21
|
+
[1, 0, 0],
|
|
22
|
+
[0, cosine, sine],
|
|
23
|
+
[0, -sine, cosine]
|
|
24
|
+
]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param angle [Astronoby::Angle] the rotation angle
|
|
28
|
+
# @return [Matrix] rotation about the y-axis
|
|
29
|
+
def about_y(angle)
|
|
30
|
+
cosine, sine = angle.cos, angle.sin
|
|
31
|
+
Matrix[
|
|
32
|
+
[cosine, 0, -sine],
|
|
33
|
+
[0, 1, 0],
|
|
34
|
+
[sine, 0, cosine]
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param angle [Astronoby::Angle] the rotation angle
|
|
39
|
+
# @return [Matrix] rotation about the z-axis
|
|
40
|
+
def about_z(angle)
|
|
41
|
+
cosine, sine = angle.cos, angle.sin
|
|
42
|
+
Matrix[
|
|
43
|
+
[cosine, sine, 0],
|
|
44
|
+
[-sine, cosine, 0],
|
|
45
|
+
[0, 0, 1]
|
|
46
|
+
]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Greenwich Apparent Sidereal Time (GAST). Derived from GMST by applying
|
|
5
|
+
# the equation of the equinoxes (nutation correction).
|
|
4
6
|
class GreenwichApparentSiderealTime < GreenwichSiderealTime
|
|
7
|
+
# Creates GAST from UTC by computing GMST and applying the equation
|
|
8
|
+
# of the equinoxes.
|
|
9
|
+
#
|
|
10
|
+
# @param utc [Time] the UTC time
|
|
11
|
+
# @return [Astronoby::GreenwichApparentSiderealTime]
|
|
5
12
|
def self.from_utc(utc)
|
|
6
13
|
gmst = GreenwichMeanSiderealTime.from_utc(utc)
|
|
7
14
|
instant = Instant.from_time(utc)
|
|
@@ -15,6 +22,8 @@ module Astronoby
|
|
|
15
22
|
new(date: gmst.date, time: gast_time)
|
|
16
23
|
end
|
|
17
24
|
|
|
25
|
+
# @param date [Date] the calendar date
|
|
26
|
+
# @param time [Numeric] GAST in hours
|
|
18
27
|
def initialize(date:, time:)
|
|
19
28
|
super(date: date, time: time, type: APPARENT)
|
|
20
29
|
end
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "iers"
|
|
4
|
+
|
|
3
5
|
module Astronoby
|
|
6
|
+
# Greenwich Mean Sidereal Time (GMST). Computed from UTC using the IERS
|
|
7
|
+
# Conventions 2010 ERA-based expression, with a polynomial fallback for
|
|
8
|
+
# dates outside the IERS EOP data range.
|
|
4
9
|
class GreenwichMeanSiderealTime < GreenwichSiderealTime
|
|
5
10
|
JULIAN_CENTURIES_EXPONENTS = [
|
|
6
11
|
6.697374558,
|
|
@@ -10,13 +15,41 @@ module Astronoby
|
|
|
10
15
|
|
|
11
16
|
SIDEREAL_MINUTE_IN_UT_MINUTE = 0.9972695663
|
|
12
17
|
|
|
18
|
+
UT_TO_SIDEREAL_RATIO = 1.002737909
|
|
19
|
+
|
|
20
|
+
# Creates GMST from UTC using the IERS Conventions 2010 ERA-based
|
|
21
|
+
# expression.
|
|
22
|
+
#
|
|
23
|
+
# Source:
|
|
24
|
+
# Title: IERS Conventions (2010)
|
|
25
|
+
# Chapter: 5.5.7 - ERA based expressions for Greenwich Sidereal Time
|
|
26
|
+
#
|
|
27
|
+
# @param utc [Time] the UTC time
|
|
28
|
+
# @return [Astronoby::GreenwichMeanSiderealTime]
|
|
29
|
+
def self.from_utc(utc)
|
|
30
|
+
date = utc.to_date
|
|
31
|
+
gmst_hours = begin
|
|
32
|
+
gmst_radians = IERS::GMST.at(utc)
|
|
33
|
+
gmst_radians * 12.0 / Math::PI
|
|
34
|
+
rescue IERS::OutOfRangeError
|
|
35
|
+
from_utc_polynomial(utc)
|
|
36
|
+
end
|
|
37
|
+
gmst_hours = normalize_time(gmst_hours)
|
|
38
|
+
|
|
39
|
+
new(date: date, time: gmst_hours)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Polynomial fallback for dates outside the IERS EOP data range.
|
|
43
|
+
#
|
|
13
44
|
# Source:
|
|
14
45
|
# Title: Practical Astronomy with your Calculator or Spreadsheet
|
|
15
46
|
# Authors: Peter Duffett-Smith and Jonathan Zwart
|
|
16
47
|
# Edition: Cambridge University Press
|
|
17
48
|
# Chapter: 12 - Conversion of UT to Greenwich sidereal time (GST)
|
|
18
|
-
|
|
19
|
-
|
|
49
|
+
#
|
|
50
|
+
# @param utc [Time] the UTC time
|
|
51
|
+
# @return [Numeric] GMST in hours
|
|
52
|
+
def self.from_utc_polynomial(utc)
|
|
20
53
|
julian_day = utc.to_date.ajd
|
|
21
54
|
t = (julian_day - JulianDate::J2000) / Constants::DAYS_PER_JULIAN_CENTURY
|
|
22
55
|
t0 = (
|
|
@@ -29,20 +62,24 @@ module Astronoby
|
|
|
29
62
|
utc.min / Constants::MINUTES_PER_HOUR +
|
|
30
63
|
(utc.sec + utc.subsec) / Constants::SECONDS_PER_HOUR
|
|
31
64
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
new(date: date, time: gmst)
|
|
65
|
+
UT_TO_SIDEREAL_RATIO * ut_in_hours + t0
|
|
35
66
|
end
|
|
36
67
|
|
|
68
|
+
# @param date [Date] the calendar date
|
|
69
|
+
# @param time [Numeric] GMST in hours
|
|
37
70
|
def initialize(date:, time:)
|
|
38
71
|
super(date: date, time: time, type: MEAN)
|
|
39
72
|
end
|
|
40
73
|
|
|
74
|
+
# Converts GMST back to UTC.
|
|
75
|
+
#
|
|
41
76
|
# Source:
|
|
42
77
|
# Title: Practical Astronomy with your Calculator or Spreadsheet
|
|
43
78
|
# Authors: Peter Duffett-Smith and Jonathan Zwart
|
|
44
79
|
# Edition: Cambridge University Press
|
|
45
80
|
# Chapter: 13 - Conversion of GST to UT
|
|
81
|
+
#
|
|
82
|
+
# @return [Time] the UTC time
|
|
46
83
|
def to_utc
|
|
47
84
|
date = @date
|
|
48
85
|
julian_day = @date.ajd
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Greenwich Sidereal Time base class. Dispatches to mean or apparent
|
|
5
|
+
# subclasses.
|
|
4
6
|
class GreenwichSiderealTime < SiderealTime
|
|
7
|
+
# Creates a Greenwich Sidereal Time from UTC.
|
|
8
|
+
#
|
|
9
|
+
# @param utc [Time] the UTC time
|
|
10
|
+
# @param type [Symbol] :mean or :apparent
|
|
11
|
+
# @return [Astronoby::GreenwichMeanSiderealTime,
|
|
12
|
+
# Astronoby::GreenwichApparentSiderealTime]
|
|
5
13
|
def self.from_utc(utc, type: MEAN)
|
|
6
14
|
validate_type!(type)
|
|
7
15
|
case type
|
|
@@ -12,14 +20,22 @@ module Astronoby
|
|
|
12
20
|
end
|
|
13
21
|
end
|
|
14
22
|
|
|
23
|
+
# @param utc [Time] the UTC time
|
|
24
|
+
# @return [Astronoby::GreenwichMeanSiderealTime]
|
|
15
25
|
def self.mean_from_utc(utc)
|
|
16
26
|
GreenwichMeanSiderealTime.from_utc(utc)
|
|
17
27
|
end
|
|
18
28
|
|
|
29
|
+
# @param utc [Time] the UTC time
|
|
30
|
+
# @return [Astronoby::GreenwichApparentSiderealTime]
|
|
19
31
|
def self.apparent_from_utc(utc)
|
|
20
32
|
GreenwichApparentSiderealTime.from_utc(utc)
|
|
21
33
|
end
|
|
22
34
|
|
|
35
|
+
# Converts to UTC.
|
|
36
|
+
#
|
|
37
|
+
# @return [Time] the UTC time
|
|
38
|
+
# @raise [NotImplementedError] if this is apparent sidereal time
|
|
23
39
|
def to_utc
|
|
24
40
|
unless mean?
|
|
25
41
|
raise NotImplementedError,
|
|
@@ -30,6 +46,11 @@ module Astronoby
|
|
|
30
46
|
gmst.to_utc
|
|
31
47
|
end
|
|
32
48
|
|
|
49
|
+
# Converts to Local Sidereal Time for a given longitude.
|
|
50
|
+
#
|
|
51
|
+
# @param longitude [Astronoby::Angle] the observer's longitude
|
|
52
|
+
# @return [Astronoby::LocalMeanSiderealTime,
|
|
53
|
+
# Astronoby::LocalApparentSiderealTime]
|
|
33
54
|
def to_lst(longitude:)
|
|
34
55
|
LocalSiderealTime.from_gst(gst: self, longitude: longitude)
|
|
35
56
|
end
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Local Apparent Sidereal Time (LAST). Derived from GAST by adding the
|
|
5
|
+
# observer's longitude.
|
|
4
6
|
class LocalApparentSiderealTime < LocalSiderealTime
|
|
7
|
+
# Creates LAST from a Greenwich Apparent Sidereal Time and longitude.
|
|
8
|
+
#
|
|
5
9
|
# Source:
|
|
6
10
|
# Title: Practical Astronomy with your Calculator or Spreadsheet
|
|
7
11
|
# Authors: Peter Duffett-Smith and Jonathan Zwart
|
|
8
12
|
# Edition: Cambridge University Press
|
|
9
13
|
# Chapter: 14 - Local sidereal time (LST)
|
|
14
|
+
#
|
|
15
|
+
# @param gst [Astronoby::GreenwichApparentSiderealTime] GAST
|
|
16
|
+
# @param longitude [Astronoby::Angle] the observer's longitude
|
|
17
|
+
# @return [Astronoby::LocalApparentSiderealTime]
|
|
18
|
+
# @raise [ArgumentError] if gst is not apparent sidereal time
|
|
10
19
|
def self.from_gst(gst:, longitude:)
|
|
11
20
|
unless gst.apparent?
|
|
12
21
|
raise ArgumentError, "GST must be apparent sidereal time"
|
|
@@ -18,20 +27,32 @@ module Astronoby
|
|
|
18
27
|
new(date: date, time: time, longitude: longitude)
|
|
19
28
|
end
|
|
20
29
|
|
|
30
|
+
# Creates LAST from UTC and longitude.
|
|
31
|
+
#
|
|
32
|
+
# @param utc [Time] the UTC time
|
|
33
|
+
# @param longitude [Astronoby::Angle] the observer's longitude
|
|
34
|
+
# @return [Astronoby::LocalApparentSiderealTime]
|
|
21
35
|
def self.from_utc(utc, longitude:)
|
|
22
36
|
gast = GreenwichApparentSiderealTime.from_utc(utc)
|
|
23
37
|
from_gst(gst: gast, longitude: longitude)
|
|
24
38
|
end
|
|
25
39
|
|
|
40
|
+
# @param date [Date] the calendar date
|
|
41
|
+
# @param time [Numeric] LAST in hours
|
|
42
|
+
# @param longitude [Astronoby::Angle] the observer's longitude
|
|
26
43
|
def initialize(date:, time:, longitude:)
|
|
27
44
|
super(date: date, time: time, longitude: longitude, type: APPARENT)
|
|
28
45
|
end
|
|
29
46
|
|
|
47
|
+
# Converts to Greenwich Apparent Sidereal Time.
|
|
48
|
+
#
|
|
30
49
|
# Source:
|
|
31
50
|
# Title: Practical Astronomy with your Calculator or Spreadsheet
|
|
32
51
|
# Authors: Peter Duffett-Smith and Jonathan Zwart
|
|
33
52
|
# Edition: Cambridge University Press
|
|
34
53
|
# Chapter: 15 - Converting LST to GST
|
|
54
|
+
#
|
|
55
|
+
# @return [Astronoby::GreenwichApparentSiderealTime]
|
|
35
56
|
def to_gst
|
|
36
57
|
date = @date
|
|
37
58
|
time = normalize_time(@time - @longitude.hours)
|