astronoby 0.10.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +134 -0
  3. data/README.md +13 -0
  4. data/UPGRADING.md +186 -0
  5. data/docs/README.md +32 -8
  6. data/docs/altitude_crossings.md +92 -0
  7. data/docs/configuration.md +8 -0
  8. data/docs/deep_sky_bodies.md +45 -0
  9. data/docs/glossary.md +2 -0
  10. data/docs/instant.md +14 -7
  11. data/docs/lunar_eclipses.md +128 -13
  12. data/docs/twilight_times.md +26 -11
  13. data/lib/astronoby/bodies/deep_sky_object_position.rb +6 -24
  14. data/lib/astronoby/cache.rb +13 -0
  15. data/lib/astronoby/constants.rb +1 -2
  16. data/lib/astronoby/constellations/data.rb +1 -1
  17. data/lib/astronoby/coordinates/equatorial.rb +15 -5
  18. data/lib/astronoby/delta_t.rb +44 -0
  19. data/lib/astronoby/events/altitude_crossing.rb +29 -0
  20. data/lib/astronoby/events/altitude_crossing_calculator.rb +511 -0
  21. data/lib/astronoby/events/altitude_extreme.rb +31 -0
  22. data/lib/astronoby/events/lunar_eclipse.rb +49 -37
  23. data/lib/astronoby/events/lunar_eclipse_calculator.rb +143 -31
  24. data/lib/astronoby/events/lunar_eclipse_geometry.rb +115 -0
  25. data/lib/astronoby/events/lunar_eclipse_phase.rb +45 -0
  26. data/lib/astronoby/events/lunar_eclipse_visibility.rb +296 -0
  27. data/lib/astronoby/events/rise_transit_set_calculator.rb +9 -13
  28. data/lib/astronoby/events/twilight_calculator.rb +37 -147
  29. data/lib/astronoby/horizon.rb +40 -0
  30. data/lib/astronoby/horizontal_transformation.rb +112 -0
  31. data/lib/astronoby/instant.rb +50 -13
  32. data/lib/astronoby/precession.rb +1 -1
  33. data/lib/astronoby/reference_frame.rb +8 -3
  34. data/lib/astronoby/reference_frames/apparent.rb +20 -8
  35. data/lib/astronoby/reference_frames/mean_of_date.rb +6 -0
  36. data/lib/astronoby/reference_frames/teme.rb +6 -0
  37. data/lib/astronoby/reference_frames/topocentric.rb +6 -0
  38. data/lib/astronoby/root_finder.rb +16 -9
  39. data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +10 -8
  40. data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +11 -9
  41. data/lib/astronoby/util/time.rb +0 -22
  42. data/lib/astronoby/version.rb +1 -1
  43. data/lib/astronoby.rb +9 -1
  44. metadata +40 -5
  45. data/lib/astronoby/events/eclipse_phase.rb +0 -27
@@ -1,6 +1,6 @@
1
1
  # Lunar Eclipses
2
2
 
3
- Astronoby computes lunar eclipses: the passages of the Moon through Earth's shadow. A lunar eclipse is a geocentric event, the same for every observer who can see the Moon, so no observer is involved. The geometry is built from the apparent geocentric positions of the Sun and Moon, which matches the standard reduction used by [NASA's Five Millennium Canon of Lunar Eclipses] and by IMCCE.
3
+ Astronoby computes lunar eclipses: the passages of the Moon through Earth's shadow. A lunar eclipse is a geocentric event, the same for every observer who can see the Moon, so finding one takes no observer. What one place sees of it depends only on whether the Moon is above its horizon, which is covered in [Local circumstances](#local-circumstances) below. The geometry is built from the apparent geocentric positions of the Sun and Moon, which matches the standard reduction used by [NASA's Five Millennium Canon of Lunar Eclipses] and by IMCCE.
4
4
 
5
5
  There are three kinds of lunar eclipses. A penumbral eclipse is when the Moon only enters Earth's penumbra, the outer, partial shadow. A partial eclipse is when part of the Moon enters the umbra, the inner, total shadow. A total eclipse is when the whole Moon enters the umbra.
6
6
 
@@ -18,20 +18,20 @@ eclipses = Astronoby::Moon.eclipse_events(
18
18
  )
19
19
 
