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,49 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
- # Calculates extrema (minima and maxima) in the distance between two
5
- # celestial bodies over a given time range using adaptive sampling and golden
6
- # section search refinement.
7
4
  class ExtremumCalculator
8
- # Mathematical constants
9
- PHI = (1 + Math.sqrt(5)) / 2
10
- INVPHI = 1 / PHI
11
- GOLDEN_SECTION_TOLERANCE = 1e-5
12
-
13
- # Algorithm parameters
14
- MIN_SAMPLES_PER_PERIOD = 20
15
- DUPLICATE_THRESHOLD_DAYS = 0.5
16
- BOUNDARY_BUFFER_DAYS = 0.01
17
-
18
- # Orbital periods
19
- ORBITAL_PERIODS = {
20
- "Astronoby::Moon" => 27.504339,
21
- "Astronoby::Mercury" => 87.969,
22
- "Astronoby::Venus" => 224.701,
23
- "Astronoby::Earth" => 365.256,
24
- "Astronoby::Mars" => 686.98,
25
- "Astronoby::Jupiter" => 4332.59,
26
- "Astronoby::Saturn" => 10759.22,
27
- "Astronoby::Uranus" => 30688.5,
28
- "Astronoby::Neptune" => 60182.0
29
- }.freeze
30
-
31
- # @param ephem [::Ephem::SPK] Ephemeris data source
32
- # @param body [Astronoby::SolarSystemBody] The celestial body to track
33
- # @param primary_body [Astronoby::SolarSystemBody] The reference body
34
- # (e.g., Sun for planetary orbits)
35
- # @param samples_per_period [Integer] Number of samples to take per orbital
36
- # period
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
37
9
  def initialize(
38
10
  body:,
39
11
  primary_body:,
40
12
  ephem:,
41
13
  samples_per_period: 60
42
14
  )
43
- @ephem = ephem
44
15
  @body = body
45
16
  @primary_body = primary_body
46
- @orbital_period = ORBITAL_PERIODS.fetch(body.name)
17
+ @ephem = ephem
47
18
  @samples_per_period = samples_per_period
48
19
  end
49
20
 
@@ -53,8 +24,8 @@ module Astronoby
53
24
  # @return [Array<Astronoby::ExtremumEvent>] Array of apoapsis events
54
25
  def apoapsis_events_between(start_time, end_time)
55
26
  find_extrema(
56
- Astronoby::Instant.from_time(start_time).tt,
57
- Astronoby::Instant.from_time(end_time).tt,
27
+ Instant.from_time(start_time).tt,
28
+ Instant.from_time(end_time).tt,
58
29
  type: :maximum
59
30
  )
60
31
  end
@@ -65,8 +36,8 @@ module Astronoby
65
36
  # @return [Array<Astronoby::ExtremumEvent>] Array of periapsis events
66
37
  def periapsis_events_between(start_time, end_time)
67
38
  find_extrema(
68
- Astronoby::Instant.from_time(start_time).tt,
69
- Astronoby::Instant.from_time(end_time).tt,
39
+ Instant.from_time(start_time).tt,
40
+ Instant.from_time(end_time).tt,
70
41
  type: :minimum
71
42
  )
72
43
  end
@@ -78,21 +49,24 @@ module Astronoby
78
49
  # @param type [Symbol] :maximum or :minimum
79
50
  # @return [Array<Astronoby::ExtremumEvent>] Array of extrema events
80
51
  def find_extrema(start_jd, end_jd, type: :maximum)
81
- # 1: Find extrema candidates through adaptive sampling
82
- candidates = find_extrema_candidates(start_jd, end_jd, type)
83
-
84
- # 2: Refine each candidate using golden section search
85
- refined_extrema = candidates
86
- .map { |candidate| refine_extremum(candidate, type) }
87
- .compact
88
-
89
- # 3: Remove duplicates and boundary artifacts
90
- refined_extrema = remove_duplicates(refined_extrema)
91
- filter_boundary_artifacts(refined_extrema, start_jd, end_jd)
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
92
58
  end
93
59
 
94
60
  private
95
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
+
96
70
  def distance_at(jd)
97
71
  instant = Instant.from_terrestrial_time(jd)
98
72
  body_geometric = @body.geometric(ephem: @ephem, instant: instant)
