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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +235 -0
  4. data/README.md +19 -1
  5. data/UPGRADING.md +270 -0
  6. data/docs/README.md +105 -16
  7. data/docs/altitude_crossings.md +92 -0
  8. data/docs/angles.md +1 -0
  9. data/docs/configuration.md +28 -17
  10. data/docs/coordinates.md +72 -12
  11. data/docs/deep_sky_bodies.md +46 -1
  12. data/docs/ephem.md +5 -2
  13. data/docs/equinoxes_solstices_times.md +4 -3
  14. data/docs/glossary.md +99 -1
  15. data/docs/iers.md +40 -0
  16. data/docs/instant.md +33 -21
  17. data/docs/lunar_eclipses.md +208 -0
  18. data/docs/lunar_observation.md +87 -0
  19. data/docs/moon_phases.md +4 -1
  20. data/docs/observer.md +20 -6
  21. data/docs/planetary_phenomena.md +78 -0
  22. data/docs/reference_frames.md +192 -34
  23. data/docs/rise_transit_set_times.md +6 -4
  24. data/docs/solar_system_bodies.md +26 -4
  25. data/docs/twilight_times.md +43 -24
  26. data/lib/astronoby/angle.rb +63 -2
  27. data/lib/astronoby/angles/dms.rb +18 -1
  28. data/lib/astronoby/angles/hms.rb +14 -1
  29. data/lib/astronoby/angular_velocity.rb +21 -0
  30. data/lib/astronoby/bodies/deep_sky_object.rb +6 -1
  31. data/lib/astronoby/bodies/deep_sky_object_position.rb +35 -38
  32. data/lib/astronoby/bodies/earth.rb +7 -44
  33. data/lib/astronoby/bodies/jupiter.rb +10 -0
  34. data/lib/astronoby/bodies/mars.rb +10 -0
  35. data/lib/astronoby/bodies/mercury.rb +10 -0
  36. data/lib/astronoby/bodies/moon.rb +158 -32
  37. data/lib/astronoby/bodies/neptune.rb +10 -0
  38. data/lib/astronoby/bodies/saturn.rb +10 -0
  39. data/lib/astronoby/bodies/solar_system_body.rb +240 -61
  40. data/lib/astronoby/bodies/sun.rb +79 -4
  41. data/lib/astronoby/bodies/uranus.rb +10 -0
  42. data/lib/astronoby/bodies/venus.rb +10 -0
  43. data/lib/astronoby/body.rb +6 -0
  44. data/lib/astronoby/cache.rb +13 -0
  45. data/lib/astronoby/center.rb +84 -0
  46. data/lib/astronoby/constants.rb +1 -2
  47. data/lib/astronoby/constellation.rb +9 -1
  48. data/lib/astronoby/constellations/data.rb +1 -1
  49. data/lib/astronoby/coordinates/ecliptic.rb +10 -1
  50. data/lib/astronoby/coordinates/equatorial.rb +79 -13
  51. data/lib/astronoby/coordinates/geodetic.rb +102 -0
  52. data/lib/astronoby/coordinates/horizontal.rb +13 -1
  53. data/lib/astronoby/delta_t.rb +44 -0
  54. data/lib/astronoby/distance.rb +35 -0
  55. data/lib/astronoby/duration.rb +116 -0
  56. data/lib/astronoby/earth_rotation.rb +70 -0
  57. data/lib/astronoby/equinox_solstice.rb +31 -8
  58. data/lib/astronoby/errors.rb +11 -0
  59. data/lib/astronoby/events/altitude_crossing.rb +29 -0
  60. data/lib/astronoby/events/altitude_crossing_calculator.rb +511 -0
  61. data/lib/astronoby/events/altitude_extreme.rb +31 -0
  62. data/lib/astronoby/events/conjunction.rb +51 -0
  63. data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
  64. data/lib/astronoby/events/extremum_calculator.rb +23 -176
  65. data/lib/astronoby/events/greatest_elongation.rb +58 -0
  66. data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
  67. data/lib/astronoby/events/lunar_eclipse.rb +111 -0
  68. data/lib/astronoby/events/lunar_eclipse_calculator.rb +397 -0
  69. data/lib/astronoby/events/lunar_eclipse_geometry.rb +115 -0
  70. data/lib/astronoby/events/lunar_eclipse_phase.rb +45 -0
  71. data/lib/astronoby/events/lunar_eclipse_visibility.rb +296 -0
  72. data/lib/astronoby/events/opposition.rb +19 -0
  73. data/lib/astronoby/events/rise_transit_set_calculator.rb +9 -13
  74. data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
  75. data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
  76. data/lib/astronoby/events/twilight_calculator.rb +37 -147
  77. data/lib/astronoby/events/twilight_event.rb +24 -6
  78. data/lib/astronoby/events/twilight_events.rb +26 -6
  79. data/lib/astronoby/extremum_finder.rb +148 -0
  80. data/lib/astronoby/horizon.rb +40 -0
  81. data/lib/astronoby/horizontal_transformation.rb +112 -0
  82. data/lib/astronoby/instant.rb +56 -16
  83. data/lib/astronoby/libration.rb +25 -0
  84. data/lib/astronoby/mean_obliquity.rb +8 -0
  85. data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
  86. data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
  87. data/lib/astronoby/nutation.rb +10 -20
  88. data/lib/astronoby/observer.rb +67 -49
  89. data/lib/astronoby/orientation.rb +107 -0
  90. data/lib/astronoby/position.rb +16 -0
  91. data/lib/astronoby/precession.rb +62 -61
  92. data/lib/astronoby/reference_frame.rb +80 -9
  93. data/lib/astronoby/reference_frames/apparent.rb +43 -12
  94. data/lib/astronoby/reference_frames/astrometric.rb +14 -1
  95. data/lib/astronoby/reference_frames/geometric.rb +7 -1
  96. data/lib/astronoby/reference_frames/mean_of_date.rb +19 -1
  97. data/lib/astronoby/reference_frames/teme.rb +159 -0
  98. data/lib/astronoby/reference_frames/topocentric.rb +36 -4
  99. data/lib/astronoby/refraction.rb +26 -5
  100. data/lib/astronoby/root_finder.rb +90 -0
  101. data/lib/astronoby/rotation.rb +49 -0
  102. data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +19 -8
  103. data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +44 -5
  104. data/lib/astronoby/time/greenwich_sidereal_time.rb +21 -0
  105. data/lib/astronoby/time/local_apparent_sidereal_time.rb +21 -0
  106. data/lib/astronoby/time/local_mean_sidereal_time.rb +21 -0
  107. data/lib/astronoby/time/local_sidereal_time.rb +24 -0
  108. data/lib/astronoby/time/sidereal_time.rb +23 -1
  109. data/lib/astronoby/true_obliquity.rb +4 -0
  110. data/lib/astronoby/util/maths.rb +8 -0
  111. data/lib/astronoby/util/time.rb +0 -497
  112. data/lib/astronoby/vector.rb +10 -0
  113. data/lib/astronoby/velocity.rb +39 -0
  114. data/lib/astronoby/version.rb +1 -1
  115. data/lib/astronoby.rb +30 -0
  116. metadata +80 -5
