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
@@ -10,15 +10,26 @@ module Astronoby
10
10
  MOLAR_MASS_OF_AIR = 0.0289644
11
11
  UNIVERSAL_GAS_CONSTANT = 8.31432
12
12
 
13
- attr_reader :latitude,
14
- :longitude,
15
- :elevation,
16
- :utc_offset,
17
- :temperature,
18
- :pressure
19
-
20
- # @param latitude [Angle] geographic latitude of the observer
21
- # @param longitude [Angle] geographic longitude of the observer
13
+ # @return [Astronoby::Angle] geographic latitude
14
+ attr_reader :latitude
15
+
16
+ # @return [Astronoby::Angle] geographic longitude
17
+ attr_reader :longitude
18
+
19
+ # @return [Astronoby::Distance] geographic elevation above sea level
20
+ attr_reader :elevation
21
+
22
+ # @return [Numeric, String] offset from Coordinated Universal Time
23
+ attr_reader :utc_offset
24
+
25
+ # @return [Numeric] temperature in kelvins
26
+ attr_reader :temperature
27
+
28
+ # @return [Numeric] atmospheric pressure in millibars
29
+ attr_reader :pressure
30
+
31
+ # @param latitude [Astronoby::Angle] geographic latitude of the observer
32
+ # @param longitude [Astronoby::Angle] geographic longitude of the observer
22
33
  # @param elevation [Astronoby::Distance] geographic elevation (or altitude)
23
34
  # of the observer above sea level
24
35
  # @param utc_offset [Numeric, String] offset from Coordinated Universal Time
@@ -42,45 +53,44 @@ module Astronoby
42
53
  @pressure = pressure || compute_pressure
43
54
  end
44
55
 
56
+ # Returns the observer's ECEF position vector (WGS-84 geodetic to ECEF).
57
+ #
58
+ # @return [Astronoby::Vector<Astronoby::Distance>] geocentric position
45
59
  def geocentric_position
46
- n = earth_prime_vertical_radius_of_curvature
47
- x = (n + @elevation.m) * @latitude.cos * @longitude.cos
48
- y = (n + @elevation.m) * @latitude.cos * @longitude.sin
49
- z = (n * (1 - Constants::WGS84_ECCENTICITY_SQUARED) + @elevation.m) *
50
- @latitude.sin
51
- Distance.vector_from_meters([x, y, z])
60
+ @geocentric_position ||= begin
61
+ n = earth_prime_vertical_radius_of_curvature
62
+ x = (n + @elevation.m) * @latitude.cos * @longitude.cos
63
+ y = (n + @elevation.m) * @latitude.cos * @longitude.sin
64
+ z = (n * (1 - Constants::WGS84_ECCENTICITY_SQUARED) + @elevation.m) *
65
+ @latitude.sin
66
+ Distance.vector_from_meters([x, y, z])
67
+ end
52
68
  end
53
69
 
70
+ # Returns the observer's ECEF velocity vector due to Earth rotation.
71
+ #
72
+ # @return [Astronoby::Vector<Astronoby::Velocity>] geocentric velocity
54
73
  def geocentric_velocity
55
- r = projected_radius
56
- vx = -Constants::EARTH_ANGULAR_VELOCITY_RAD_PER_S * r * @longitude.sin
57
- vy = Constants::EARTH_ANGULAR_VELOCITY_RAD_PER_S * r * @longitude.cos
58
- vz = 0.0
59
- Velocity.vector_from_mps([vx, vy, vz])
74
+ @geocentric_velocity ||= begin
75
+ r = projected_radius
76
+ vx = -Constants::EARTH_ANGULAR_VELOCITY_RAD_PER_S * r * @longitude.sin
77
+ vy = Constants::EARTH_ANGULAR_VELOCITY_RAD_PER_S * r * @longitude.cos
78
+ vz = 0.0
79
+ Velocity.vector_from_mps([vx, vy, vz])
80
+ end
60
81
  end
61
82
 
