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,87 +1,128 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Represents a distance with meters as its internal representation.
5
+ # Provides conversions between meters, kilometers, astronomical units,
6
+ # and parsecs.
7
+ #
8
+ # @example Create a distance from astronomical units
9
+ # distance = Astronoby::Distance.from_au(1.0)
10
+ # distance.km # => 149597870.7
11
+ #
4
12
  class Distance
5
13
  include Comparable
6
14
 
7
15
  class << self
16
+ # @return [Astronoby::Distance] a zero distance
8
17
  def zero
9
18
  new(0)
10
19
  end
11
20
 
21
+ # @param meters [Numeric] the distance in meters
22
+ # @return [Astronoby::Distance] a new Distance
12
23
  def from_meters(meters)
13
24
  new(meters)
14
25
  end
15
26
  alias_method :from_m, :from_meters
16
27
 
28
+ # @param kilometers [Numeric] the distance in kilometers
29
+ # @return [Astronoby::Distance] a new Distance
17
30
  def from_kilometers(kilometers)
18
31
  meters = kilometers * Constants::KILOMETER_IN_METERS
19
32
  from_meters(meters)
20
33
  end
21
34
  alias_method :from_km, :from_kilometers
22
35
 
36
+ # @param astronomical_units [Numeric] the distance in AU
37
+ # @return [Astronoby::Distance] a new Distance
23
38
  def from_astronomical_units(astronomical_units)
24
39
  meters = astronomical_units * Constants::ASTRONOMICAL_UNIT_IN_METERS
25
40
  from_meters(meters)
26
41
  end
27
42
  alias_method :from_au, :from_astronomical_units
28
43
 
44
+ # @param parsecs [Numeric] the distance in parsecs
45
+ # @return [Astronoby::Distance] a new Distance
46
+ def from_parsecs(parsecs)
47
+ meters = parsecs * Constants::PARSEC_IN_METERS
48
+ from_meters(meters)
49
+ end
50
+ alias_method :from_pc, :from_parsecs
51
+
52
+ # @param array [Array<Numeric>] array of meter values
53
+ # @return [Astronoby::Vector<Astronoby::Distance>] a vector of Distances
29
54
  def vector_from_meters(array)
30
55
  Vector.elements(array.map { from_meters(_1) })
31
56
  end
32
57
  alias_method :vector_from_m, :vector_from_meters
33
58
  end
34
59
 
60
+ # @return [Numeric] the distance in meters
35
61
  attr_reader :meters
36
62
  alias_method :m, :meters
37
63
 
64
+ # @param meters [Numeric] the distance in meters
38
65
  def initialize(meters)
39
66
  @meters = meters
40
67
  freeze
41
68
  end
42
69
 
70
+ # @return [Float] the distance in kilometers
43
71
  def kilometers
44
72
  @meters / Constants::KILOMETER_IN_METERS.to_f
45
73
  end
46
74
  alias_method :km, :kilometers
47
75
 
76
+ # @return [Float] the distance in astronomical units
48
77
  def astronomical_units
49
78
  @meters / Constants::ASTRONOMICAL_UNIT_IN_METERS.to_f
50
79
  end
51
80
  alias_method :au, :astronomical_units
52
81
 
82
+ # @param other [Astronoby::Distance] distance to add
83
+ # @return [Astronoby::Distance] the sum
53
84
  def +(other)
54
85
  self.class.from_meters(meters + other.meters)
55
86
  end
56
87
 
88
+ # @param other [Astronoby::Distance] distance to subtract
89
+ # @return [Astronoby::Distance] the difference
57
90
  def -(other)
58
91
  self.class.from_meters(@meters - other.meters)
59
92
  end
60
93
 
94
+ # @return [Astronoby::Distance] the negated distance
61
95
  def -@
62
96
  self.class.from_meters(-@meters)
63
97
  end
64
98
 
99
+ # @return [Boolean] true if the distance is positive
65
100
  def positive?
66
101
  meters > 0
67
102
  end
68
103
 
104
+ # @return [Boolean] true if the distance is negative
69
105
  def negative?
70
106
  meters < 0
71
107
  end
72
108
 
109
+ # @return [Boolean] true if the distance is zero
73
110
  def zero?