20
20
  eclipses.map { |eclipse| [eclipse.instant.to_time, eclipse.kind] }
21
- # => [[2025-03-14 06:58:47 UTC, :total], [2025-09-07 18:11:49 UTC, :total]]
21
+ # => [[2025-03-14 06:58:46 UTC, :total], [2025-09-07 18:11:49 UTC, :total]]
22
22
  ```
23
23
 
24
24
  ## An eclipse
25
25
 
26
- Each eclipse is an `Astronoby::LunarEclipse`. Its `#instant` (also available as `#greatest_eclipse`) is the moment of greatest eclipse, when the Moon's centre is least distant from the axis of Earth's shadow.
26
+ Each eclipse is an `Astronoby::LunarEclipse`. Its `#instant` (also available as `#greatest_eclipse_instant`) is the moment of greatest eclipse, when the Moon's centre is least distant from the axis of Earth's shadow.
27
27
 
28
28
  ```rb
29
29
  eclipse = eclipses.first
30
30
 
31
31
  eclipse.kind # => :total
32
32
  eclipse.total? # => true
33
- eclipse.instant.to_time # => 2025-03-14 06:58:47 UTC
34
- eclipse.greatest_eclipse # => same as #instant
33
+ eclipse.instant.to_time # => 2025-03-14 06:58:46 UTC
34
+ eclipse.greatest_eclipse_instant # => same as #instant
35
35
  ```
36
36
 
37
37
  The umbral and penumbral magnitudes are the fractions of the Moon's diameter immersed in the umbra and the penumbra at greatest eclipse. The umbral magnitude is negative for a penumbral eclipse (the Moon misses the umbra), between 0 and 1 for a partial eclipse, and 1 or more for a total eclipse.
@@ -41,7 +41,7 @@ eclipse.umbral_magnitude # => 1.179
41
41
  eclipse.penumbral_magnitude # => 2.26
42
42
  ```
43
43
 
44
- `#gamma` is the least distance of the Moon's centre from the axis of Earth's shadow at greatest eclipse, in Earth radii, positive when the Moon passes north of the axis and negative when it passes south. It is the standard, dimensionless eclipse parameter used by IMCCE and NASA. The same quantity is also available as a `Astronoby::Distance` through `#shadow_axis_distance`.
44
+ `#gamma` is the least distance of the Moon's centre from the axis of Earth's shadow at greatest eclipse, in Earth radii, positive when the Moon passes north of the axis and negative when it passes south. It is the standard, dimensionless eclipse parameter used by IMCCE and NASA. The same quantity is also available as an `Astronoby::Distance` through `#shadow_axis_distance`, and the side of the axis it takes its sign from is on the geometry as `#north_of_axis?`.
45
45
 
46
46
  ```rb
47
47
  eclipse.gamma # => 0.348
@@ -50,21 +50,53 @@ eclipse.shadow_axis_distance.km # => 2222.37
50
50
 
51
51
  ## Phases
52
52
 
53
- An eclipse exposes its phases as `Astronoby::EclipsePhase` objects, each with a `#starting_instant`, an `#ending_instant`, and a `#duration` (an `Astronoby::Duration`). The penumbral phase is always present. The partial phase is present for partial and total eclipses, and the total phase (totality) only for total eclipses. A phase that does not occur is `nil`.
53
+ An eclipse exposes its phases as `Astronoby::LunarEclipsePhase` objects, each with a `#starting_instant`, an `#ending_instant`, and a `#duration` (an `Astronoby::Duration`). The penumbral phase is always present. The partial phase is present for partial and total eclipses, and the total phase (totality) only for total eclipses. A phase that does not occur is `nil`.
54
54
 
