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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +101 -0
  4. data/README.md +6 -1
  5. data/UPGRADING.md +84 -0
  6. data/docs/README.md +80 -15
  7. data/docs/angles.md +1 -0
  8. data/docs/configuration.md +20 -17
  9. data/docs/coordinates.md +72 -12
  10. data/docs/deep_sky_bodies.md +1 -1
  11. data/docs/ephem.md +5 -2
  12. data/docs/equinoxes_solstices_times.md +4 -3
  13. data/docs/glossary.md +97 -1
  14. data/docs/iers.md +40 -0
  15. data/docs/instant.md +20 -15
  16. data/docs/lunar_eclipses.md +93 -0
  17. data/docs/lunar_observation.md +87 -0
  18. data/docs/moon_phases.md +4 -1
  19. data/docs/observer.md +20 -6
  20. data/docs/planetary_phenomena.md +78 -0
  21. data/docs/reference_frames.md +192 -34
  22. data/docs/rise_transit_set_times.md +6 -4
  23. data/docs/solar_system_bodies.md +26 -4
  24. data/docs/twilight_times.md +25 -21
  25. data/lib/astronoby/angle.rb +63 -2
  26. data/lib/astronoby/angles/dms.rb +18 -1
  27. data/lib/astronoby/angles/hms.rb +14 -1
  28. data/lib/astronoby/angular_velocity.rb +21 -0
  29. data/lib/astronoby/bodies/deep_sky_object.rb +6 -1
  30. data/lib/astronoby/bodies/deep_sky_object_position.rb +32 -17
  31. data/lib/astronoby/bodies/earth.rb +7 -44
  32. data/lib/astronoby/bodies/jupiter.rb +10 -0
  33. data/lib/astronoby/bodies/mars.rb +10 -0
  34. data/lib/astronoby/bodies/mercury.rb +10 -0
  35. data/lib/astronoby/bodies/moon.rb +158 -32
  36. data/lib/astronoby/bodies/neptune.rb +10 -0
  37. data/lib/astronoby/bodies/saturn.rb +10 -0
  38. data/lib/astronoby/bodies/solar_system_body.rb +240 -61
  39. data/lib/astronoby/bodies/sun.rb +79 -4
  40. data/lib/astronoby/bodies/uranus.rb +10 -0
  41. data/lib/astronoby/bodies/venus.rb +10 -0
  42. data/lib/astronoby/body.rb +6 -0
  43. data/lib/astronoby/center.rb +84 -0
  44. data/lib/astronoby/constellation.rb +9 -1
  45. data/lib/astronoby/coordinates/ecliptic.rb +10 -1
  46. data/lib/astronoby/coordinates/equatorial.rb +64 -8
  47. data/lib/astronoby/coordinates/geodetic.rb +102 -0
  48. data/lib/astronoby/coordinates/horizontal.rb +13 -1
  49. data/lib/astronoby/distance.rb +35 -0
  50. data/lib/astronoby/duration.rb +116 -0
  51. data/lib/astronoby/earth_rotation.rb +70 -0
  52. data/lib/astronoby/equinox_solstice.rb +31 -8
  53. data/lib/astronoby/errors.rb +11 -0
  54. data/lib/astronoby/events/conjunction.rb +51 -0
  55. data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
  56. data/lib/astronoby/events/eclipse_phase.rb +27 -0
  57. data/lib/astronoby/events/extremum_calculator.rb +23 -176
  58. data/lib/astronoby/events/greatest_elongation.rb +58 -0
  59. data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
  60. data/lib/astronoby/events/lunar_eclipse.rb +99 -0
  61. data/lib/astronoby/events/lunar_eclipse_calculator.rb +285 -0
  62. data/lib/astronoby/events/opposition.rb +19 -0
  63. data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
  64. data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
  65. data/lib/astronoby/events/twilight_event.rb +24 -6
  66. data/lib/astronoby/events/twilight_events.rb +26 -6
  67. data/lib/astronoby/extremum_finder.rb +148 -0
  68. data/lib/astronoby/instant.rb +10 -7
  69. data/lib/astronoby/libration.rb +25 -0
  70. data/lib/astronoby/mean_obliquity.rb +8 -0
  71. data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
  72. data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
  73. data/lib/astronoby/nutation.rb +10 -20
  74. data/lib/astronoby/observer.rb +67 -49
  75. data/lib/astronoby/orientation.rb +107 -0
  76. data/lib/astronoby/position.rb +16 -0
  77. data/lib/astronoby/precession.rb +61 -60
  78. data/lib/astronoby/reference_frame.rb +73 -7
  79. data/lib/astronoby/reference_frames/apparent.rb +26 -7
  80. data/lib/astronoby/reference_frames/astrometric.rb +14 -1
  81. data/lib/astronoby/reference_frames/geometric.rb +7 -1
  82. data/lib/astronoby/reference_frames/mean_of_date.rb +13 -1
  83. data/lib/astronoby/reference_frames/teme.rb +153 -0
  84. data/lib/astronoby/reference_frames/topocentric.rb +30 -4
  85. data/lib/astronoby/refraction.rb +26 -5
  86. data/lib/astronoby/root_finder.rb +83 -0
  87. data/lib/astronoby/rotation.rb +49 -0
  88. data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +9 -0
  89. data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +42 -5
  90. data/lib/astronoby/time/greenwich_sidereal_time.rb +21 -0
  91. data/lib/astronoby/time/local_apparent_sidereal_time.rb +21 -0
  92. data/lib/astronoby/time/local_mean_sidereal_time.rb +21 -0
  93. data/lib/astronoby/time/local_sidereal_time.rb +24 -0
  94. data/lib/astronoby/time/sidereal_time.rb +23 -1
  95. data/lib/astronoby/true_obliquity.rb +4 -0
  96. data/lib/astronoby/util/maths.rb +8 -0
  97. data/lib/astronoby/util/time.rb +10 -485
  98. data/lib/astronoby/vector.rb +10 -0
  99. data/lib/astronoby/velocity.rb +39 -0
  100. data/lib/astronoby/version.rb +1 -1
  101. data/lib/astronoby.rb +22 -0
  102. 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
- center_identifier: SolarSystemBody::SOLAR_SYSTEM_BARYCENTER,
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
- center_identifier: SolarSystemBody::EARTH,
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
- center_identifier: [observer.longitude, observer.latitude],
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
- center_identifier:,
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
- center_identifier: center_identifier,
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(instant: @instant)
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,
@@ -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
- # Source:
21
- # Title: Practical Astronomy with your Calculator or Spreadsheet
22
- # Authors: Peter Duffett-Smith and Jonathan Zwart
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
- def self.from_utc(utc)
19
- date = utc.to_date
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
- gmst = normalize_time(1.002737909 * ut_in_hours + t0)
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)