astronoby 0.8.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 +159 -0
- data/README.md +12 -5
- data/UPGRADING.md +109 -0
- data/docs/README.md +109 -16
- data/docs/angles.md +2 -1
- data/docs/configuration.md +20 -17
- data/docs/coordinates.md +73 -13
- data/docs/deep_sky_bodies.md +101 -0
- data/docs/ephem.md +6 -3
- data/docs/equinoxes_solstices_times.md +4 -3
- data/docs/glossary.md +97 -1
- data/docs/iers.md +40 -0
- data/docs/instant.md +21 -16
- data/docs/lunar_eclipses.md +93 -0
- data/docs/lunar_observation.md +87 -0
- data/docs/moon_phases.md +5 -2
- data/docs/observer.md +21 -7
- data/docs/planetary_phenomena.md +78 -0
- data/docs/reference_frames.md +193 -35
- data/docs/rise_transit_set_times.md +10 -8
- data/docs/{celestial_bodies.md → solar_system_bodies.md} +27 -5
- data/docs/twilight_times.md +25 -21
- data/lib/astronoby/angle.rb +69 -4
- data/lib/astronoby/angles/dms.rb +18 -1
- data/lib/astronoby/angles/hms.rb +14 -1
- data/lib/astronoby/angular_velocity.rb +97 -0
- data/lib/astronoby/bodies/deep_sky_object.rb +49 -0
- data/lib/astronoby/bodies/deep_sky_object_position.rb +142 -0
- data/lib/astronoby/bodies/earth.rb +9 -42
- 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 +162 -15
- data/lib/astronoby/bodies/neptune.rb +10 -0
- data/lib/astronoby/bodies/saturn.rb +10 -0
- data/lib/astronoby/bodies/solar_system_body.rb +257 -53
- 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 +1 -0
- data/lib/astronoby/center.rb +84 -0
- data/lib/astronoby/constants.rb +7 -2
- data/lib/astronoby/constellation.rb +9 -1
- data/lib/astronoby/coordinates/ecliptic.rb +10 -1
- data/lib/astronoby/coordinates/equatorial.rb +66 -13
- data/lib/astronoby/coordinates/geodetic.rb +102 -0
- data/lib/astronoby/coordinates/horizontal.rb +13 -1
- data/lib/astronoby/distance.rb +41 -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 +80 -0
- data/lib/astronoby/events/extremum_event.rb +15 -0
- 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_calculator.rb +9 -6
- 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 +1 -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 +35 -9
- 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 +25 -16
- 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 +31 -5
- 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/stellar_propagation.rb +162 -0
- data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +31 -0
- data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +101 -0
- data/lib/astronoby/time/greenwich_sidereal_time.rb +41 -58
- data/lib/astronoby/time/local_apparent_sidereal_time.rb +63 -0
- data/lib/astronoby/time/local_mean_sidereal_time.rb +63 -0
- data/lib/astronoby/time/local_sidereal_time.rb +59 -26
- data/lib/astronoby/time/sidereal_time.rb +64 -0
- data/lib/astronoby/true_obliquity.rb +4 -0
- data/lib/astronoby/util/maths.rb +8 -0
- data/lib/astronoby/util/time.rb +10 -467
- data/lib/astronoby/vector.rb +10 -0
- data/lib/astronoby/velocity.rb +44 -0
- data/lib/astronoby/version.rb +1 -1
- data/lib/astronoby.rb +33 -0
- metadata +58 -6
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Apparent reference frame. Represents a body's geocentric position as it
|
|
5
|
+
# appears from Earth, corrected for aberration, precession, and nutation.
|
|
4
6
|
class Apparent < ReferenceFrame
|
|
7
|
+
# Builds an apparent frame from an astrometric frame by applying
|
|
8
|
+
# aberration in GCRS, then rotating by nutation * precession (with ICRS
|
|
9
|
+
# frame bias).
|
|
10
|
+
#
|
|
11
|
+
# @param instant [Astronoby::Instant] the time instant
|
|
12
|
+
# @param target_astrometric [Astronoby::Astrometric] target's astrometric
|
|
13
|
+
# frame
|
|
14
|
+
# @param earth_geometric [Astronoby::Geometric] Earth's geometric frame
|
|
15
|
+
# @param target_body [Astronoby::Body, nil] the target body
|
|
16
|
+
# @return [Astronoby::Apparent] a new apparent frame
|
|
5
17
|
def self.build_from_astrometric(
|
|
6
18
|
instant:,
|
|
7
19
|
target_astrometric:,
|
|
@@ -13,46 +25,43 @@ module Astronoby
|
|
|
13
25
|
precession_matrix = Precession.matrix_for(instant)
|
|
14
26
|
nutation_matrix = Nutation.matrix_for(instant)
|
|
15
27
|
|
|
16
|
-
corrected_position = Distance.vector_from_meters(
|
|
17
|
-
precession_matrix * nutation_matrix * position.map(&:m)
|
|
18
|
-
)
|
|
19
28
|
corrected_position = Aberration.new(
|
|
20
|
-
astrometric_position:
|
|
29
|
+
astrometric_position: position,
|
|
21
30
|
observer_velocity: earth_geometric.velocity
|
|
22
31
|
).corrected_position
|
|
32
|
+
|
|
23
33
|
# In theory, here we should also apply light deflection. However, so far
|
|
24
34
|
# the deflection algorithm hasn't shown any significant changes to the
|
|
25
35
|
# apparent position. Therefore, for now, we are saving some computation
|
|
26
36
|
# time by not applying it, and we will investigate if the algorithm is
|
|
27
37
|
# correct or if the deflection is indeed negligible.
|
|
28
38
|
|
|
39
|
+
corrected_position = Distance.vector_from_meters(
|
|
40
|
+
nutation_matrix * precession_matrix * corrected_position.map(&:m)
|
|
41
|
+
)
|
|
42
|
+
|
|
29
43
|
corrected_velocity = Velocity.vector_from_mps(
|
|
30
|
-
|
|
44
|
+
nutation_matrix * precession_matrix * velocity.map(&:mps)
|
|
31
45
|
)
|
|
32
46
|
|
|
33
47
|
new(
|
|
34
48
|
position: corrected_position,
|
|
35
49
|
velocity: corrected_velocity,
|
|
36
50
|
instant: instant,
|
|
37
|
-
|
|
51
|
+
center: Center.geocentric,
|
|
38
52
|
target_body: target_body
|
|
39
53
|
)
|
|
40
54
|
end
|
|
41
55
|
|
|
56
|
+
# @return [Astronoby::Coordinates::Ecliptic] ecliptic coordinates at the
|
|
57
|
+
# current instant (true ecliptic and equinox of date)
|
|
42
58
|
def ecliptic
|
|
43
59
|
@ecliptic ||= begin
|
|
44
60
|
return Coordinates::Ecliptic.zero if distance.zero?
|
|
45
61
|
|
|
46
|
-
equatorial.to_ecliptic(
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
62
|
+
equatorial.to_ecliptic(
|
|
63
|
+
instant: @instant,
|
|
64
|
+
obliquity: TrueObliquity.at(@instant)
|
|
56
65
|
)
|
|
57
66
|
end
|
|
58
67
|
end
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Astrometric reference frame (GCRS). Represents a body's position relative
|
|
5
|
+
# to the Earth, corrected for light-time delay.
|
|
4
6
|
class Astrometric < ReferenceFrame
|
|
7
|
+
# Builds an astrometric frame from geometric frames with light-time
|
|
8
|
+
# correction.
|
|
9
|
+
#
|
|
10
|
+
# @param instant [Astronoby::Instant] the time instant
|
|
11
|
+
# @param earth_geometric [Astronoby::Geometric] Earth's geometric frame
|
|
12
|
+
# @param light_time_corrected_position [Astronoby::Vector<Astronoby::Distance>]
|
|
13
|
+
# target position corrected for light-time delay
|
|
14
|
+
# @param light_time_corrected_velocity [Astronoby::Vector<Astronoby::Velocity>]
|
|
15
|
+
# target velocity corrected for light-time delay
|
|
16
|
+
# @param target_body [Astronoby::Body, nil] the target body
|
|
17
|
+
# @return [Astronoby::Astrometric] a new astrometric frame
|
|
5
18
|
def self.build_from_geometric(
|
|
6
19
|
instant:,
|
|
7
20
|
earth_geometric:,
|
|
@@ -13,7 +26,7 @@ module Astronoby
|
|
|
13
26
|
position: light_time_corrected_position - earth_geometric.position,
|
|
14
27
|
velocity: light_time_corrected_velocity - earth_geometric.velocity,
|
|
15
28
|
instant: instant,
|
|
16
|
-
|
|
29
|
+
center: Center.geocentric,
|
|
17
30
|
target_body: target_body
|
|
18
31
|
)
|
|
19
32
|
end
|
|
@@ -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:,
|
|
@@ -14,7 +24,7 @@ module Astronoby
|
|
|
14
24
|
matrix * observer.geocentric_position.map(&:m)
|
|
15
25
|
)
|
|
16
26
|
observer_velocity = Velocity.vector_from_mps(
|
|
17
|
-
matrix * observer.geocentric_velocity.map(&:
|
|
27
|
+
matrix * observer.geocentric_velocity.map(&:mps)
|
|
18
28
|
)
|
|
19
29
|
|
|
20
30
|
position = apparent.position - observer_position
|
|
@@ -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
|