55
55
  ```rb
56
- eclipse.penumbral.starting_instant.to_time # => 2025-03-14 03:57:29 UTC (P1)
57
- eclipse.partial.starting_instant.to_time # => 2025-03-14 05:09:37 UTC (U1)
58
- eclipse.total.starting_instant.to_time # => 2025-03-14 06:26:01 UTC (U2)
59
- eclipse.total.ending_instant.to_time # => 2025-03-14 07:31:32 UTC (U3)
56
+ eclipse.penumbral.starting_instant.to_time # => 2025-03-14 03:57:28 UTC (P1)
57
+ eclipse.partial.starting_instant.to_time # => 2025-03-14 05:09:36 UTC (U1)
58
+ eclipse.total.starting_instant.to_time # => 2025-03-14 06:26:02 UTC (U2)
59
+ eclipse.total.ending_instant.to_time # => 2025-03-14 07:31:29 UTC (U3)
60
60
  eclipse.partial.ending_instant.to_time # => 2025-03-14 08:47:55 UTC (U4)
61
61
  eclipse.penumbral.ending_instant.to_time # => 2025-03-14 10:00:08 UTC (P4)
62
62
 
63
- eclipse.total.duration.seconds # => 3931 (seconds of totality)
63
+ eclipse.total.duration.seconds # => 3927.11 (seconds of totality)
64
64
  ```
65
65
 
66
66
  For a penumbral eclipse, `#partial` and `#total` are `nil`. For a partial eclipse, `#total` is `nil`.
67
67
 
68
+ ## Shadow geometry
69
+
70
+ `Astronoby::LunarEclipseGeometry` describes Earth's shadow and the Moon's place in it at one instant. The eclipse carries the geometry at greatest eclipse, and each phase carries the geometry at its two contacts, so all six contacts of a total eclipse are reachable as well as greatest eclipse. It gives the size of the two shadow cones, how far the Moon's centre is from the shadow axis, and where on the Moon's limb the shadow bites.
71
+
72
+ ```rb
73
+ geometry = eclipse.geometry
74
+
75
+ geometry.umbra_angular_radius.degrees * 60 # => 39.23 (arcminutes)
76
+ geometry.penumbra_angular_radius.degrees * 60 # => 71.39
77
+ geometry.angular_axis_distance.degrees * 60 # => 19.03
78
+ geometry.position_angle.degrees # => 29.07
79
+ ```
80
+
81
+ The cone radii are measured in the plane perpendicular to the shadow axis at the Moon's distance, which is where they are published. They are available as an `Astronoby::Distance` through `#umbra_radius` and `#penumbra_radius`, and as an `Astronoby::Angle` through `#umbra_angular_radius` and `#penumbra_angular_radius`. `#moon_distance` is the geocentric distance of the Moon, the distance of that plane.
82
+
83
+ `#position_angle` is the position angle of the contact point on the Moon's limb, seen from the Moon's centre, from celestial north through east. This is the quantity IMCCE and NASA publish. At greatest eclipse, where there is no contact point, it is the direction from the shadow axis to the Moon, which is what they report there too.
84
+
85
+ `#umbral_magnitude` and `#penumbral_magnitude` are available at any of these instants, not only at greatest eclipse. Each one is 0 at the contacts that define it, so the penumbral magnitude at P1 and P4 and the umbral magnitude at U1 and U4. The umbral magnitude is 1 at U2 and U3, where the Moon is wholly inside the umbra. The penumbral magnitude is well past 1 by then, since the Moon is far deeper into the penumbra than into the umbra.
86
+
87
+ ```rb
88
+ eclipse.penumbral.starting_geometry.penumbral_magnitude # => 0.0 (P1)
89
+ eclipse.partial.starting_geometry.umbral_magnitude # => 0.0 (U1)
90
+ eclipse.total.starting_geometry.umbral_magnitude # => 1.0 (U2)
91
+ ```
92
+
93
+ ```rb
94
+ eclipse.penumbral.starting_geometry.position_angle.degrees # => 131.8 (P1)
95
+ eclipse.total.starting_geometry.position_angle.degrees # => 350.46 (U2)
96
+ ```
97
+
98
+ Note that IMCCE labels the end of the penumbral phase `P2`, where this documentation uses `P4`.
99
+
68
100
  ```rb
69
101
  penumbral_eclipse = Astronoby::Moon.eclipse_events(
70
102
  ephem: ephem,
@@ -77,9 +109,92 @@ penumbral_eclipse.partial # => nil
77
109
  penumbral_eclipse.total # => nil
78
110
  ```
79
111
 