@@ -102,132 +76,5 @@ module Astronoby
102
76
  distance_vector = body_geometric.position - primary_geometric.position
103
77
  distance_vector.magnitude
104
78
  end
105
-
106
- def find_extrema_candidates(start_jd, end_jd, type)
107
- samples = collect_samples(start_jd, end_jd)
108
- find_local_extrema_in_samples(samples, type)
109
- end
110
-
111
- def collect_samples(start_jd, end_jd)
112
- duration = end_jd - start_jd
113
- sample_count = calculate_sample_count(duration)
114
- step = duration / sample_count
115
-
116
- (0..sample_count).map do |i|
117
- jd = start_jd + (i * step)
118
- {jd: jd, value: distance_at(jd)}
119
- end
120
- end
121
-
122
- def calculate_sample_count(duration)
123
- # Adaptive sampling: scale with duration and orbital period
124
- periods_in_range = duration / @orbital_period
125
- base_samples = (periods_in_range * @samples_per_period).to_i
126
-
127
- # Ensure minimum sample density for short ranges
128
- [base_samples, MIN_SAMPLES_PER_PERIOD].max
129
- end
130
-
131
- def find_local_extrema_in_samples(samples, type)
132
- candidates = []
133
-
134
- # Check each interior point for local extrema
135
- (1...samples.length - 1).each do |i|
136
- if local_extremum?(samples, i, type)
137
- candidates << {
138
- start_jd: samples[i - 1][:jd],
139
- end_jd: samples[i + 1][:jd],
140
- center_jd: samples[i][:jd]
141
- }
142
- end
143
- end
144
-
145
- candidates
146
- end
147
-
148
- def local_extremum?(samples, index, type)
149
- current_val = samples[index][:value].m
150
- prev_val = samples[index - 1][:value].m
151
- next_val = samples[index + 1][:value].m
152
-
153
- if type == :maximum
154
- current_val > prev_val && current_val > next_val
155
- else
156
- current_val < prev_val && current_val < next_val
157
- end
158
- end
159
-
160
- def refine_extremum(candidate, type)
161
- golden_section_search(candidate[:start_jd], candidate[:end_jd], type)
162
- end
163
-
164
- def golden_section_search(a, b, type)
165
- return nil if b <= a
166
-
167
- tol = GOLDEN_SECTION_TOLERANCE * (b - a).abs
168
-
169
- # Initial points using golden ratio
170
- x1 = a + (1 - INVPHI) * (b - a)
171
- x2 = a + INVPHI * (b - a)
172
-
173
- f1 = distance_at(x1).m
174
- f2 = distance_at(x2).m
175
-
176
- while (b - a).abs > tol
177
- should_keep_left = (type == :maximum) ? (f1 > f2) : (f1 < f2)
178
-
179
- if should_keep_left
180
- b = x2
181
- x2 = x1
182
- f2 = f1
183
- x1 = a + (1 - INVPHI) * (b - a)
184
- f1 = distance_at(x1).m
185
- else
186
- a = x1
187
- x1 = x2
188
- f1 = f2
189
- x2 = a + INVPHI * (b - a)
190
- f2 = distance_at(x2).m
191
- end
192
- end
193
-
194
- mid = (a + b) / 2
195
- ExtremumEvent.new(
196
- Instant.from_terrestrial_time(mid),
197
- distance_at(mid)
198
- )
199
- end
200
-
201
- def remove_duplicates(extrema)
202
- return extrema if extrema.length <= 1
203
-
204
- cleaned = [extrema.first]
205
-
206
- extrema.each_with_index do |current, i|
207
- next if i == 0
208
-
209
- is_duplicate = cleaned.any? do |existing|
210
- time_diff = (
211
- current.instant.tt - existing.instant.tt
212
- ).abs
213
- time_diff < DUPLICATE_THRESHOLD_DAYS
214
- end
215
-
216
- cleaned << current unless is_duplicate
217
- end
218
-
219
- cleaned
220
- end
221
-
222
- def filter_boundary_artifacts(extrema, start_jd, end_jd)
223
- extrema.reject do |extreme|
224
- start_diff = (extreme.instant.tt - start_jd).abs
225
- end_diff = (extreme.instant.tt - end_jd).abs
226
-
227
- too_close_to_start = start_diff < BOUNDARY_BUFFER_DAYS
228
- too_close_to_end = end_diff < BOUNDARY_BUFFER_DAYS
229
- too_close_to_start || too_close_to_end
230
- end
231
- end
232
79
  end