74
111
  meters.zero?
75
112
  end
76
113
 
114
+ # @return [Numeric] the square of the distance in meters
77
115
  def abs2
78
116
  meters**2
79
117
  end
80
118
 
119
+ # @return [Integer] hash value
81
120
  def hash
82
121
  [meters, self.class].hash
83
122
  end
84
123
 
124
+ # @param other [Astronoby::Distance] distance to compare with
125
+ # @return [Integer, nil] -1, 0, or 1; nil if not comparable
85
126
  def <=>(other)
86
127
  return unless other.is_a?(self.class)
87
128
 
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class Duration
5
+ include Comparable
6
+
7
+ class << self
8
+ # @return [Astronoby::Duration] a zero duration
9
+ def zero
10
+ new(0)
11
+ end
12
+
13
+ # @param seconds [Numeric] the duration in seconds
14
+ # @return [Astronoby::Duration] a new Duration
15
+ def from_seconds(seconds)
16
+ new(seconds)
17
+ end
18
+
19
+ # @param minutes [Numeric] the duration in minutes
20
+ # @return [Astronoby::Duration] a new Duration
21
+ def from_minutes(minutes)
22
+ seconds = minutes * Constants::SECONDS_PER_MINUTE
23
+ from_seconds(seconds)
24
+ end
25
+
26
+ # @param hours [Numeric] the duration in hours
27
+ # @return [Astronoby::Duration] a new Duration
28
+ def from_hours(hours)
29
+ seconds = hours * Constants::SECONDS_PER_HOUR
30
+ from_seconds(seconds)
31
+ end
32
+
33
+ # @param days [Numeric] the duration in days
34
+ # @return [Astronoby::Duration] a new Duration
35
+ def from_days(days)
36
+ seconds = days * Constants::SECONDS_PER_DAY
37
+ from_seconds(seconds)
38
+ end
39
+ end
40
+
41
+ # @return [Numeric] the duration in seconds
42
+ attr_reader :seconds
43
+
44
+ # @param seconds [Numeric] the duration in seconds
45
+ def initialize(seconds)
46
+ @seconds = seconds
47
+ freeze
48
+ end
49
+
50
+ # @return [Float] the duration in minutes
51
+ def minutes
52
+ @seconds / Constants::SECONDS_PER_MINUTE
53
+ end
54
+
55
+ # @return [Float] the duration in hours
56
+ def hours
57
+ @seconds / Constants::SECONDS_PER_HOUR
58
+ end
59
+
60
+ # @return [Float] the duration in days
61
+ def days
62
+ @seconds / Constants::SECONDS_PER_DAY
63
+ end
64
+
65
+ # @param other [Astronoby::Duration] duration to add
66
+ # @return [Astronoby::Duration] the sum
67
+ def +(other)
68
+ self.class.from_seconds(@seconds + other.seconds)
69
+ end
70
+
71
+ # @param other [Astronoby::Duration] duration to subtract
72
+ # @return [Astronoby::Duration] the difference
73
+ def -(other)
74
+ self.class.from_seconds(@seconds - other.seconds)
75
+ end
76
+
77
+ # @return [Astronoby::Duration] the negated duration
78
+ def -@
79
+ self.class.from_seconds(-@seconds)
80
+ end
81
+
82
+ # @return [Astronoby::Duration] the absolute duration
83
+ def abs
84
+ self.class.from_seconds(@seconds.abs)
85
+ end
86
+
87
+ # @return [Boolean] true if the duration is positive
88
+ def positive?
89
+ @seconds > 0
90
+ end
91
+
92
+ # @return [Boolean] true if the duration is negative
93
+ def negative?
94
+ @seconds < 0
95
+ end
96
+
97
+ # @return [Boolean] true if the duration is zero
98
+ def zero?
99
+ @seconds.zero?
100
+ end
101
+
102
+ # @return [Integer] hash value
103
+ def hash
104
+ [@seconds, self.class].hash
105
+ end
106
+
107
+ # @param other [Astronoby::Duration] duration to compare with
108
+ # @return [Integer, nil] -1, 0, or 1; nil if not comparable
109
+ def <=>(other)
110
+ return unless other.is_a?(self.class)
111
+
112
+ seconds <=> other.seconds
113
+ end
114
+ alias_method :eql?, :==
115
+ end
116
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # Computes Earth rotation matrices using Greenwich Sidereal Time.
5
+ # Provides both apparent (GAST-based) and mean (GMST-based) rotation
6
+ # matrices for transforming between Earth-fixed and celestial frames.
7
+ class EarthRotation
8
+ # Computes the apparent Earth rotation matrix R₃(GAST) for a given
9
+ # instant.
10
+ #
11
+ # @param instant [Astronoby::Instant] the time instant
12
+ # @return [Matrix] 3x3 Earth rotation matrix
13
+ def self.matrix_for(instant)
14
+ new(instant: instant).matrix
15
+ end
16
+
17
+ # Computes the mean Earth rotation matrix R₃(GMST) for a given
18
+ # instant.
19
+ #
20
+ # @param instant [Astronoby::Instant] the time instant
21
+ # @return [Matrix] 3x3 mean Earth rotation matrix
22
+ def self.mean_matrix_for(instant)
23
+ new(instant: instant).mean_matrix
24
+ end
25
+
26
+ # @param instant [Astronoby::Instant] the time instant
27
+ def initialize(instant:)
28
+ @instant = instant
29
+ end
30
+
31
+ # Computes R₃(GAST), the apparent Earth rotation matrix.
32
+ # GAST = GMST + equation of the equinoxes (Δψ cos ε₀).
33
+ #
34
+ # @return [Matrix] 3x3 rotation matrix
35
+ def matrix
36
+ rotation_matrix_for(gast)
37
+ end
38
+
39
+ # Computes R₃(GMST), the mean Earth rotation matrix.
40
+ #
41
+ # @return [Matrix] 3x3 rotation matrix
42
+ def mean_matrix
43
+ rotation_matrix_for(gmst)
44
+ end
45
+
46
+ private
47
+
48
+ def gmst
49
+ Angle.from_hours(@instant.gmst)
50
+ end
51
+
52
+ def gast
53
+ nutation = Nutation.new(instant: @instant)
54
+ dpsi = nutation.nutation_in_longitude
55
+ mean_obliquity = MeanObliquity.at(@instant)
56
+ Angle.from_radians(
57
+ gmst.radians + dpsi.radians * mean_obliquity.cos
58
+ )
59
+ end
60
+
61
+ def rotation_matrix_for(angle)
62
+ c, s = angle.cos, angle.sin
63
+ Matrix[
64
+ [c, -s, 0],
65
+ [s, c, 0],
66
+ [0, 0, 1]
67
+ ]
68
+ end
69
+ end
70
+ end
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Computes the time of equinoxes and solstices for a given year.
5
+ #
6
+ # Source:
7
+ # Title: Astronomical Algorithms
8
+ # Author: Jean Meeus
9
+ # Edition: 2nd edition
10
+ # Chapter: 27 - Equinoxes and Soltices
4
11
  class EquinoxSolstice