@@ -1,88 +1,89 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Computes precession matrices using the IAU 2006 Fukushima-Williams
5
+ # angles with ICRS frame bias. Also provides coordinate precession
6
+ # using the classical method.
4
7
  class Precession
8
+ # Computes the combined precession-bias matrix for a given instant.
9
+ #
10
+ # @param instant [Astronoby::Instant] the time instant
11
+ # @return [Matrix] the 3x3 precession-bias matrix PB(t)
5
12
  def self.matrix_for(instant)
6
13
  new(instant: instant).matrix
7
14
  end
8
15
 
16
+ # @param instant [Astronoby::Instant] the time instant
9
17
  def initialize(instant:)
10
18
  @instant = instant
11
19
  end
12
20
 
21
+ # Computes the combined precession-bias matrix PB(t) = R1(-εA) R3(-ψ̄) R1(φ̄) R3(γ̄)
22
+ #
23
+ # @return [Matrix] the 3x3 precession-bias matrix
13
24
  def matrix
25
+ # Fukushima-Williams precession angles including frame bias.
14
26
  # Source:
15
- # IAU resolution in 2006 in favor of the P03 astronomical model
16
- # https://syrte.obspm.fr/iau2006/aa03_412_P03.pdf
17
- # P(t) = R3(χA) R1(−ωA) R3(−ψA) R1(ϵ0)
27
+ # IERS Conventions 2010, Section 5.6.4
28
+ # Wallace & Capitaine (2006), A&A 459, 981
29
+ # ERFA eraPfw06 / eraFw2m
30
+ # PB(t) = R1(−εA) R3(−ψ̄) R1(φ̄) R3(γ̄)
18
31
 