112
+ ## Local circumstances
113
+
114
+ The eclipse is the same everywhere, so there is no path of visibility to compute. All that changes from place to place is whether the Moon is above the horizon, and visibility is the intersection of the eclipse with the time the Moon spends up. Half the Earth sees an eclipse; the other half is in daylight, with the Moon below the horizon.
115
+
116
+ `LunarEclipse#visibility_from` takes an `Astronoby::Observer` and returns an `Astronoby::LunarEclipseVisibility`. No ephemeris is involved: the Moon's place at every contact is already carried by the geometry, so an eclipse can be observed from as many places as wanted, including after a round trip through a cache.
117
+
118
+ ```rb
119
+ paris = Astronoby::Observer.new(
120
+ latitude: Astronoby::Angle.from_degrees(48.8566),
121
+ longitude: Astronoby::Angle.from_degrees(2.3522)
122
+ )
123
+
124
+ visibility = eclipse.visibility_from(paris)
125
+
126
+ visibility.visible? # => true
127
+ visibility.entirely_visible? # => false
128
+ visibility.circumstance # => :ends_after_moonset
129
+ ```
130
+
131
+ `#circumstance` sums up what the place gets of the eclipse:
132
+
133
+ - `:not_visible`, the Moon is below the horizon for the whole eclipse
134
+ - `:entirely_visible`, the Moon is up from the first to the last penumbral contact
135
+ - `:begins_before_moonrise`, the eclipse is already under way when the Moon rises
136
+ - `:ends_after_moonset`, the eclipse is still under way when the Moon sets
137
+ - `:partially_visible`, anything else, which at high latitudes can be the Moon rising and setting within the eclipse
138
+
139
+ `#coverage` takes a phase and answers how much of it happens with the Moon up, in the same three words `#circumstance` uses of the whole eclipse: `:entirely_visible`, `:partially_visible`, `:not_visible`, or `nil` when the eclipse has no such phase. In the example, in Paris, the Moon sets during the partial phase, before totality begins.
140
+
141
+ ```rb
142
+ visibility.coverage(:penumbral) # => :partially_visible
143
+ visibility.coverage(:partial) # => :partially_visible
144
+ visibility.coverage(:total) # => :not_visible
145
+ ```
146
+
147
+ `#horizon_crossings` are the instants of the eclipse at which the Moon rises or sets, and `#observable_windows` are the stretches of the eclipse spent with the Moon up. There is one window for an eclipse seen whole or in part, none for one not seen at all, and more than one only where the Moon crosses the horizon repeatedly.
148
+
149
+ ```rb
150
+ visibility.horizon_crossings.map { |instant| instant.to_time }
151
+ # => [2025-03-14 06:11:41 UTC]
152
+
153
+ visibility.observable_windows.map { |window| [window.first.to_time, window.last.to_time] }
154
+ # => [[2025-03-14 03:57:28 UTC, 2025-03-14 06:11:41 UTC]]
155
+ ```
156
+
157
+ From Mexico City the same eclipse runs from end to end with the Moon high in the sky.
158
+
159
+ ```rb
160
+ mexico_city = Astronoby::Observer.new(
161
+ latitude: Astronoby::Angle.from_degrees(19.4326),
162
+ longitude: Astronoby::Angle.from_degrees(-99.1332)
163
+ )
164
+
165
+ visibility = eclipse.visibility_from(mexico_city)
166
+
167
+ visibility.circumstance # => :entirely_visible
168
+ visibility.coverage(:total) # => :entirely_visible
169
+ visibility.horizon_crossings # => []
170
+ ```
171
+
172
+ `#above_horizon_at?` answers whether the Moon is up at an instant of the eclipse, and raises outside it.
173
+
174
+ ```rb
175
+ visibility.above_horizon_at?(eclipse.total.starting_instant) # => true
176
+ ```
177
+
178
+ Visibility answers whether the Moon is up, not how high it is. The Moon's place at each contact is interpolated from what the eclipse carries, which is what lets an eclipse be observed from anywhere without an ephemeris, and that is accurate well past what the horizon convention can resolve. For the altitude itself, ask the Moon, which computes it from the ephemeris with no interpolation at all.
179
+
180
+ ```rb
181
+ Astronoby::Moon
182
+ .new(ephem: ephem, instant: eclipse.total.starting_instant)
183
+ .observed_by(mexico_city)
184
+ .horizontal
185
+ .altitude
186
+ .degrees # => 72.45
187
+ ```
188
+
189
+ The Moon counts as up once its upper limb clears the refracted horizon, which is the convention Astronoby uses for moonrise and moonset, through `Astronoby::Horizon`.
190
+
80
191
  ## Precision