5
- # Source:
6
- # Title: Astronomical Algorithms
7
- # Author: Jean Meeus
8
- # Edition: 2nd edition
9
- # Chapter: 27 - Equinoxes and Soltices
10
-
11
12
  EVENTS = [
12
13
  MARCH_EQUINOX = 0,
13
14
  JUNE_SOLSTICE = 1,
@@ -73,22 +74,39 @@ module Astronoby
73
74
  [8, 15.45, 16859.074]
74
75
  ].freeze
75
76
 
77
+ # @param year [Integer] the year
78
+ # @param ephem [::Ephem::SPK] ephemeris data source
79
+ # @return [Time] time of the March equinox
76
80
  def self.march_equinox(year, ephem)
77
81
  new(year, MARCH_EQUINOX, ephem).time
78
82
  end
79
83
 
84
+ # @param year [Integer] the year
85
+ # @param ephem [::Ephem::SPK] ephemeris data source
86
+ # @return [Time] time of the June solstice
80
87
  def self.june_solstice(year, ephem)
81
88
  new(year, JUNE_SOLSTICE, ephem).time
82
89
  end
83
90
 
91
+ # @param year [Integer] the year
92
+ # @param ephem [::Ephem::SPK] ephemeris data source
93
+ # @return [Time] time of the September equinox
84
94
  def self.september_equinox(year, ephem)