19
32
  cache.fetch(cache_key) do
20
- # Precession in right ascension
21
- psi_a = ((((
22
- -0.0000000951 * t +
23
- +0.000132851) * t +
24
- -0.00114045) * t +
25
- -1.0790069) * t +
26
- +5038.481507) * t
27
-
28
- # Precession in declination
29
- omega_a = ((((
30
- +0.0000003337 * t +
31
- -0.000000467) * t +
32
- -0.00772503) * t +
33
- +0.0512623) * t +
34
- -0.025754) * t +
35
- eps0
36
-
37
- # Precession of the ecliptic
38
- chi_a = ((((
39
- -0.0000000560 * t +
40
- +0.000170663) * t +
41
- -0.00121197) * t +
42
- -2.3814292) * t +
43
- +10.556403) * t
44
-
45
- psi_a = Angle.from_degree_arcseconds(psi_a)
46
- omega_a = Angle.from_degree_arcseconds(omega_a)
47
- chi_a = Angle.from_degree_arcseconds(chi_a)
48
-
49
- r3_psi = rotation_z(-psi_a)
50
- r1_omega = rotation_x(-omega_a)
51
- r3_chi = rotation_z(chi_a)
52
- r1_eps0 = rotation_x(MeanObliquity.obliquity_of_reference)
53
-
54
- r3_chi * r1_omega * r3_psi * r1_eps0
33
+ gamma_bar = ((((
34
+ +0.0000000260 * t +
35
+ -0.000002788) * t +
36
+ -0.00031238) * t +
37
+ +0.4932044) * t +
38
+ +10.556378) * t +
39
+ -0.052928
40
+
41
+ phi_bar = ((((
42
+ -0.0000000176 * t +
43
+ -0.000000440) * t +
44
+ +0.00053289) * t +
45
+ +0.0511268) * t +
46
+ -46.811016) * t +
47
+ +84381.412819
48
+
49
+ psi_bar = ((((
50
+ -0.0000000148 * t +
51
+ -0.000026452) * t +
52
+ -0.00018522) * t +
53
+ +1.5584175) * t +
54
+ +5038.481484) * t +
55
+ -0.041775
56
+
57
+ gamma_bar = Angle.from_degree_arcseconds(gamma_bar)
58
+ phi_bar = Angle.from_degree_arcseconds(phi_bar)
59
+ psi_bar = Angle.from_degree_arcseconds(psi_bar)
60
+ eps_a = MeanObliquity.at(@instant)
61
+
62
+ Rotation.about_x(-eps_a) * Rotation.about_z(-psi_bar) *
63
+ Rotation.about_x(phi_bar) * Rotation.about_z(gamma_bar)
55
64
  end
56
65
  end
57
66
 
58
- def rotation_x(angle)
59
- c, s = angle.cos, angle.sin
60
- Matrix[
61
- [1, 0, 0],
62
- [0, c, s],
63
- [0, -s, c]
64
- ]
65
- end
66
-
67
- def rotation_z(angle)
68
- c, s = angle.cos, angle.sin
69
- Matrix[
70
- [c, s, 0],
71
- [-s, c, 0],
72
- [0, 0, 1]
73
- ]
74
- end
75
-
76
67
  # Source:
