astronoby 0.6.0 → 0.7.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/.standard.yml +1 -0
  4. data/CHANGELOG.md +116 -0
  5. data/Gemfile.lock +45 -23
  6. data/README.md +42 -285
  7. data/UPGRADING.md +238 -0
  8. data/lib/astronoby/aberration.rb +56 -31
  9. data/lib/astronoby/angle.rb +20 -16
  10. data/lib/astronoby/angles/dms.rb +2 -2
  11. data/lib/astronoby/angles/hms.rb +2 -2
  12. data/lib/astronoby/bodies/earth.rb +56 -0
  13. data/lib/astronoby/bodies/jupiter.rb +11 -0
  14. data/lib/astronoby/bodies/mars.rb +11 -0
  15. data/lib/astronoby/bodies/mercury.rb +11 -0
  16. data/lib/astronoby/bodies/moon.rb +50 -290
  17. data/lib/astronoby/bodies/neptune.rb +11 -0
  18. data/lib/astronoby/bodies/saturn.rb +11 -0
  19. data/lib/astronoby/bodies/solar_system_body.rb +122 -0
  20. data/lib/astronoby/bodies/sun.rb +16 -220
  21. data/lib/astronoby/bodies/uranus.rb +11 -0
  22. data/lib/astronoby/bodies/venus.rb +11 -0
  23. data/lib/astronoby/constants.rb +13 -1
  24. data/lib/astronoby/coordinates/ecliptic.rb +2 -37
  25. data/lib/astronoby/coordinates/equatorial.rb +25 -7
  26. data/lib/astronoby/coordinates/horizontal.rb +0 -46
  27. data/lib/astronoby/corrections/light_time_delay.rb +90 -0
  28. data/lib/astronoby/deflection.rb +187 -0
  29. data/lib/astronoby/distance.rb +9 -0
  30. data/lib/astronoby/ephem.rb +39 -0
  31. data/lib/astronoby/equinox_solstice.rb +21 -18
  32. data/lib/astronoby/errors.rb +4 -0
  33. data/lib/astronoby/events/moon_phases.rb +2 -1
  34. data/lib/astronoby/events/rise_transit_set_calculator.rb +352 -0
  35. data/lib/astronoby/events/rise_transit_set_event.rb +13 -0
  36. data/lib/astronoby/events/rise_transit_set_events.rb +13 -0
  37. data/lib/astronoby/events/twilight_calculator.rb +166 -0
  38. data/lib/astronoby/events/twilight_event.rb +28 -0
  39. data/lib/astronoby/instant.rb +171 -0
  40. data/lib/astronoby/mean_obliquity.rb +23 -10
  41. data/lib/astronoby/nutation.rb +227 -42
  42. data/lib/astronoby/observer.rb +55 -0
  43. data/lib/astronoby/precession.rb +91 -17
  44. data/lib/astronoby/reference_frame.rb +49 -0
  45. data/lib/astronoby/reference_frames/apparent.rb +60 -0
  46. data/lib/astronoby/reference_frames/astrometric.rb +21 -0
  47. data/lib/astronoby/reference_frames/geometric.rb +20 -0
  48. data/lib/astronoby/reference_frames/mean_of_date.rb +38 -0
  49. data/lib/astronoby/reference_frames/topocentric.rb +82 -0
  50. data/lib/astronoby/true_obliquity.rb +2 -1
  51. data/lib/astronoby/util/maths.rb +70 -73
  52. data/lib/astronoby/util/time.rb +454 -31
  53. data/lib/astronoby/vector.rb +36 -0
  54. data/lib/astronoby/velocity.rb +116 -0
  55. data/lib/astronoby/version.rb +1 -1
  56. data/lib/astronoby.rb +26 -5
  57. metadata +61 -16
  58. data/.tool-versions +0 -1
  59. data/lib/astronoby/astronomical_models/ephemeride_lunaire_parisienne.rb +0 -143
  60. data/lib/astronoby/events/observation_events.rb +0 -285
  61. data/lib/astronoby/events/rise_transit_set_iteration.rb +0 -218
  62. data/lib/astronoby/events/twilight_events.rb +0 -121
  63. data/lib/astronoby/util/astrodynamics.rb +0 -60
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class RiseTransitSetEvent
5
+ attr_reader :rising_time, :transit_time, :setting_time
6
+
7
+ def initialize(rising, transit, setting)
8
+ @rising_time = rising
9
+ @transit_time = transit
10
+ @setting_time = setting
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class RiseTransitSetEvents
5
+ attr_reader :rising_times, :transit_times, :setting_times
6
+
7
+ def initialize(risings, transits, settings)
8
+ @rising_times = risings
9
+ @transit_times = transits
10
+ @setting_times = settings
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class TwilightCalculator
5
+ TWILIGHTS = [
6
+ CIVIL = :civil,
7
+ NAUTICAL = :nautical,
8
+ ASTRONOMICAL = :astronomical
9
+ ].freeze
10
+
11
+ TWILIGHT_ANGLES = {
12
+ CIVIL => Angle.from_degrees(96),
13
+ NAUTICAL => Angle.from_degrees(102),
14
+ ASTRONOMICAL => Angle.from_degrees(108)
15
+ }.freeze
16
+
17
+ PERIODS_OF_THE_DAY = [
18
+ MORNING = :morning,
19
+ EVENING = :evening
20
+ ].freeze
21
+
22
+ def initialize(observer:, ephem:)
23
+ @observer = observer
24
+ @ephem = ephem
25
+ end
26
+
27
+ def event_on(date)
28
+ observation_events = get_observation_events(date)
29
+ midday_instant = create_midday_instant(date)
30
+ sun_at_midday = Sun.new(instant: midday_instant, ephem: @ephem)
31
+ equatorial_coordinates = sun_at_midday.apparent.equatorial
32
+
33
+ morning_civil = compute_twilight_time(
34
+ MORNING,
35
+ TWILIGHT_ANGLES[CIVIL],
36
+ observation_events,
37
+ equatorial_coordinates
38
+ )
39
+
40
+ evening_civil = compute_twilight_time(
41
+ EVENING,
42
+ TWILIGHT_ANGLES[CIVIL],
43
+ observation_events,
44
+ equatorial_coordinates
45
+ )
46
+
47
+ morning_nautical = compute_twilight_time(
48
+ MORNING,
49
+ TWILIGHT_ANGLES[NAUTICAL],
50
+ observation_events,
51
+ equatorial_coordinates
52
+ )
53
+
54
+ evening_nautical = compute_twilight_time(
55
+ EVENING,
56
+ TWILIGHT_ANGLES[NAUTICAL],
57
+ observation_events,
58
+ equatorial_coordinates
59
+ )
60
+
61
+ morning_astronomical = compute_twilight_time(
62
+ MORNING,
63
+ TWILIGHT_ANGLES[ASTRONOMICAL],
64
+ observation_events,
65
+ equatorial_coordinates
66
+ )
67
+
68
+ evening_astronomical = compute_twilight_time(
69
+ EVENING,
70
+ TWILIGHT_ANGLES[ASTRONOMICAL],
71
+ observation_events,
72
+ equatorial_coordinates
73
+ )
74
+
75
+ TwilightEvent.new(
76
+ morning_civil_twilight_time: morning_civil,
77
+ evening_civil_twilight_time: evening_civil,
78
+ morning_nautical_twilight_time: morning_nautical,
79
+ evening_nautical_twilight_time: evening_nautical,
80
+ morning_astronomical_twilight_time: morning_astronomical,
81
+ evening_astronomical_twilight_time: evening_astronomical
82
+ )
83
+ end
84
+
85
+ def time_for_zenith_angle(date:, period_of_the_day:, zenith_angle:)
86
+ unless PERIODS_OF_THE_DAY.include?(period_of_the_day)
87
+ raise IncompatibleArgumentsError,
88
+ "Only #{PERIODS_OF_THE_DAY.join(" or ")} are allowed as period_of_the_day, got #{period_of_the_day}"
89
+ end
90
+
91
+ observation_events = get_observation_events(date)
92
+ midday_instant = create_midday_instant(date)
93
+ sun_at_midday = Sun.new(instant: midday_instant, ephem: @ephem)
94
+ equatorial_coordinates = sun_at_midday.apparent.equatorial
95
+
96
+ compute_twilight_time(
97
+ period_of_the_day,
98
+ zenith_angle,
99
+ observation_events,
100
+ equatorial_coordinates
101
+ )
102
+ end
103
+
104
+ private
105
+
106
+ def create_midday_instant(date)
107
+ time = Time.utc(date.year, date.month, date.day, 12)
108
+ Instant.from_time(time)
109
+ end
110
+
111
+ def get_observation_events(date)
112
+ Astronoby::RiseTransitSetCalculator.new(
113
+ body: Sun,
114
+ observer: @observer,
115
+ ephem: @ephem
116
+ ).event_on(date)
117
+ end
118
+
119
+ def compute_twilight_time(
120
+ period_of_the_day,
121
+ zenith_angle,
122
+ observation_events,
123
+ equatorial_coordinates
124
+ )
125
+ period_time = if period_of_the_day == MORNING
126
+ observation_events.rising_time
127
+ else
128
+ observation_events.setting_time
129
+ end
130
+
131
+ # If the sun doesn't rise or set on this day, we can't calculate
132
+ # twilight
133
+ return nil unless period_time
134
+
135
+ hour_angle_at_period = equatorial_coordinates
136
+ .compute_hour_angle(time: period_time, longitude: @observer.longitude)
137
+
138
+ term1 = zenith_angle.cos -
139
+ @observer.latitude.sin * equatorial_coordinates.declination.sin
140
+ term2 = @observer.latitude.cos * equatorial_coordinates.declination.cos
141
+ hour_angle_ratio_at_twilight = term1 / term2
142
+
143
+ # Check if twilight occurs at this location and date
144
+ return nil unless hour_angle_ratio_at_twilight.between?(-1, 1)
145
+
146
+ hour_angle_at_twilight = Angle.acos(hour_angle_ratio_at_twilight)
147
+ time_sign = -1
148
+
149
+ if period_of_the_day == MORNING
150
+ hour_angle_at_twilight = Angle.from_degrees(
151
+ Constants::DEGREES_PER_CIRCLE - hour_angle_at_twilight.degrees
152
+ )
153
+ time_sign = 1
154
+ end
155
+
156
+ twilight_in_hours =
157
+ time_sign * (hour_angle_at_twilight - hour_angle_at_period).hours *
158
+ GreenwichSiderealTime::SIDEREAL_MINUTE_IN_UT_MINUTE
159
+ twilight_in_seconds = time_sign *
160
+ twilight_in_hours *
161
+ Constants::SECONDS_PER_HOUR
162
+
163
+ (period_time + twilight_in_seconds).round
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class TwilightEvent
5
+ attr_reader :morning_civil_twilight_time,
6
+ :evening_civil_twilight_time,
7
+ :morning_nautical_twilight_time,
8
+ :evening_nautical_twilight_time,
9
+ :morning_astronomical_twilight_time,
10
+ :evening_astronomical_twilight_time
11
+
12
+ def initialize(
13
+ morning_civil_twilight_time: nil,
14
+ evening_civil_twilight_time: nil,
15
+ morning_nautical_twilight_time: nil,
16
+ evening_nautical_twilight_time: nil,
17
+ morning_astronomical_twilight_time: nil,
18
+ evening_astronomical_twilight_time: nil
19
+ )
20
+ @morning_civil_twilight_time = morning_civil_twilight_time
21
+ @evening_civil_twilight_time = evening_civil_twilight_time
22
+ @morning_nautical_twilight_time = morning_nautical_twilight_time
23
+ @evening_nautical_twilight_time = evening_nautical_twilight_time
24
+ @morning_astronomical_twilight_time = morning_astronomical_twilight_time
25
+ @evening_astronomical_twilight_time = evening_astronomical_twilight_time
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Astronoby
6
+ # Represents a specific instant in time using Terrestrial Time (TT) as its
7
+ # internal representation. This class provides conversions between different
8
+ # time scales commonly used in astronomy:
9
+ # - Terrestrial Time (TT)
10
+ # - International Atomic Time (TAI)
11
+ # - Universal Time Coordinated (UTC)
12
+ # - Greenwich Mean Sidereal Time (GMST)
13
+ #
14
+ # @example Create an instant from the current time
15
+ # instant = Astronoby::Instant.from_time(Time.now)
16
+ # instant.tai # Get International Atomic Time
17
+ #
18
+ # @example Create an instant from Terrestrial Time
19
+ # instant = Astronoby::Instant.from_terrestrial_time(2460000.5)
20
+ #
21
+ class Instant
22
+ include Comparable
23
+
24
+ JULIAN_DAY_NUMBER_OFFSET = 0.5
25
+
26
+ class << self
27
+ # Creates a new Instant from a Terrestrial Time value
28
+ #
29
+ # @param terrestrial_time [Numeric] the Terrestrial Time as a Julian Date
30
+ # @return [Astronoby::Instant] a new Instant object
31
+ # @raise [Astronoby::UnsupportedFormatError] if terrestrial_time is not
32
+ # numeric
33
+ def from_terrestrial_time(terrestrial_time)
34
+ new(terrestrial_time)
35
+ end
36
+
37
+ # Creates a new Instant from a Time object
38
+ #
39
+ # @param time [Time] a Time object to convert
40
+ # @return [Astronoby::Instant] a new Instant object
41
+ def from_time(time)
42
+ delta_t = Util::Time.terrestrial_universal_time_delta(time)
43
+ terrestrial_time = time.utc.to_datetime.ajd +
44
+ Rational(delta_t, Constants::SECONDS_PER_DAY)
45
+ from_terrestrial_time(terrestrial_time)
46
+ end
47
+
48
+ # Creates a new Instant from a Julian Date in UTC
49
+ #
50
+ # @param julian_date [Numeric] the Julian Date in UTC
51
+ # @return [Astronoby::Instant] a new Instant object
52
+ def from_utc_julian_date(julian_date)
53
+ delta_t = Util::Time.terrestrial_universal_time_delta(julian_date)
54
+ terrestrial_time = julian_date +
55
+ Rational(delta_t, Constants::SECONDS_PER_DAY)
56
+ from_terrestrial_time(terrestrial_time)
57
+ end
58
+ end
59
+
60
+ attr_reader :terrestrial_time
61
+ alias_method :tt, :terrestrial_time
62
+ alias_method :julian_date, :terrestrial_time
63
+
64
+ # Initialize a new Instant
65
+ #
66
+ # @param terrestrial_time [Numeric] the Terrestrial Time as Julian Date
67
+ # @raise [Astronoby::UnsupportedFormatError] if terrestrial_time is not
68
+ # numeric
69
+ def initialize(terrestrial_time)
70
+ unless terrestrial_time.is_a?(Numeric)
71
+ raise UnsupportedFormatError, "terrestrial_time must be a Numeric"
72
+ end
73
+
74
+ @terrestrial_time = terrestrial_time
75
+ freeze
76
+ end
77
+
78
+ # Calculate the time difference between two Instant objects
79
+ #
80
+ # @param other [Astronoby::Instant] another Instant to compare with
81
+ # @return [Numeric] the difference in days
82
+ def diff(other)
83
+ @terrestrial_time - other.terrestrial_time
84
+ end
85
+
86
+ # Convert to DateTime (UTC)
87
+ #
88
+ # @return [DateTime] the UTC time as DateTime
89
+ def to_datetime
90
+ DateTime.jd(
91
+ @terrestrial_time -
92
+ Rational(delta_t / Constants::SECONDS_PER_DAY) +
93
+ JULIAN_DAY_NUMBER_OFFSET
94
+ )
95
+ end
96
+
97
+ # Convert to Date (UTC)
98
+ #
99
+ # @return [Date] the UTC date
100
+ def to_date
101
+ to_datetime.to_date
102
+ end
103
+
104
+ # Convert to Time (UTC)
105
+ #
106
+ # @return [Time] the UTC time
107
+ def to_time
108
+ to_datetime.to_time.utc
109
+ end
110
+
111
+ # Get the ΔT (Delta T) value for this instant
112
+ # ΔT is the difference between TT and UT1
113
+ #
114
+ # @return [Numeric] Delta T in seconds
115
+ def delta_t
116
+ Util::Time.terrestrial_universal_time_delta(@terrestrial_time)
117
+ end
118
+
119
+ # Get the Greenwich Mean Sidereal Time
120
+ #
121
+ # @return [Numeric] the sidereal time in radians
122
+ def gmst
123
+ GreenwichSiderealTime.from_utc(to_time).time
124
+ end
125
+
126
+ # Get the International Atomic Time (TAI)
127
+ #
128
+ # @return [Numeric] TAI as Julian Date
129
+ def tai
130
+ @terrestrial_time -
131
+ Rational(Constants::TAI_TT_OFFSET, Constants::SECONDS_PER_DAY)
132
+ end
133
+
134
+ # Get the Barycentric Dynamical Time (TDB)
135
+ # Note: Currently approximated as equal to TT
136
+ #
137
+ # @return [Numeric] TDB as Julian Date
138
+ def tdb
139
+ # This is technically false, there is a slight difference between TT and
140
+ # TDB. However, this difference is so small that currenly Astronoby
141
+ # doesn't support it and consider they are the same value.
142
+ @terrestrial_time
143
+ end
144
+
145
+ # Get the offset between TT and UTC for this instant
146
+ #
147
+ # @return [Numeric] the offset in days
148
+ def utc_offset
149
+ @terrestrial_time - to_time.utc.to_datetime.ajd
150
+ end
151
+
152
+ # Calculate hash value for the instant
153
+ #
154
+ # @return [Integer] hash value
155
+ def hash
156
+ [@terrestrial_time, self.class].hash
157
+ end
158
+
159
+ # Compare this instant with another
160
+ #
161
+ # @param other [Astronoby::Instant] another instant to compare with
162
+ # @return [Integer, nil] -1, 0, 1 for less than, equal to, greater than;
163
+ # nil if other is not an Instant
164
+ def <=>(other)
165
+ return unless other.is_a?(self.class)
166
+
167
+ @terrestrial_time <=> other.terrestrial_time
168
+ end
169
+ alias_method :eql?, :==
170
+ end
171
+ end
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "date"
3
+ # TODO: This needs to be improved by receiving an instant instead of an Epoch
4
+ # as these coefficients work with TT (Terrestrial Time).
4
5
 