85
95
  new(year, SEPTEMBER_EQUINOX, ephem).time
86
96
  end
87
97
 
98
+ # @param year [Integer] the year
99
+ # @param ephem [::Ephem::SPK] ephemeris data source
100
+ # @return [Time] time of the December solstice
88
101
  def self.december_solstice(year, ephem)
89
102
  new(year, DECEMBER_SOLSTICE, ephem).time
90
103
  end
91
104
 
105
+ # @param year [Integer] the year
106
+ # @param event [Integer] one of MARCH_EQUINOX, JUNE_SOLSTICE,
107
+ # SEPTEMBER_EQUINOX, or DECEMBER_SOLSTICE
108
+ # @param ephem [::Ephem::SPK] ephemeris data source
109
+ # @raise [Astronoby::UnsupportedEventError] if event is invalid
92
110
  def initialize(year, event, ephem)
93
111
  unless EVENTS.include?(event)
94
112
  raise UnsupportedEventError.new(
@@ -97,12 +115,13 @@ module Astronoby
97
115
  end
98
116
 
99
117
  @event = event
118
+ @ephem = ephem
100
119
  @year = (year.to_i - 2000) / 1000.0
101
120
  uncorrected_time = compute
102
121
  @instant = Instant.from_time(uncorrected_time)
103
- @sun = Sun.new(ephem: ephem, instant: @instant)
104
122
  end
105
123
 
124
+ # @return [Time] the corrected UTC time of the event
106
125
  def time
107
126
  Instant.from_terrestrial_time(@instant.tt + corrected).to_time.round
108
127
  end
@@ -135,8 +154,12 @@ module Astronoby
135
154
  component[4] * @year**4
136
155
  end
137
156
 
157
+ def sun
158
+ @sun ||= Sun.new(ephem: @ephem, instant: @instant)
159
+ end
160
+
138
161
  def corrected
139
- longitude = @sun.apparent.ecliptic.longitude
162
+ longitude = sun.apparent.ecliptic.longitude
140
163
  58 * Angle.from_degrees(@event * 90 - longitude.degrees).sin
141
164
  end
142
165
  end
@@ -1,11 +1,22 @@
1
1
  module Astronoby
2
+ # Raised when arguments are mutually incompatible.
2
3
  class IncompatibleArgumentsError < ArgumentError; end
3
4
 
5
+ # Raised when an unsupported format is requested.
4
6
  class UnsupportedFormatError < ArgumentError; end
5
7
 
8
+ # Raised when an unsupported event type is requested.
6
9
  class UnsupportedEventError < ArgumentError; end
7
10
 
11
+ # Raised when an astronomical calculation fails.
8
12
  class CalculationError < StandardError; end
9
13
 
14
+ # Raised when there is an error with ephemeris data.
10
15
  class EphemerisError < StandardError; end
16
+
17
+ # Raised when an orientation kernel is not a supported orientation source.
18
+ class OrientationError < StandardError; end
19
+
20
+ # Raised when an orientation kernel does not cover the requested instant.
21
+ class OrientationOutOfRangeError < StandardError; end
11
22
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class Conjunction
5
+ INFERIOR = :inferior
6
+ SUPERIOR = :superior
7
+
8
+ # @param instant [Astronoby::Instant] when the conjunction occurs
9
+ # @param body [Astronoby::Body] the body in conjunction with the Sun
10
+ # @return [Astronoby::Conjunction] an inferior conjunction
11
+ def self.inferior(instant:, body:)
12
+ new(instant: instant, body: body, subtype: INFERIOR)
13
+ end
14
+
15
+ # @param instant [Astronoby::Instant] when the conjunction occurs
16
+ # @param body [Astronoby::Body] the body in conjunction with the Sun
17
+ # @return [Astronoby::Conjunction] a superior conjunction
18
+ def self.superior(instant:, body:)
19
+ new(instant: instant, body: body, subtype: SUPERIOR)
20
+ end
21
+
22
+ # @return [Astronoby::Instant] when the conjunction occurs
23
+ attr_reader :instant
24
+
25
+ # @return [Astronoby::Body] the body in conjunction with the Sun
26
+ attr_reader :body
27
+
28
+ # @return [Symbol] +INFERIOR+ or +SUPERIOR+
29
+ attr_reader :subtype
30
+
31
+ # @param instant [Astronoby::Instant] when the conjunction occurs
32
+ # @param body [Astronoby::Body] the body in conjunction with the Sun
33
+ # @param subtype [Symbol] +INFERIOR+ or +SUPERIOR+
34
+ def initialize(instant:, body:, subtype:)
35
+ @instant = instant
36
+ @body = body
37
+ @subtype = subtype
38
+ freeze
39
+ end
40
+
41
+ # @return [Boolean] true for an inferior conjunction
42
+ def inferior?
43
+ @subtype == INFERIOR
44
+ end
45
+
46
+ # @return [Boolean] true for a superior conjunction
47
+ def superior?
48
+ @subtype == SUPERIOR
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class ConjunctionOppositionCalculator
5
+ # @param body [Astronoby::SolarSystemBody] the planet to track
6
+ # @param ephem [::Ephem::SPK] ephemeris data source
7
+ # @param samples_per_period [Integer] number of samples per synodic period
8
+ def initialize(body:, ephem:, samples_per_period: 60)
9
+ @body = body
10
+ @ephem = ephem
11
+ @samples_per_period = samples_per_period
12
+ end
13
+
14
+ # @param start_time [Time] start time
15
+ # @param end_time [Time] end time
16
+ # @return [Array<Astronoby::Conjunction>] conjunctions in the range
17
+ def conjunction_events_between(start_time, end_time)
18
+ roots_between(start_time, end_time, accept: method(:conjunction?))
19
+ .map { |jd| conjunction_at(Instant.from_terrestrial_time(jd)) }
20
+ end
21
+
22
+ # @param start_time [Time] start time
23
+ # @param end_time [Time] end time
24
+ # @return [Array<Astronoby::Opposition>] oppositions in the range
25
+ def opposition_events_between(start_time, end_time)
26
+ roots_between(start_time, end_time, accept: method(:opposition?))
27
+ .map do |jd|
28
+ Opposition.new(
29
+ instant: Instant.from_terrestrial_time(jd),
30
+ body: @body
31
+ )
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def roots_between(start_time, end_time, accept:)
38
+ finder.roots(
39
+ Instant.from_time(start_time).tt,
40
+ Instant.from_time(end_time).tt,
41
+ accept: accept
42
+ )
43
+ end
44
+
45
+ def finder
46
+ @finder ||= RootFinder.new(
47
+ value_at: ->(jd) { delta_longitude_at(jd).sin },
48
+ period: synodic_period,
49
+ samples_per_period: @samples_per_period
50
+ )
51
+ end
52
+
53
+ def conjunction?(jd)
54
+ delta_longitude_at(jd).cos.positive?
55
+ end
56
+
57
+ def opposition?(jd)
58
+ delta_longitude_at(jd).cos.negative?
59
+ end
60
+
61
+ def delta_longitude_at(jd)
62
+ instant = Instant.from_terrestrial_time(jd)
63
+ planet = @body.new(instant: instant, ephem: @ephem)
64
+ sun = Sun.new(instant: instant, ephem: @ephem)
65
+ planet.apparent.ecliptic.longitude - sun.apparent.ecliptic.longitude
66
+ end
67
+
68
+ def conjunction_at(instant)
69
+ planet = @body.new(instant: instant, ephem: @ephem)
70
+ sun = Sun.new(instant: instant, ephem: @ephem)
71
+
72
+ if planet.apparent.distance < sun.apparent.distance
73
+ Conjunction.inferior(instant: instant, body: @body)
74
+ else
75
+ Conjunction.superior(instant: instant, body: @body)
76
+ end
77
+ end
78
+
79
+ def synodic_period
80
+ @synodic_period ||=
81
+ 1.0 / ((1.0 / @body::ORBITAL_PERIOD) - (1.0 / Earth::ORBITAL_PERIOD)).abs
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # A bounded phase of an eclipse, delimited by its two boundary instants.
5
+ class EclipsePhase
6
+ # @return [Astronoby::Instant] when the phase begins
7
+ attr_reader :starting_instant
8
+
9
+ # @return [Astronoby::Instant] when the phase ends
10
+ attr_reader :ending_instant
11
+
12
+ # @param starting_instant [Astronoby::Instant] when the phase begins
13
+ # @param ending_instant [Astronoby::Instant] when the phase ends
14
+ def initialize(starting_instant:, ending_instant:)
15
+ @starting_instant = starting_instant
16
+ @ending_instant = ending_instant
17
+ freeze
18
+ end
19
+
20
+ # @return [Astronoby::Duration] phase duration
21
+ def duration
22
+ Duration.from_seconds(
23
+ (@ending_instant.to_time - @starting_instant.to_time).round
24
+ )
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class ExtremumCalculator
5
+ # @param body [Astronoby::SolarSystemBody] the celestial body to track
6
+ # @param primary_body [Astronoby::SolarSystemBody] the reference body
7
+ # @param ephem [::Ephem::SPK] ephemeris data source
8
+ # @param samples_per_period [Integer] number of samples per orbital period
9
+ def initialize(
10
+ body:,
11
+ primary_body:,
12
+ ephem:,
13
+ samples_per_period: 60
14
+ )
15
+ @body = body
16
+ @primary_body = primary_body
17
+ @ephem = ephem
18
+ @samples_per_period = samples_per_period
19
+ end
20
+
21
+ # Finds all apoapsis events between two times
22
+ # @param start_time [Time] Start time
23
+ # @param end_time [Time] End time
24
+ # @return [Array<Astronoby::ExtremumEvent>] Array of apoapsis events
25
+ def apoapsis_events_between(start_time, end_time)
26
+ find_extrema(
27
+ Instant.from_time(start_time).tt,
28
+ Instant.from_time(end_time).tt,
29
+ type: :maximum
30
+ )
31
+ end
32
+
33
+ # Finds all periapsis events between two times
34
+ # @param start_time [Time] Start time
35
+ # @param end_time [Time] End time
36
+ # @return [Array<Astronoby::ExtremumEvent>] Array of periapsis events
37
+ def periapsis_events_between(start_time, end_time)
38
+ find_extrema(
39
+ Instant.from_time(start_time).tt,
40
+ Instant.from_time(end_time).tt,
41
+ type: :minimum
42
+ )
43
+ end
44
+
45
+ # Finds extrema (minima or maxima) in the distance between bodies within
46
+ # a time range
47
+ # @param start_jd [Float] Start time in Julian Date (Terrestrial Time)
48
+ # @param end_jd [Float] End time in Julian Date (Terrestrial Time)
49
+ # @param type [Symbol] :maximum or :minimum
50
+ # @return [Array<Astronoby::ExtremumEvent>] Array of extrema events
51
+ def find_extrema(start_jd, end_jd, type: :maximum)
52
+ finder.extrema(start_jd, end_jd, type: type).map do |extremum|
53
+ ExtremumEvent.new(
54
+ Instant.from_terrestrial_time(extremum[:jd]),
55
+ extremum[:value]
56
+ )
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def finder
63
+ @finder ||= ExtremumFinder.new(
64
+ value_at: method(:distance_at),
65
+ period: @body::ORBITAL_PERIOD,
66
+ samples_per_period: @samples_per_period
67
+ )
68
+ end
69
+
70
+ def distance_at(jd)
71
+ instant = Instant.from_terrestrial_time(jd)
72
+ body_geometric = @body.geometric(ephem: @ephem, instant: instant)
73
+ primary_geometric = @primary_body
74
+ .geometric(ephem: @ephem, instant: instant)
75
+
76
+ distance_vector = body_geometric.position - primary_geometric.position
77
+ distance_vector.magnitude
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # Represents an extremum event with its timing and value
5
+ class ExtremumEvent
6
+ attr_reader :instant, :value
7
+
8
+ # @param instant [Astronoby::Instant] When the event occurs
9
+ # @param value [Object] The extreme value
10
+ def initialize(instant, value)
11
+ @instant = instant
12
+ @value = value
13
+ end
14
+ end
15
+ end