81
192
 
82
- Earth's shadow is enlarged by its atmosphere. Astronoby enlarges Earth's radius by 1/99 before building the shadow cones, a factor calibrated against IMCCE. Validated across the 2023 to 2025 eclipses, the eclipse kind, greatest eclipse instant, magnitudes, and contact times all match IMCCE to within a second or two.
193
+ Two conventions shape the result. Earth's shadow is enlarged by its atmosphere, so Astronoby adds a 64 km shell to Earth's radius before building the shadow cones. And eclipse contacts are reduced with the IAU eclipse constant k, which puts the Moon's radius at 1738.09 km, slightly above its physical equatorial radius. Both are calibrated against IMCCE.
194
+
195
+ Validated against IMCCE, the eclipse kind and the magnitudes match, and the contacts agree to within a couple of seconds. The shadow radii and the distance from the shadow axis match to well under an arcsecond, and position angles to 0.03 degrees.
196
+
197
+ The local circumstances are reduced without the ephemeris, from the Moon's place at the contacts and the sidereal rate. The altitudes agree with the topocentric places computed from the ephemeris to under an arcminute, and the horizon crossings put the Moon on the horizon to a few arcseconds, which is well inside the uncertainty of the fixed refraction the convention assumes.
83
198
 
84
199
  ## See also
85
200
 