5
6
  module Astronoby
6
7
  class MeanObliquity
7
8
  # Source:
8
9
  # IAU resolution in 2006 in favor of the P03 astronomical model
9
- # The Astronomical Almanac for 2010
10
+ # https://syrte.obspm.fr/iau2006/aa03_412_P03.pdf
10
11
 
11
12
  EPOCH_OF_REFERENCE = Epoch::DEFAULT_EPOCH
12
13
  OBLIQUITY_OF_REFERENCE = 23.4392794
@@ -14,19 +15,31 @@ module Astronoby
14
15
  def self.for_epoch(epoch)
15
16
  return obliquity_of_reference if epoch == EPOCH_OF_REFERENCE
16
17
 
17
- t = (epoch - EPOCH_OF_REFERENCE) / Constants::DAYS_PER_JULIAN_CENTURY
18
+ t = Rational(
19
+ (epoch - EPOCH_OF_REFERENCE),
20
+ Constants::DAYS_PER_JULIAN_CENTURY
21
+ )
22
+
23
+ epsilon0 = obliquity_of_reference_in_milliarcseconds
24
+ c1 = -46.836769
25
+ c2 = -0.0001831
26
+ c3 = 0.00200340
27
+ c4 = -0.000000576
28
+ c5 = -0.0000000434
18
29
 
19
- Angle.from_degrees(
20
- obliquity_of_reference.degrees - (
21
- 46.815 * t -
22
- 0.0006 * t * t +
23
- 0.00181 * t * t * t
24
- ) / Constants::SECONDS_PER_DEGREE
30
+ Angle.from_dms(
31
+ 0,
32
+ 0,
33
+ epsilon0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * c5))))
25
34
  )
26
35
  end
27
36
 
28
37
  def self.obliquity_of_reference
29
- Angle.from_dms(23, 26, 21.45)
38
+ Angle.from_dms(0, 0, obliquity_of_reference_in_milliarcseconds)
39
+ end
40
+
41
+ def self.obliquity_of_reference_in_milliarcseconds
42
+ 84381.406
30
43
  end
31
44
  end
32
45
  end