77
68
  # Title: Practical Astronomy with your Calculator or Spreadsheet
78
69
  # Authors: Peter Duffett-Smith and Jonathan Zwart
79
70
  # Edition: Cambridge University Press
80
71
  # Chapter: 34 - Precession
81
72
 
73
+ # Precesses equatorial coordinates from their current epoch to a new epoch.
74
+ #
75
+ # @param coordinates [Astronoby::Coordinates::Equatorial] coordinates to
76
+ # precess
77
+ # @param epoch [Numeric] the target Julian Date epoch
78
+ # @return [Astronoby::Coordinates::Equatorial] precessed coordinates
82
79
  def self.for_equatorial_coordinates(coordinates:, epoch:)
83
80
  precess(coordinates, epoch)
84
81
  end
85
82
 
83
+ # @param coordinates [Astronoby::Coordinates::Equatorial] coordinates to
84
+ # precess
85
+ # @param epoch [Numeric] the target Julian Date epoch
86
+ # @return [Astronoby::Coordinates::Equatorial] precessed coordinates
86
87
  def self.precess(coordinates, epoch)
87
88
  matrix_a = matrix_for_epoch(coordinates.epoch)
88
89
  matrix_b = matrix_for_epoch(epoch).transpose
@@ -107,6 +108,10 @@ module Astronoby
107
108
  )
108
109
  end
109
110
 
111
+ # Computes the classical precession matrix for a given epoch.
112
+ #
113
+ # @param epoch [Numeric] a Julian Date epoch
114
+ # @return [Matrix] 3x3 precession matrix
110
115
  def self.matrix_for_epoch(epoch)
111
116
  t = (epoch - JulianDate::DEFAULT_EPOCH) / Constants::DAYS_PER_JULIAN_CENTURY
112
117
 
@@ -158,13 +163,9 @@ module Astronoby
158
163
 
159
164
  def t
160
165
  @t ||= Rational(
161
- @instant.tdb - JulianDate::DEFAULT_EPOCH,
166
+ @instant.tt - JulianDate::DEFAULT_EPOCH,
162
167
  Constants::DAYS_PER_JULIAN_CENTURY
163
168
  )
164
169
  end
165
-
166
- def eps0
167
- @eps0 ||= MeanObliquity.obliquity_of_reference_in_arcseconds
168
- end
169
170
  end
170
171
  end
@@ -1,35 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Base class for reference frames in the astronomical reference frame chain.
5
+ # Each frame represents a body's position and velocity relative to a center,
6
+ # at a specific instant.
4
7
  class ReferenceFrame
5
- attr_reader :position,
6
- :velocity,
7
- :instant,
8
- :center_identifier,
9
- :target_body
8
+ # @return [Astronoby::Vector<Astronoby::Distance>] position vector
9
+ attr_reader :position
10
10
 
11
+ # @return [Astronoby::Vector<Astronoby::Velocity>] velocity vector
12
+ attr_reader :velocity
13
+
14
+ # @return [Astronoby::Instant] the time instant
15
+ attr_reader :instant
16
+
17
+ # @return [Astronoby::Center] the center of the frame
18
+ attr_reader :center
19
+
20
+ # @return [Astronoby::Body, nil] the target body
21
+ attr_reader :target_body
22
+
23
+ # @param position [Astronoby::Vector<Astronoby::Distance>] position vector
24
+ # @param velocity [Astronoby::Vector<Astronoby::Velocity>] velocity vector
25
+ # @param instant [Astronoby::Instant] the time instant
26
+ # @param center [Astronoby::Center] the center of the frame
27
+ # @param target_body [Astronoby::Body, nil] the target body
11
28
  def initialize(
12
29
  position:,
13
30
  velocity:,
14
31
  instant:,
15
- center_identifier:,
32
+ center:,
16
33
  target_body:
17
34
  )
18
35
  @position = position
19
36
  @velocity = velocity
20
37
  @instant = instant
21
- @center_identifier = center_identifier
38
+ @center = center
22
39
  @target_body = target_body
23
40
  end
24
41
 