@@ -77,13 +77,13 @@ events = calculator.events_between(
77
77
 
78
78
  events.morning_civil_twilight_times
79
79
  # =>
80
- # [2025-08-01 02:29:17 UTC,
81
- # 2025-08-02 02:30:21 UTC,
82
- # 2025-08-03 02:31:25 UTC,
83
- # 2025-08-04 02:32:30 UTC,
84
- # 2025-08-05 02:33:34 UTC,
85
- # 2025-08-06 02:34:39 UTC,
86
- # 2025-08-07 02:35:44 UTC]
80
+ # [2025-08-01 02:29:11 UTC,
81
+ # 2025-08-02 02:30:15 UTC,
82
+ # 2025-08-03 02:31:20 UTC,
83
+ # 2025-08-04 02:32:24 UTC,
84
+ # 2025-08-05 02:33:29 UTC,
85
+ # 2025-08-06 02:34:34 UTC,
86
+ # 2025-08-07 02:35:39 UTC]
87
87
  ```
88
88
 
89
89
  ## `#event_on`
@@ -104,23 +104,38 @@ event.morning_astronomical_twilight_time
104
104
  # => 2025-05-01 01:16:21 UTC
105
105
 
106
106
  event.morning_nautical_twilight_time
107
- # => 2025-05-01 01:55:54 UTC
107
+ # => 2025-05-01 01:55:49 UTC
108
108
 
109
109
  event.evening_civil_twilight_time
110
- # => 2025-05-01 17:30:45 UTC
110
+ # => 2025-05-01 17:30:50 UTC
111
111
 
112
112
  event.evening_nautical_twilight_time
113
- # => 2025-05-01 18:07:13 UTC
113
+ # => 2025-05-01 18:07:27 UTC
114
114
 
115
115
  event.evening_astronomical_twilight_time
116
- # => 2025-05-01 18:46:47 UTC
116
+ # => 2025-05-01 18:47:10 UTC
117
117
  ```
118
118
 
119
+ ## Accuracy and polar regions
120
+
121
+ Each twilight time is the moment the Sun's centre, without refraction, reaches
122
+ the matching altitude, found by `Astronoby::AltitudeCrossingCalculator`. Times
123
+ are rounded to the second. Twilight that exists is returned even when the Sun
124
+ does not rise or set that day, as in a polar night. Use the [altitude crossing
125
+ calculator](altitude_crossings.md) directly for other altitudes or for unrounded
126
+ instants.
127
+
128
+ Solving for the real crossing costs more than approximating the Sun's position
129
+ from a single evaluation, as versions before 0.11.0 did, which could be off by
130
+ up to 30 seconds. `#event_on` takes around 7.5 milliseconds for one day, against
131
+ around 4 before.
132
+
119
133
  [Ephem page]: ephem.md
120
134
  [Observer page]: observer.md
121
135
 
122
136
  ## See also
123
137
 
138
+ - [Altitude Crossings](altitude_crossings.md) - for any altitude, unrounded
124
139
  - [Rise, Transit and Set Times](rise_transit_set_times.md) - for sun and moon events
125
140
  - [Observer](observer.md) - for location setup
126
141
  - [Ephemerides](ephem.md) - for data sources
@@ -113,30 +113,12 @@ module Astronoby
113
113
  end
114
114
 
115
115
  def compute_apparent
116
- @apparent = if @earth_geometric
117
- Apparent.build_from_astrometric(
118
- instant: @instant,
119
- target_astrometric: astrometric,
120
- earth_geometric: @earth_geometric,
121
- target_body: body
122
- )
123
- else
124
- precession_matrix = Precession.matrix_for(@instant)
125
- nutation_matrix = Nutation.matrix_for(@instant)
126
- corrected_position = Distance.vector_from_meters(
127
- precession_matrix * nutation_matrix * astrometric.position.map(&:m)
128
- )
129
- corrected_velocity = Velocity.vector_from_mps(
130
- precession_matrix * nutation_matrix * astrometric.velocity.map(&:mps)
131
- )
132
- Apparent.new(
133
- position: corrected_position,
134
- velocity: corrected_velocity,
135
- instant: @instant,
136
- center: Center.geocentric,
137
- target_body: body
138
- )
139
- end
116
+ @apparent = Apparent.build_from_astrometric(
117
+ instant: @instant,
118
+ target_astrometric: astrometric,
119
+ earth_geometric: @earth_geometric,
120
+ target_body: body
121
+ )
140
122
  end
141
123
  end
142
124
  end
@@ -175,6 +175,19 @@ module Astronoby
175
175
  [type, rounded_tt, *components]
176
176
  end
177
177
 
178
+ # Generate a cache key for a UTC time, without rounding. Sidereal time
179
+ # moves by about 15 arcseconds per second, so unlike nutation it can't
180
+ # share an entry with a nearby instant.
181
+ # @param type [Symbol] The calculation type (e.g., :gmst)
182
+ # @param utc [Time] The UTC time
183
+ # @param components [Array] Additional components for the key
184
+ # @return [Array, nil] The complete cache key, nil if cache is disabled
185
+ def generate_for_utc(type, utc, *components)
186
+ return nil unless Astronoby.configuration.cache_enabled?
187
+
188
+ [type, utc.to_r, *components]
189
+ end
190
+
178
191
  # Round terrestrial time to specified precision
179
192
  # @param tt [Numeric] Terrestrial time value
180
193
  # @param precision [Numeric] Precision in seconds
@@ -36,6 +36,7 @@ module Astronoby
36
36
 
37
37
  # WGS84 Earth Constants
38
38
  WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS = 6378137
39
+ IAU_MOON_RADIUS_IN_METERS = 0.2725076 * WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS
39
40
  WGS84_INVERSE_FLATTENING = 298.257223563
40
41
  WGS84_FLATTENING = 1.0 / WGS84_INVERSE_FLATTENING
41
42
  WGS84_ECCENTICITY_SQUARED = 2.0 * WGS84_FLATTENING - WGS84_FLATTENING * WGS84_FLATTENING
@@ -45,7 +46,5 @@ module Astronoby
45
46
  EARTH_FLATTENING_CORRECTION = 0.996647
46
47
 
47
48
  EARTH_ANGULAR_VELOCITY_RAD_PER_S = 7.2921159e-5
48
-
49
- TAI_TT_OFFSET = 32.184
50
49
  end
51
50
  end
@@ -31,7 +31,7 @@ module Astronoby
31
31
  end
32
32
 
33
33
  def self.read_file(filename)
34
- File.readlines(data_path(filename)).map(&:strip)
34
+ File.readlines(data_path(filename), encoding: "UTF-8").map(&:strip)
35
35
  end
36
36
 
37
37
  def self.data_path(filename)
@@ -32,17 +32,23 @@ module Astronoby
32
32
  @epoch = epoch
33
33
  end
34
34
 
35
+ # @param epoch [Numeric] Julian Date epoch the coordinates are referred to
35
36
  # @return [Astronoby::Coordinates::Equatorial] zero coordinates
36
- def self.zero
37
- new(declination: Angle.zero, right_ascension: Angle.zero)
37
+ def self.zero(epoch: JulianDate::DEFAULT_EPOCH)
38
+ new(
39
+ declination: Angle.zero,
40
+ right_ascension: Angle.zero,
41
+ epoch: epoch
42
+ )
38
43
  end
39
44
 
40
45
  # Derives equatorial coordinates from a position vector.
41
46
  #
42
47
  # @param position [Astronoby::Vector<Astronoby::Distance>] position vector
48
+ # @param epoch [Numeric] Julian Date epoch the vector is referred to
43
49
  # @return [Astronoby::Coordinates::Equatorial] equatorial coordinates
44
- def self.from_position_vector(position)
45
- return zero if position.zero?
50
+ def self.from_position_vector(position, epoch: JulianDate::DEFAULT_EPOCH)
51
+ return zero(epoch: epoch) if position.zero?
46
52
 
47
53
  term1 = position.z.m
48
54
  term2 = position.magnitude.m
@@ -58,7 +64,11 @@ module Astronoby
58
64
  angle
59
65
  )
