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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +159 -0
  4. data/README.md +12 -5
  5. data/UPGRADING.md +109 -0
  6. data/docs/README.md +109 -16
  7. data/docs/angles.md +2 -1
  8. data/docs/configuration.md +20 -17
  9. data/docs/coordinates.md +73 -13
  10. data/docs/deep_sky_bodies.md +101 -0
  11. data/docs/ephem.md +6 -3
  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 +21 -16
  16. data/docs/lunar_eclipses.md +93 -0
  17. data/docs/lunar_observation.md +87 -0
  18. data/docs/moon_phases.md +5 -2
  19. data/docs/observer.md +21 -7
  20. data/docs/planetary_phenomena.md +78 -0
  21. data/docs/reference_frames.md +193 -35
  22. data/docs/rise_transit_set_times.md +10 -8
  23. data/docs/{celestial_bodies.md → solar_system_bodies.md} +27 -5
  24. data/docs/twilight_times.md +25 -21
  25. data/lib/astronoby/angle.rb +69 -4
  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 +97 -0
  29. data/lib/astronoby/bodies/deep_sky_object.rb +49 -0
  30. data/lib/astronoby/bodies/deep_sky_object_position.rb +142 -0
  31. data/lib/astronoby/bodies/earth.rb +9 -42
  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 +162 -15
  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 +257 -53
  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/cache.rb +1 -0
  44. data/lib/astronoby/center.rb +84 -0
  45. data/lib/astronoby/constants.rb +7 -2
  46. data/lib/astronoby/constellation.rb +9 -1
  47. data/lib/astronoby/coordinates/ecliptic.rb +10 -1
  48. data/lib/astronoby/coordinates/equatorial.rb +66 -13
  49. data/lib/astronoby/coordinates/geodetic.rb +102 -0
  50. data/lib/astronoby/coordinates/horizontal.rb +13 -1
  51. data/lib/astronoby/distance.rb +41 -0
  52. data/lib/astronoby/duration.rb +116 -0
  53. data/lib/astronoby/earth_rotation.rb +70 -0
  54. data/lib/astronoby/equinox_solstice.rb +31 -8
  55. data/lib/astronoby/errors.rb +11 -0
  56. data/lib/astronoby/events/conjunction.rb +51 -0
  57. data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
  58. data/lib/astronoby/events/eclipse_phase.rb +27 -0
  59. data/lib/astronoby/events/extremum_calculator.rb +80 -0
  60. data/lib/astronoby/events/extremum_event.rb +15 -0
  61. data/lib/astronoby/events/greatest_elongation.rb +58 -0
  62. data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
  63. data/lib/astronoby/events/lunar_eclipse.rb +99 -0
  64. data/lib/astronoby/events/lunar_eclipse_calculator.rb +285 -0
  65. data/lib/astronoby/events/opposition.rb +19 -0
  66. data/lib/astronoby/events/rise_transit_set_calculator.rb +9 -6
  67. data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
  68. data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
  69. data/lib/astronoby/events/twilight_calculator.rb +1 -1
  70. data/lib/astronoby/events/twilight_event.rb +24 -6
  71. data/lib/astronoby/events/twilight_events.rb +26 -6
  72. data/lib/astronoby/extremum_finder.rb +148 -0
  73. data/lib/astronoby/instant.rb +35 -9
  74. data/lib/astronoby/libration.rb +25 -0
  75. data/lib/astronoby/mean_obliquity.rb +8 -0
  76. data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
  77. data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
  78. data/lib/astronoby/nutation.rb +10 -20
  79. data/lib/astronoby/observer.rb +67 -49
  80. data/lib/astronoby/orientation.rb +107 -0
  81. data/lib/astronoby/position.rb +16 -0
  82. data/lib/astronoby/precession.rb +61 -60
  83. data/lib/astronoby/reference_frame.rb +73 -7
  84. data/lib/astronoby/reference_frames/apparent.rb +25 -16
  85. data/lib/astronoby/reference_frames/astrometric.rb +14 -1
  86. data/lib/astronoby/reference_frames/geometric.rb +7 -1
  87. data/lib/astronoby/reference_frames/mean_of_date.rb +13 -1
  88. data/lib/astronoby/reference_frames/teme.rb +153 -0
  89. data/lib/astronoby/reference_frames/topocentric.rb +31 -5
  90. data/lib/astronoby/refraction.rb +26 -5
  91. data/lib/astronoby/root_finder.rb +83 -0
  92. data/lib/astronoby/rotation.rb +49 -0
  93. data/lib/astronoby/stellar_propagation.rb +162 -0
  94. data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +31 -0
  95. data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +101 -0
  96. data/lib/astronoby/time/greenwich_sidereal_time.rb +41 -58
  97. data/lib/astronoby/time/local_apparent_sidereal_time.rb +63 -0
  98. data/lib/astronoby/time/local_mean_sidereal_time.rb +63 -0
  99. data/lib/astronoby/time/local_sidereal_time.rb +59 -26
  100. data/lib/astronoby/time/sidereal_time.rb +64 -0
  101. data/lib/astronoby/true_obliquity.rb +4 -0
  102. data/lib/astronoby/util/maths.rb +8 -0
  103. data/lib/astronoby/util/time.rb +10 -467
  104. data/lib/astronoby/vector.rb +10 -0
  105. data/lib/astronoby/velocity.rb +44 -0
  106. data/lib/astronoby/version.rb +1 -1
  107. data/lib/astronoby.rb +33 -0
  108. 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: 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)
47
- end
48
- end
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
- 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,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:,
@@ -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(&:kmps)
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
- 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