233
80
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class GreatestElongation
5
+ EASTERN = :eastern
6
+ WESTERN = :western
7
+
8
+ # @param instant [Astronoby::Instant] when the greatest elongation occurs
9
+ # @param body [Astronoby::Body] the body reaching greatest elongation
10
+ # @param angle [Astronoby::Angle] the Sun-Earth-body angle at that instant
11
+ # @return [Astronoby::GreatestElongation] a greatest eastern elongation
12
+ def self.eastern(instant:, body:, angle:)
13
+ new(instant: instant, body: body, angle: angle, direction: EASTERN)
14
+ end
15
+
16
+ # @param instant [Astronoby::Instant] when the greatest elongation occurs
17
+ # @param body [Astronoby::Body] the body reaching greatest elongation
18
+ # @param angle [Astronoby::Angle] the Sun-Earth-body angle at that instant
19
+ # @return [Astronoby::GreatestElongation] a greatest western elongation
20
+ def self.western(instant:, body:, angle:)
21
+ new(instant: instant, body: body, angle: angle, direction: WESTERN)
22
+ end
23
+
24
+ # @return [Astronoby::Instant] when the greatest elongation occurs
25
+ attr_reader :instant
26
+
27
+ # @return [Astronoby::Body] the body reaching greatest elongation
28
+ attr_reader :body
29
+
30
+ # @return [Astronoby::Angle] the Sun-Earth-body angle at that instant
31
+ attr_reader :angle
32
+
33
+ # @return [Symbol] +EASTERN+ or +WESTERN+
34
+ attr_reader :direction
35
+
36
+ # @param instant [Astronoby::Instant] when the greatest elongation occurs
37
+ # @param body [Astronoby::Body] the body reaching greatest elongation
38
+ # @param angle [Astronoby::Angle] the Sun-Earth-body angle at that instant
39
+ # @param direction [Symbol] +EASTERN+ or +WESTERN+
40
+ def initialize(instant:, body:, angle:, direction:)
41
+ @instant = instant
42
+ @body = body
43
+ @angle = angle
44
+ @direction = direction
45
+ freeze
46
+ end
47
+
48
+ # @return [Boolean] true for a greatest eastern elongation
49
+ def eastern?
50
+ @direction == EASTERN
51
+ end
52
+
53
+ # @return [Boolean] true for a greatest western elongation
54
+ def western?
55
+ @direction == WESTERN
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class GreatestElongationCalculator
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::GreatestElongation>] greatest elongations in
17
+ # the range
18
+ def greatest_elongation_events_between(start_time, end_time)
19
+ finder.extrema(
20
+ Instant.from_time(start_time).tt,
21
+ Instant.from_time(end_time).tt,
22
+ type: :maximum
23
+ ).map { |extremum| build_event(extremum) }
24
+ end
25
+
26
+ private
27
+
28
+ def finder
29
+ @finder ||= ExtremumFinder.new(
30
+ value_at: ->(jd) { planet_at(jd).elongation },
31
+ period: synodic_period,
32
+ samples_per_period: @samples_per_period
33
+ )
34
+ end
35
+
36
+ def build_event(extremum)
37
+ instant = Instant.from_terrestrial_time(extremum[:jd])
38
+ angle = extremum[:value]
39
+
40
+ if planet_at(instant.tt).eastern?
41
+ GreatestElongation.eastern(instant: instant, body: @body, angle: angle)
42
+ else
43
+ GreatestElongation.western(instant: instant, body: @body, angle: angle)
44
+ end
45
+ end
46
+
47
+ def planet_at(jd)
48
+ @body.new(instant: Instant.from_terrestrial_time(jd), ephem: @ephem)
49
+ end
50
+
51
+ def synodic_period
52
+ @synodic_period ||=
53
+ 1.0 / ((1.0 / @body::ORBITAL_PERIOD) - (1.0 / Earth::ORBITAL_PERIOD)).abs
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # A lunar eclipse: a geocentric passage of the Moon through Earth's shadow.
5
+ # Immutable; built by LunarEclipseCalculator.
6
+ #
7
+ # The penumbral phase is always present. The partial phase is present for
8
+ # partial and total eclipses, and the total phase only for total eclipses.
9
+ class LunarEclipse
10
+ PENUMBRAL = :penumbral
11
+ PARTIAL = :partial
12
+ TOTAL = :total
13
+
14
+ # @return [Astronoby::Instant] greatest eclipse, when the Moon's centre is
15
+ # least distant from the axis of Earth's shadow
16
+ attr_reader :instant
17
+ alias_method :greatest_eclipse_instant, :instant
18
+
19
+ # @return [Symbol] +PENUMBRAL+, +PARTIAL+ or +TOTAL+
20
+ attr_reader :kind
21
+
22
+ # @return [Astronoby::LunarEclipseGeometry] the geometry of Earth's shadow
23
+ # and the Moon's place in it at greatest eclipse. The geometry at each
24
+ # contact is on the phases.
25
+ attr_reader :geometry
26
+
27
+ # @return [Astronoby::LunarEclipsePhase] the penumbral phase (always present)
28
+ attr_reader :penumbral
29
+
30
+ # @return [Astronoby::LunarEclipsePhase, nil] the partial phase, present for
31
+ # partial and total eclipses
32
+ attr_reader :partial
33
+
34
+ # @return [Astronoby::LunarEclipsePhase, nil] the total phase (totality),
35
+ # present only for total eclipses
36
+ attr_reader :total
37
+
38
+ # @param instant [Astronoby::Instant] greatest eclipse
39
+ # @param kind [Symbol] +PENUMBRAL+, +PARTIAL+ or +TOTAL+
40
+ # @param geometry [Astronoby::LunarEclipseGeometry] the shadow geometry at
41
+ # greatest eclipse
42
+ # @param penumbral [Astronoby::LunarEclipsePhase] the penumbral phase
43
+ # @param partial [Astronoby::LunarEclipsePhase, nil] the partial phase
44
+ # @param total [Astronoby::LunarEclipsePhase, nil] the total phase
45
+ def initialize(
46
+ instant:,
47
+ kind:,
48
+ geometry:,
49
+ penumbral:,
50
+ partial: nil,
51
+ total: nil
52
+ )
53
+ @instant = instant
54
+ @kind = kind
55
+ @geometry = geometry
56
+ @penumbral = penumbral
57
+ @partial = partial
58
+ @total = total
59
+ freeze
60
+ end
61
+
62
+ # @return [Float] least distance of the Moon's centre from the axis of
63
+ # Earth's shadow at greatest eclipse, in Earth radii, positive when the
64
+ # Moon passes north of the axis
65
+ def gamma
66
+ sign = @geometry.north_of_axis? ? 1 : -1
67
+ sign * @geometry.axis_distance.m /
68
+ Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS
69
+ end
70
+
71
+ # @return [Float] fraction of the Moon's diameter immersed in the umbra at
72
+ # greatest eclipse (negative when the Moon misses the umbra)
73
+ def umbral_magnitude
74
+ @geometry.umbral_magnitude
75
+ end
76
+
77
+ # @return [Float] fraction of the Moon's diameter immersed in the penumbra
78
+ # at greatest eclipse
79
+ def penumbral_magnitude
80
+ @geometry.penumbral_magnitude
81
+ end
82
+
83
+ # @return [Astronoby::Distance] least distance of the Moon's centre from the
84
+ # axis of Earth's shadow at greatest eclipse. This is the unsigned length
85
+ # of which gamma is the value in Earth radii.
86
+ def shadow_axis_distance
87
+ @geometry.axis_distance
88
+ end
89
+
90
+ # @return [Boolean] true for a penumbral eclipse (the Moon misses the umbra)
91
+ def penumbral?
92
+ @kind == PENUMBRAL
93
+ end
94
+
95
+ # @return [Boolean] true for a partial eclipse
96
+ def partial?
97
+ @kind == PARTIAL
98
+ end
99
+
100
+ # @return [Boolean] true for a total eclipse
101
+ def total?
102
+ @kind == TOTAL
103
+ end
104
+
105
+ # @param observer [Astronoby::Observer] the observer
106
+ # @return [Astronoby::LunarEclipseVisibility] the local circumstances
107
+ def visibility_from(observer)
108
+ LunarEclipseVisibility.new(eclipse: self, observer: observer)
109
+ end
110
+ end
111
+ end