60
66
 
61
- new(declination: declination, right_ascension: right_ascension)
67
+ new(
68
+ declination: declination,
69
+ right_ascension: right_ascension,
70
+ epoch: epoch
71
+ )
62
72
  end
63
73
 
64
74
  # Computes the hour angle for a given time and observer longitude.
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "iers"
4
+
5
+ module Astronoby
6
+ module DeltaT
7
+ class << self
8
+ # @param instant [Numeric, Time, Date, DateTime] a Julian Date, or a date
9
+ # and time. Any time scale will do.
10
+ # @return [Numeric] ΔT in seconds
11
+ # @raise [Astronoby::IncompatibleArgumentsError] if the instant is not a
12
+ # Numeric, Time, Date or DateTime
13
+ def at(instant)
14
+ case instant
15
+ when Numeric
16
+ IERS::DeltaT.at(jd: instant).delta_t
17
+ when ::Time, ::Date, ::DateTime
18
+ IERS::DeltaT.at(instant).delta_t
19
+ else
20
+ raise IncompatibleArgumentsError,
21
+ "Expected a Numeric, Time, Date or DateTime object, got #{instant.class}"
22
+ end
23
+ rescue IERS::OutOfRangeError => error
24
+ outside_available_range(error)
25
+ end
26
+
27
+ private
28
+
29
+ # IERS covers 1800 up to the end of its EOP series, and refuses anything
30
+ # outside. Rather than propagate that, we answer with the closest value
31
+ # we have: 0 before 1800, where ΔT is small next to the uncertainty on
32
+ # it, and the last measured value after the series, which drifts slowly
33
+ # enough to beat any extrapolation we could invent.
34
+ def outside_available_range(error)
35
+ last_measured = IERS::Data.finals_entries.last
36
+ return 0 if last_measured.nil?
37
+ return 0 if error.requested_mjd.nil?
38
+ return 0 if error.requested_mjd < last_measured.mjd
39
+
40
+ IERS::DeltaT.at(mjd: last_measured.mjd).delta_t
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # A moment when a body's altitude passes through a given value.
5
+ class AltitudeCrossing
6
+ # @return [Astronoby::Instant] when the crossing happens, unrounded
7
+ attr_reader :instant
8
+
9
+ # @return [Symbol] +:rising+ when the altitude increases through the
10
+ # target, +:setting+ when it decreases
11
+ attr_reader :direction
12
+
13
+ # @param instant [Astronoby::Instant] when the crossing happens
14
+ # @param direction [Symbol] +:rising+ or +:setting+
15
+ # @param grazing [Boolean] whether the body only just reaches the target
16
+ def initialize(instant:, direction:, grazing: false)
17
+ @instant = instant
18
+ @direction = direction
19
+ @grazing = grazing
20
+ end
21
+
22
+ # @return [Boolean] true when the target is within a few hundredths of a
23
+ # degree of the body's highest or lowest altitude, so the crossing is
24
+ # nearly tangent and its instant is sensitive to small errors
25
+ def grazing?
26
+ @grazing
27
+ end
28
+ end
29
+ end