42
+ # @return [Numeric] Julian Date epoch
43
+ def epoch
44
+ JulianDate::DEFAULT_EPOCH
45
+ end
46
+
47
+ # @return [Astronoby::Coordinates::Equatorial] equatorial coordinates
48
+ # derived from the position vector, referred to the frame's epoch
25
49
  def equatorial
26
50
  @equatorial ||= begin
27
- return Coordinates::Equatorial.zero if distance.zero?
51
+ return Coordinates::Equatorial.zero(epoch: epoch) if distance.zero?
28
52
 
29
- Coordinates::Equatorial.from_position_vector(@position)
53
+ Coordinates::Equatorial.from_position_vector(@position, epoch: epoch)
30
54
  end
31
55
  end
32
56
 
57
+ # @return [Astronoby::Coordinates::Ecliptic] ecliptic coordinates derived
58
+ # from the equatorial coordinates at J2000.0
33
59
  def ecliptic
34
60
  @ecliptic ||= begin
35
61
  return Coordinates::Ecliptic.zero if distance.zero?
@@ -39,6 +65,7 @@ module Astronoby
39
65
  end
40
66
  end
41
67
 
68
+ # @return [Astronoby::Distance] the Euclidean distance from the center
42
69
  def distance
43
70
  @distance ||= begin
44
71
  return Distance.zero if @position.zero?
@@ -46,5 +73,49 @@ module Astronoby
46
73
  @position.magnitude
47
74
  end
48
75
  end
76
+
77
+ # @param other [Astronoby::ReferenceFrame] another frame at the same stage
78
+ # and instant
79
+ # @return [Astronoby::Angle] the angular separation, between 0° and 180°
80
+ # @raise [Astronoby::IncompatibleArgumentsError] if the frames cannot be
81
+ # meaningfully compared
82
+ def separation_from(other)
83
+ ensure_comparable!(other)
84
+ return Angle.zero if @position.zero? || other.position.zero?
85
+
86
+ position_vector = @position.map(&:m)
87
+ other_position_vector = other.position.map(&:m)
88
+ cross = Util::Maths.cross_product(position_vector, other_position_vector)
89
+ cross_magnitude = Math.sqrt(Util::Maths.dot_product(cross, cross))
90
+ dot = Util::Maths.dot_product(position_vector, other_position_vector)
91
+
92
+ Angle.from_radians(Math.atan2(cross_magnitude, dot))
93
+ end
94
+
95
+ private
96
+
97
+ def ensure_comparable!(other)
98
+ unless other.is_a?(ReferenceFrame)
99
+ raise IncompatibleArgumentsError,
100
+ "Expected an Astronoby::ReferenceFrame, got #{other.class}"
101
+ end
102
+
103
+ unless instance_of?(other.class)
104
+ raise IncompatibleArgumentsError,
105
+ "Cannot compute the separation between different reference frames " \
106
+ "(#{self.class} and #{other.class}); both frames must be at the " \
107
+ "same stage of the reference frame chain"
108
+ end
109
+
110
+ unless instant == other.instant
111
+ raise IncompatibleArgumentsError,
112
+ "Cannot compute the separation between frames at different instants"
113
+ end
114
+
115
+ unless center == other.center
116
+ raise IncompatibleArgumentsError,
117
+ "Cannot compute the separation between frames with different centers"
118
+ end
119
+ end
49
120
  end
50
121
  end
@@ -1,49 +1,80 @@
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). Without Earth's geometric frame there is no observer
10
+ # velocity to derive the aberration from, so only the rotation is applied.
11
+ #
12
+ # @param instant [Astronoby::Instant] the time instant
13
+ # @param target_astrometric [Astronoby::Astrometric] target's astrometric
14
+ # frame
15
+ # @param earth_geometric [Astronoby::Geometric, nil] Earth's geometric
16
+ # frame
17
+ # @param target_body [Astronoby::Body, nil] the target body
18
+ # @return [Astronoby::Apparent] a new apparent frame
5
19
  def self.build_from_astrometric(
6
20
  instant:,
7
21
  target_astrometric:,
8
- earth_geometric:,
9
- target_body:
22
+ target_body:,
23
+ earth_geometric: nil
10
24
  )