83
+ # Computes the Earth-fixed rotation matrix R₃(GAST) * W (polar motion)
84
+ # for a given instant.
85
+ #
86
+ # @param instant [Astronoby::Instant] the time instant
87
+ # @return [Matrix] 3x3 rotation matrix
62
88
  def earth_fixed_rotation_matrix_for(instant)
63
- dpsi = Nutation.new(instant: instant).nutation_in_longitude
64
-
65
- mean_obliquity = MeanObliquity.at(instant)
66
-
67
- gast = Angle.from_radians(
68
- Angle.from_hours(instant.gmst).radians +
69
- dpsi.radians * mean_obliquity.cos
70
- )
71
-
72
- earth_rotation_matrix = Matrix[
73
- [gast.cos, -gast.sin, 0],
74
- [gast.sin, gast.cos, 0],
75
- [0, 0, 1]
76
- ]
77
-
78
- nutation_matrix = Nutation.matrix_for(instant)
79
- precession_matrix = Precession.matrix_for(instant)
80
-
81
- earth_rotation_matrix * nutation_matrix * precession_matrix
89
+ EarthRotation.matrix_for(instant) * polar_motion_matrix_for(instant)
82
90
  end
83
91
 
92
+ # @param other [Astronoby::Observer] observer to compare with
93
+ # @return [Boolean] true if all attributes are equal
84
94
  def ==(other)
85
95
  return false unless other.is_a?(self.class)
86
96
 
@@ -93,6 +103,7 @@ module Astronoby
93
103
  end
94
104
  alias_method :eql?, :==
95
105
 
106
+ # @return [Integer] hash value
96
107
  def hash