11
25
  position = target_astrometric.position
12
26
  velocity = target_astrometric.velocity
13
27
  precession_matrix = Precession.matrix_for(instant)
14
28
  nutation_matrix = Nutation.matrix_for(instant)
15
29
 
16
- corrected_position = Distance.vector_from_meters(
17
- precession_matrix * nutation_matrix * position.map(&:m)
18
- )
19
- corrected_position = Aberration.new(
20
- astrometric_position: corrected_position,
21
- observer_velocity: earth_geometric.velocity
22
- ).corrected_position
30
+ corrected_position = if earth_geometric
31
+ Aberration.new(
32
+ astrometric_position: position,
33
+ observer_velocity: earth_geometric.velocity
34
+ ).corrected_position
35
+ else
36
+ position
37
+ end
38
+
23
39
  # In theory, here we should also apply light deflection. However, so far
24
40
  # the deflection algorithm hasn't shown any significant changes to the
25
41
  # apparent position. Therefore, for now, we are saving some computation
26
42
  # time by not applying it, and we will investigate if the algorithm is
27
43
  # correct or if the deflection is indeed negligible.
28
44
 
45
+ corrected_position = Distance.vector_from_meters(
46
+ nutation_matrix * precession_matrix * corrected_position.map(&:m)
47
+ )
48
+
29
49
  corrected_velocity = Velocity.vector_from_mps(
30
- precession_matrix * nutation_matrix * velocity.map(&:mps)
50
+ nutation_matrix * precession_matrix * velocity.map(&:mps)
31
51
  )
32
52
 
33
53
  new(
34
54
  position: corrected_position,
35
55
  velocity: corrected_velocity,
36
56
  instant: instant,
37
- center_identifier: SolarSystemBody::EARTH,
57
+ center: Center.geocentric,
38
58
  target_body: target_body
39
59
  )
40
60
  end
41
61
 
62
+ # @return [Numeric] Julian Date epoch of the frame's coordinates, which
63
+ # are referred to the true equator and equinox of date
64
+ def epoch
65
+ instant.tt
66
+ end
67
+
68
+ # @return [Astronoby::Coordinates::Ecliptic] ecliptic coordinates at the
69
+ # current instant (true ecliptic and equinox of date)
42
70
  def ecliptic
43
71
  @ecliptic ||= begin
44
72
  return Coordinates::Ecliptic.zero if distance.zero?
45
73
 
46
- equatorial.to_ecliptic(instant: @instant)
74
+ equatorial.to_ecliptic(
75
+ instant: @instant,
76
+ obliquity: TrueObliquity.at(@instant)
77
+ )
47
78
  end
48
79
  end
49
80
  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
- center_identifier: SolarSystemBody::EARTH,
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
- 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,19 @@ 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 [Numeric] Julian Date epoch of the frame's coordinates, which
41
+ # are referred to the mean equator and equinox of date
42
+ def epoch
43
+ instant.tt
44
+ end
45
+
46
+ # @return [Astronoby::Coordinates::Ecliptic] ecliptic coordinates at the
47
+ # current instant (mean equinox of date)
30
48
  def ecliptic
31
49
  @ecliptic ||= begin
32
50
  return Coordinates::Ecliptic.zero if distance.zero?