97
108
  [
98
109
  self.class,
@@ -107,7 +118,6 @@ module Astronoby
107
118
 
108
119
  private
109
120
 
110
- # @return [Float] the atmospheric pressure in millibars.
111
121
  def compute_pressure
112
122
  @pressure ||= PRESSURE_AT_SEA_LEVEL * pressure_ratio
113
123
  end
@@ -124,20 +134,28 @@ module Astronoby
124
134
  Math.exp(-term1 / term2)
125
135
  end
126
136
 
137
+ def polar_motion_matrix_for(instant)
138
+ rows = IERS::PolarMotion.rotation_matrix_at(instant.to_time)
139
+ Matrix[*rows]
140
+ rescue IERS::OutOfRangeError
141
+ Matrix.identity(3)
142
+ end
143
+
127
144
  def earth_prime_vertical_radius_of_curvature
128
- Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS./(
129
- Math.sqrt(
130
- 1 -
131
- Constants::WGS84_ECCENTICITY_SQUARED * @latitude.sin * @latitude.sin
145
+ @earth_prime_vertical_radius_of_curvature ||=
146
+ Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS./(
147
+ Math.sqrt(
148
+ 1 -
149
+ Constants::WGS84_ECCENTICITY_SQUARED * @latitude.sin * @latitude.sin
150
+ )
132
151
  )
133
- )
134
152
  end
135
153
 
136
154
  def projected_radius
137
- Math.sqrt(
138
- geocentric_position.x.m * geocentric_position.x.m +
139
- geocentric_position.y.m * geocentric_position.y.m
140
- )
155
+ @projected_radius ||= begin
156
+ pos = geocentric_position
157
+ Math.sqrt(pos.x.m * pos.x.m + pos.y.m * pos.y.m)
158
+ end
141
159
  end
142
160
  end
143
161
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ephem"
4
+ require "matrix"
5
+
6
+ module Astronoby
7
+ # Wraps a binary PCK lunar orientation kernel (for example
8
+ # +moon_pa_de440_200625.bpc+) and exposes the rotation from the inertial frame
9
+ # (J2000/ICRF) to the Moon's mean-Earth (ME) body-fixed frame at a given
10
+ # instant.
11
+ #
12
+ # A binary kernel only provides the principal-axis (PA) orientation, and which
13
+ # PA frame it describes is read from the kernel itself. JPL Horizons and IMCCE
14
+ # report selenographic positions in the mean-Earth frame, whose small fixed
15
+ # offset from the principal axes is published in the NAIF lunar frame kernels
16
+ # (the +moon_*.tf+ files) rather than in the binary kernel. That alignment is
17
+ # essentially constant across ephemeris versions (below 0.15 arcsecond), so
18
+ # the DE440 values are applied here.
19
+ class Orientation
20
+ # Principal-axis to mean-Earth alignment from the NAIF DE440 lunar frame
21
+ # kernel (moon_de440_250416.tf): the rotation angles about the z, y and x
22
+ # axes that align the principal-axis frame with the mean-Earth frame.
23
+ MEAN_EARTH_ALIGNMENT = [
24
+ Angle.from_degree_arcseconds(67.8526),
25
+ Angle.from_degree_arcseconds(78.6944),
26
+ Angle.from_degree_arcseconds(0.2785)
27
+ ].freeze
28
+
29
+ # Download a binary PCK orientation kernel.
30
+ #
31
+ # @param name [String] kernel name supported by the Ephem gem
32
+ # @param target [String] destination path
33
+ # @return [Boolean] true if the download was successful
34
+ def self.download(name:, target:)
35
+ ::Ephem::Download.call(name: name, target: target)
36
+ end
37
+
38
+ # Load a binary PCK orientation kernel.
39
+ #
40
+ # @param target [String] path to the +.bpc+ file
41
+ # @return [Astronoby::Orientation]
42
+ # @raise [Astronoby::OrientationError] if the kernel has no orientation data
43
+ def self.load(target)
44
+ new(::Ephem::PCK.open(target))
45
+ end
46
+
47
+ # @param pck [::Ephem::PCK] an opened binary PCK
48
+ def initialize(pck)
49
+ @pck = pck
50
+ @source = orientation_source
51
+ @mean_earth_rotation = mean_earth_rotation
52
+ @start_jd = @pck.segments.map(&:start_jd).min
53
+ @end_jd = @pck.segments.map(&:end_jd).max
54
+ end
55
+
56
+ # Rotation from the inertial frame (J2000/ICRF) to the Moon's mean-Earth
57
+ # body-fixed frame.
58
+ #
59
+ # @param instant [Astronoby::Instant] the time instant
60
+ # @return [Matrix] a 3x3 rotation matrix
61
+ # @raise [Astronoby::OrientationOutOfRangeError] if the kernel does not
62
+ # cover the instant
63
+ def rotation_for(instant)
64
+ @mean_earth_rotation * principal_axis_rotation(instant)
65
+ end
66
+
67
+ # @return [void]
68
+ def close
69
+ @pck.close
70
+ end
71
+
72
+ private
73
+
74
+ # The orientation source for the body frame the kernel describes, read from
75
+ # the kernel rather than assumed.
76
+ def orientation_source
77
+ segment = @pck.segments.first
78
+ raise OrientationError, "Orientation kernel has no segments" unless segment
79
+
80
+ @pck[segment.target]
81
+ end
82
+
83
+ # Inertial -> principal-axis rotation from the binary kernel's Euler angles.
84
+ def principal_axis_rotation(instant)
85
+ terrestrial_time = instant.terrestrial_time
86
+ unless terrestrial_time.between?(@start_jd, @end_jd)
87
+ raise OrientationOutOfRangeError,
88
+ "Orientation kernel covers #{date(@start_jd)} to #{date(@end_jd)}; " \
89
+ "requested #{date(terrestrial_time)}"
90
+ end
91
+
92
+ Matrix[*@source.matrix_at(terrestrial_time)]
93
+ end
94
+
95
+ # Principal-axis -> mean-Earth rotation (constant), about the x, y, z axes.
96
+ def mean_earth_rotation
97
+ about_z, about_y, about_x = MEAN_EARTH_ALIGNMENT
98
+ Rotation.about_x(-about_x) *
99
+ Rotation.about_y(-about_y) *
100
+ Rotation.about_z(-about_z)
101
+ end
102
+
103
+ def date(terrestrial_time)
104
+ Instant.from_terrestrial_time(terrestrial_time).to_time.utc.to_date
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Position
5
+ # @param observer [Astronoby::Observer] the observer
6
+ # @return [Astronoby::Topocentric] the topocentric reference frame
7
+ def observed_by(observer)
8
+ Topocentric.build_from_apparent(
9
+ apparent: apparent,
10
+ observer: observer,
11
+ instant: instant,
12
+ target_body: body
13
+ )
14
+ end
15
+ end
16
+ end
@@ -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
 
@@ -162,9 +167,5 @@ module Astronoby
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,27 +1,46 @@
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 [Astronoby::Coordinates::Equatorial] equatorial coordinates
43
+ # derived from the position vector
25
44
  def equatorial
26
45
  @equatorial ||= begin
27
46
  return Coordinates::Equatorial.zero if distance.zero?
@@ -30,6 +49,8 @@ module Astronoby
30
49
  end
31
50
  end
32
51
 
52
+ # @return [Astronoby::Coordinates::Ecliptic] ecliptic coordinates derived
53
+ # from the equatorial coordinates at J2000.0
33
54
  def ecliptic
34
55
  @ecliptic ||= begin
35
56
  return Coordinates::Ecliptic.zero if distance.zero?
@@ -39,6 +60,7 @@ module Astronoby
39
60
  end
40
61
  end
41
62
 
63
+ # @return [Astronoby::Distance] the Euclidean distance from the center
42
64
  def distance
43
65
  @distance ||= begin
44
66
  return Distance.zero if @position.zero?
@@ -46,5 +68,49 @@ module Astronoby
46
68
  @position.magnitude
47
69
  end
48
70
  end
71
+
72
+ # @param other [Astronoby::ReferenceFrame] another frame at the same stage
73
+ # and instant
74
+ # @return [Astronoby::Angle] the angular separation, between 0° and 180°
75
+ # @raise [Astronoby::IncompatibleArgumentsError] if the frames cannot be
76
+ # meaningfully compared
77
+ def separation_from(other)
78
+ ensure_comparable!(other)
79
+ return Angle.zero if @position.zero? || other.position.zero?
80
+
81
+ position_vector = @position.map(&:m)
82
+ other_position_vector = other.position.map(&:m)
83
+ cross = Util::Maths.cross_product(position_vector, other_position_vector)
84
+ cross_magnitude = Math.sqrt(Util::Maths.dot_product(cross, cross))
85
+ dot = Util::Maths.dot_product(position_vector, other_position_vector)
86
+
87
+ Angle.from_radians(Math.atan2(cross_magnitude, dot))
88
+ end
89
+
90
+ private
91
+
92
+ def ensure_comparable!(other)
93
+ unless other.is_a?(ReferenceFrame)
94
+ raise IncompatibleArgumentsError,
95
+ "Expected an Astronoby::ReferenceFrame, got #{other.class}"
96
+ end
97
+
98
+ unless instance_of?(other.class)
99
+ raise IncompatibleArgumentsError,
100
+ "Cannot compute the separation between different reference frames " \
101
+ "(#{self.class} and #{other.class}); both frames must be at the " \
102
+ "same stage of the reference frame chain"
103
+ end
104
+
105
+ unless instant == other.instant
106
+ raise IncompatibleArgumentsError,
107
+ "Cannot compute the separation between frames at different instants"
108
+ end
109
+
110
+ unless center == other.center
111
+ raise IncompatibleArgumentsError,
112
+ "Cannot compute the separation between frames with different centers"
113
+ end
114
+ end
49
115
  end
50
116
  end
@@ -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,37 +25,44 @@ 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: corrected_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
- precession_matrix * nutation_matrix * velocity.map(&:mps)
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
- center_identifier: SolarSystemBody::EARTH,
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(instant: @instant)
62
+ equatorial.to_ecliptic(
63
+ instant: @instant,
64
+ obliquity: TrueObliquity.at(@instant)
65
+ )
47
66
  end
48
67
  end
49
68
  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