@@ -0,0 +1,159 @@
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
+ # @return [Numeric] Julian Date epoch of the frame's coordinates, which
47
+ # are referred to the true equator and mean equinox of date
48
+ def epoch
49
+ instant.tt
50
+ end
51
+
52
+ # Converts TEME position and velocity to ECEF using the canonical
53
+ # Vallado formulation with R₃(GMST).
54
+ #
55
+ # Velocity includes the ω×r transport term to account for Earth
56
+ # rotation.
57
+ #
58
+ # @return [Astronoby::Teme::EcefCoordinates] ECEF position and velocity
59
+ def to_ecef
60
+ mean_rotation_matrix = EarthRotation.mean_matrix_for(@instant).transpose
61
+
62
+ position = mean_rotation_matrix * @position.map(&:m)
63
+ velocity_mps = mean_rotation_matrix * @velocity.map(&:mps)
64
+
65
+ omega = Constants::EARTH_ANGULAR_VELOCITY_RAD_PER_S
66
+ corrected_vel = ::Vector[
67
+ velocity_mps[0] + omega * position[1],
68
+ velocity_mps[1] - omega * position[0],
69
+ velocity_mps[2]
70
+ ]
71
+
72
+ EcefCoordinates.new(
73
+ position: Distance.vector_from_meters(position),
74
+ velocity: Velocity.vector_from_mps(corrected_vel)
75
+ )
76
+ end
77
+
78
+ # Converts TEME to GCRS using the pragmatic approach: reuse existing
79
+ # IAU 2006/2000B precession and nutation matrices transposed.
80
+ #
81
+ # The transformation chain is: r_GCRS = PBᵀ * Nᵀ * R₃(EoE) * r_TEME
82
+ #
83
+ # @return [Astronoby::Astrometric] GCRS Earth-centered frame
84
+ def to_gcrs
85
+ Astrometric.new(
86
+ position: Distance.vector_from_meters(
87
+ gcrs_rotation_matrix * @position.map(&:m)
88
+ ),
89
+ velocity: Velocity.vector_from_mps(
90
+ gcrs_rotation_matrix * @velocity.map(&:mps)
91
+ ),
92
+ instant: @instant,
93
+ center: Center.geocentric,
94
+ target_body: nil
95
+ )
96
+ end
97
+
98
+ # Converts TEME to topocentric coordinates as seen from an observer.
99
+ #
100
+ # Both satellite (via equation of equinoxes) and observer (via
101
+ # R₃(GAST) * W) are placed in the TOD frame, then subtracted.
102
+ #
103
+ # @param observer [Astronoby::Observer] the observer
104
+ # @return [Astronoby::Topocentric] topocentric frame
105
+ def observed_by(observer)
106
+ satellite_position = Distance.vector_from_meters(
107
+ equation_of_equinoxes_matrix * @position.map(&:m)
108
+ )
109
+ satellite_velocity = Velocity.vector_from_mps(
110
+ equation_of_equinoxes_matrix * @velocity.map(&:mps)
111
+ )
112
+
113
+ matrix = observer.earth_fixed_rotation_matrix_for(@instant)
114
+ observer_position = Distance.vector_from_meters(
115
+ matrix * observer.geocentric_position.map(&:m)
116
+ )
117
+ observer_velocity = Velocity.vector_from_mps(
118
+ matrix * observer.geocentric_velocity.map(&:mps)
119
+ )
120
+
121
+ Topocentric.new(
122
+ position: satellite_position - observer_position,
123
+ velocity: satellite_velocity - observer_velocity,
124
+ instant: @instant,
125
+ center: Center.topocentric(observer),
126
+ target_body: nil,
127
+ observer: observer
128
+ )
129
+ end
130
+
131
+ private
132
+
133
+ def gcrs_rotation_matrix
134
+ @gcrs_rotation_matrix ||= begin
135
+ precession_matrix = Precession.matrix_for(@instant)
136
+ nutation_matrix = Nutation.matrix_for(@instant)
137
+ precession_matrix.transpose *
138
+ nutation_matrix.transpose *
139
+ equation_of_equinoxes_matrix
140
+ end
141
+ end
142
+
143
+ def equation_of_equinoxes_matrix
144
+ @equation_of_equinoxes_matrix ||= begin
145
+ nutation = Nutation.new(instant: @instant)
146
+ dpsi = nutation.nutation_in_longitude
147
+ mean_obliquity = MeanObliquity.at(@instant)
148
+ eoe = dpsi.radians * mean_obliquity.cos
149
+
150
+ c, s = Math.cos(eoe), Math.sin(eoe)
151
+ Matrix[
152
+ [c, -s, 0],
153
+ [s, c, 0],
154
+ [0, 0, 1]
155
+ ]
156
+ end
157
+ end
158
+ end
159
+ end