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,14 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Holds arrays of twilight times over a time range.
4
5
  class TwilightEvents
5
- attr_reader :morning_civil_twilight_times,
6
- :evening_civil_twilight_times,
7
- :morning_nautical_twilight_times,
8
- :evening_nautical_twilight_times,
9
- :morning_astronomical_twilight_times,
10
- :evening_astronomical_twilight_times
6
+ # @return [Array<Time>] morning civil twilight times
7
+ attr_reader :morning_civil_twilight_times
11
8
 
9
+ # @return [Array<Time>] evening civil twilight times
10
+ attr_reader :evening_civil_twilight_times
11
+
12
+ # @return [Array<Time>] morning nautical twilight times
13
+ attr_reader :morning_nautical_twilight_times
14
+
15
+ # @return [Array<Time>] evening nautical twilight times
16
+ attr_reader :evening_nautical_twilight_times
17
+
18
+ # @return [Array<Time>] morning astronomical twilight times
19
+ attr_reader :morning_astronomical_twilight_times
20
+
21
+ # @return [Array<Time>] evening astronomical twilight times
22
+ attr_reader :evening_astronomical_twilight_times
23
+
24
+ # @param morning_civil [Array<Time>] morning civil twilight times
25
+ # @param evening_civil [Array<Time>] evening civil twilight times
26
+ # @param morning_nautical [Array<Time>] morning nautical twilight times
27
+ # @param evening_nautical [Array<Time>] evening nautical twilight times
28
+ # @param morning_astronomical [Array<Time>] morning astronomical twilight
29
+ # times
30
+ # @param evening_astronomical [Array<Time>] evening astronomical twilight
31
+ # times
12
32
  def initialize(
13
33
  morning_civil,
14
34
  evening_civil,
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class ExtremumFinder
5
+ PHI = (1 + Math.sqrt(5)) / 2
6
+ INVPHI = 1 / PHI
7
+ GOLDEN_SECTION_TOLERANCE = 1e-5
8
+
9
+ MIN_SAMPLES_PER_PERIOD = 20
10
+ DUPLICATE_THRESHOLD_DAYS = 0.5
11
+ BOUNDARY_BUFFER_DAYS = 0.01
12
+
13
+ # @param value_at [#call] callable mapping a Julian Date (Terrestrial Time)
14
+ # to a comparable value
15
+ # @param period [Float] the characteristic period of the quantity in days,
16
+ # used to scale the sampling density
17
+ # @param samples_per_period [Integer] number of samples per period
18
+ def initialize(value_at:, period:, samples_per_period: 60)
19
+ @value_at = value_at
20
+ @period = period
21
+ @samples_per_period = samples_per_period
22
+ end
23
+
24
+ # @param start_jd [Float] start time in Julian Date (Terrestrial Time)
25
+ # @param end_jd [Float] end time in Julian Date (Terrestrial Time)
26
+ # @param type [Symbol] +:maximum+ or +:minimum+
27
+ # @return [Array<Hash>] extrema as +{jd: Float, value: Comparable}+, sorted
28
+ # by time
29
+ def extrema(start_jd, end_jd, type: :maximum)
30
+ candidates = find_candidates(start_jd, end_jd, type)
31
+ refined = candidates.map { |candidate| refine(candidate, type) }.compact
32
+ refined = remove_duplicates(refined)
33
+ filter_boundary_artifacts(refined, start_jd, end_jd)
34
+ end
35
+
36
+ private
37
+
38
+ def find_candidates(start_jd, end_jd, type)
39
+ samples = collect_samples(start_jd, end_jd)
40
+ find_local_extrema(samples, type)
41
+ end
42
+
43
+ def collect_samples(start_jd, end_jd)
44
+ duration = end_jd - start_jd
45
+ sample_count = sample_count_for(duration)
46
+ step = duration / sample_count
47
+
48
+ (0..sample_count).map do |i|
49
+ jd = start_jd + (i * step)
50
+ {jd: jd, value: @value_at.call(jd)}
51
+ end
52
+ end
53
+
54
+ def sample_count_for(duration)
55
+ periods_in_range = duration / @period
56
+ base_samples = (periods_in_range * @samples_per_period).to_i
57
+ [base_samples, MIN_SAMPLES_PER_PERIOD].max
58
+ end
59
+
60
+ def find_local_extrema(samples, type)
61
+ candidates = []
62
+
63
+ (1...samples.length - 1).each do |i|
64
+ next unless local_extremum?(samples, i, type)
65
+
66
+ candidates << {
67
+ start_jd: samples[i - 1][:jd],
68
+ end_jd: samples[i + 1][:jd]
69
+ }
70
+ end
71
+
72
+ candidates
73
+ end
74
+
75
+ def local_extremum?(samples, index, type)
76
+ current = samples[index][:value]
77
+ previous = samples[index - 1][:value]
78
+ following = samples[index + 1][:value]
79
+
80
+ if type == :maximum
81
+ current > previous && current > following
82
+ else
83
+ current < previous && current < following
84
+ end
85
+ end
86
+
87
+ def refine(candidate, type)
88
+ golden_section_search(candidate[:start_jd], candidate[:end_jd], type)
89
+ end
90
+
91
+ def golden_section_search(a, b, type)
92
+ return nil if b <= a
93
+
94
+ tol = GOLDEN_SECTION_TOLERANCE * (b - a).abs
95
+
96
+ x1 = a + (1 - INVPHI) * (b - a)
97
+ x2 = a + INVPHI * (b - a)
98
+ f1 = @value_at.call(x1)
99
+ f2 = @value_at.call(x2)
100
+
101
+ while (b - a).abs > tol
102
+ keep_left = (type == :maximum) ? (f1 > f2) : (f1 < f2)
103
+
104
+ if keep_left
105
+ b = x2
106
+ x2 = x1
107
+ f2 = f1
108
+ x1 = a + (1 - INVPHI) * (b - a)
109
+ f1 = @value_at.call(x1)
110
+ else
111
+ a = x1
112
+ x1 = x2
113
+ f1 = f2
114
+ x2 = a + INVPHI * (b - a)
115
+ f2 = @value_at.call(x2)
116
+ end
117
+ end
118
+
119
+ mid = (a + b) / 2
120
+ {jd: mid, value: @value_at.call(mid)}
121
+ end
122
+
123
+ def remove_duplicates(extrema)
124
+ return extrema if extrema.length <= 1
125
+
126
+ cleaned = [extrema.first]
127
+
128
+ extrema.each_with_index do |current, i|
129
+ next if i == 0
130
+
131
+ is_duplicate = cleaned.any? do |existing|
132
+ (current[:jd] - existing[:jd]).abs < DUPLICATE_THRESHOLD_DAYS
133
+ end
134
+
135
+ cleaned << current unless is_duplicate
136
+ end
137
+
138
+ cleaned
139
+ end
140
+
141
+ def filter_boundary_artifacts(extrema, start_jd, end_jd)
142
+ extrema.reject do |extreme|
143
+ (extreme[:jd] - start_jd).abs < BOUNDARY_BUFFER_DAYS ||
144
+ (extreme[:jd] - end_jd).abs < BOUNDARY_BUFFER_DAYS
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ module Horizon
5
+ # @return [Astronoby::Angle] altitude of the centre of a point-like body at
6
+ # rise and set
7
+ REFRACTION_ANGLE = -Angle.from_dms(0, 34, 0)
8
+
9
+ # @return [Astronoby::Angle] altitude of the Sun's centre at rise and set
10
+ SUN_ANGLE = -Angle.from_dms(0, 50, 0)
11
+
12
+ class << self
13
+ # @param body [Class] the body rising or setting
14
+ # @param distance [Astronoby::Distance] distance to the body, used for
15
+ # the Moon's semidiameter
16
+ # @return [Astronoby::Angle] altitude of the body's centre at rise and set
17
+ def angle_for(body:, distance:)
18
+ if body == Sun
19
+ SUN_ANGLE
20
+ elsif body == Moon
21
+ REFRACTION_ANGLE - moon_semidiameter(distance)
22
+ else
23
+ REFRACTION_ANGLE
24
+ end
25
+ end
26
+
27
+ # @param distance [Astronoby::Distance] distance to the Moon
28
+ # @return [Astronoby::Angle] the Moon's semidiameter
29
+ def moon_semidiameter(distance)
30
+ Angle.from_radians(moon_semidiameter_radians(distance.m))
31
+ end
32
+
33
+ # @param distance_in_meters [Numeric] distance to the Moon in meters
34
+ # @return [Float] the Moon's semidiameter in radians
35
+ def moon_semidiameter_radians(distance_in_meters)
36
+ Moon::EQUATORIAL_RADIUS.m.to_f / distance_in_meters
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # Transforms equatorial coordinates at J2000.0 into horizontal coordinates
5
+ # for one observer at one instant.
6
+ #
7
+ # Everything that depends only on the observer and the instant is computed
8
+ # once at construction: the rotation from the ICRS to the true equator and
9
+ # equinox of date (nutation * precession, with the ICRS frame bias), and the
10
+ # local apparent sidereal time. Converting a point then costs a matrix
11
+ # product and a few trigonometric functions, instead of rebuilding the
12
+ # reference frame chain for each point. It is meant for converting many
13
+ # points for the same observer and instant, such as the stars of a catalogue
14
+ # for a sky chart.
15
+ #
16
+ # It plays the role of the "star-independent astrometry parameters" of SOFA
17
+ # and ERFA, restricted to what Astronoby applies without an ephemeris. The
18
+ # result is the same as
19
+ # `DeepSkyObject#at(instant).observed_by(observer).horizontal`, but the
20
+ # points are treated as fixed directions: no aberration, no proper motion and
21
+ # no parallax. The topocentric correction is not applied either, as it is
22
+ # nil for objects at stellar distances. Atmospheric refraction is optional.
23
+ #
24
+ # @example Convert the stars of a catalogue for one observer and instant
25
+ # transformation = Astronoby::HorizontalTransformation.new(
26
+ # observer: observer,
27
+ # instant: instant
28
+ # )
29
+ # horizontals = catalogue.map { |star| transformation.apply(star) }
30
+ class HorizontalTransformation
31
+ # @return [Astronoby::Observer] the observer
32
+ attr_reader :observer
33
+
34
+ # @return [Astronoby::Instant] the instant
35
+ attr_reader :instant
36
+
37
+ # @param observer [Astronoby::Observer] the observer
38
+ # @param instant [Astronoby::Instant] the instant of the observation
39
+ def initialize(observer:, instant:)
40
+ @observer = observer
41
+ @instant = instant
42
+ @rotation_rows = (
43
+ Nutation.matrix_for(instant) * Precession.matrix_for(instant)
44
+ ).to_a.freeze
45
+ @local_apparent_sidereal_time = LocalApparentSiderealTime.from_utc(
46
+ instant.to_time,
47
+ longitude: observer.longitude
48
+ ).time
49
+ freeze
50
+ end
51
+
52
+ # @param equatorial_coordinates [Astronoby::Coordinates::Equatorial]
53
+ # equatorial coordinates at epoch J2000.0
54
+ # @param refraction [Boolean] whether to apply atmospheric refraction
55
+ # correction (default: false)
56
+ # @return [Astronoby::Coordinates::Horizontal] horizontal coordinates
57
+ # @raise [Astronoby::IncompatibleArgumentsError] if the coordinates are not
58
+ # referred to epoch J2000.0
59
+ def apply(equatorial_coordinates, refraction: false)
60
+ ensure_j2000!(equatorial_coordinates)
61
+
62
+ horizontal = apparent_equatorial(equatorial_coordinates).to_horizontal(
63
+ time: @instant.to_time,
64
+ observer: @observer
65
+ )
66
+
67
+ if refraction
68
+ Refraction.correct_horizontal_coordinates(coordinates: horizontal)
69
+ else
70
+ horizontal
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ # Equatorial coordinates of date, with the hour angle already known so that
77
+ # the sidereal time is not computed again.
78
+ def apparent_equatorial(coordinates)
79
+ declination = coordinates.declination
80
+ right_ascension = coordinates.right_ascension
81
+ cos_declination = declination.cos
82
+ x = cos_declination * right_ascension.cos
83
+ y = cos_declination * right_ascension.sin
84
+ z = declination.sin
85
+
86
+ row_x, row_y, row_z = @rotation_rows
87
+ apparent_x = row_x[0] * x + row_x[1] * y + row_x[2] * z
88
+ apparent_y = row_y[0] * x + row_y[1] * y + row_y[2] * z
89
+ apparent_z = row_z[0] * x + row_z[1] * y + row_z[2] * z
90
+
91
+ apparent_right_ascension =
92
+ Angle.from_radians(Math.atan2(apparent_y, apparent_x))
93
+
94
+ Coordinates::Equatorial.new(
95
+ right_ascension: apparent_right_ascension,
96
+ declination: Angle.asin(apparent_z.clamp(-1.0, 1.0)),
97
+ hour_angle:
98
+ Angle.from_hours(@local_apparent_sidereal_time) -
99
+ apparent_right_ascension,
100
+ epoch: @instant.tt
101
+ )
102
+ end
103
+
104
+ def ensure_j2000!(coordinates)
105
+ return if coordinates.epoch == JulianDate::J2000
106
+
107
+ raise IncompatibleArgumentsError,
108
+ "Expected equatorial coordinates at epoch J2000.0, got epoch " \
109
+ "#{coordinates.epoch}"
110
+ end
111
+ end
112
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "date"
4
+ require "horologium"
4
5
 
5
6
  module Astronoby
6
7
  # Represents a specific instant in time using Terrestrial Time (TT) as its
@@ -8,9 +9,15 @@ module Astronoby
8
9
  # time scales commonly used in astronomy:
9
10
  # - Terrestrial Time (TT)
10
11
  # - International Atomic Time (TAI)
12
+ # - Barycentric Dynamical Time (TDB)
11
13
  # - Universal Time Coordinated (UTC)
12
14
  # - Greenwich Mean Sidereal Time (GMST)
13
15
  #
16
+ # TAI and TDB are delegated to the horologium gem, which owns the scales
17
+ # that convert by definition or by model. UT1 is not: it rests on
18
+ # observations of the rotation of the Earth, and comes from
19
+ # {Astronoby::DeltaT}.
20
+ #
14
21
  # @example Create an instant from the current time
15
22
  # instant = Astronoby::Instant.from_time(Time.now)
16
23
  # instant.tai # Get International Atomic Time
@@ -28,6 +35,16 @@ module Astronoby
28
35
  # midnight. This constant adds 0.5 days (12 hours) to make the conversion.
29
36
  DATETIME_JD_EPOCH_ADJUSTMENT = 0.5
30
37
 
38
+ # Astronoby pins the precision it asks horologium for rather than reading
39
+ # horologium's global default, so a host application configuring
40
+ # horologium for itself cannot change what Astronoby computes.
41
+ #
42
+ # Exact rather than standard: standard would round the Julian Date into a
43
+ # two-part float on the way in, and TAI is a fixed offset from TT, so
44
+ # there is nothing to round it for. It is also the cheaper of the two
45
+ # here, having no conversion to make in either direction.
46
+ PRECISION = :exact
47
+
31
48
  class << self
32
49
  # Creates a new Instant from a Terrestrial Time value
33
50
  #
@@ -44,7 +61,7 @@ module Astronoby
44
61
  # @param time [Time] a Time object to convert
45
62
  # @return [Astronoby::Instant] a new Instant object
46
63
  def from_time(time)
47
- delta_t = Util::Time.terrestrial_universal_time_delta(time)
64
+ delta_t = DeltaT.at(time)
48
65
  terrestrial_time = time.utc.to_datetime.ajd +
49
66
  Rational(delta_t, Constants::SECONDS_PER_DAY)
50
67
  from_terrestrial_time(terrestrial_time)
@@ -55,13 +72,14 @@ module Astronoby
55
72
  # @param julian_date [Numeric] the Julian Date in UTC
56
73
  # @return [Astronoby::Instant] a new Instant object
57
74
  def from_utc_julian_date(julian_date)
58
- delta_t = Util::Time.terrestrial_universal_time_delta(julian_date)
75
+ delta_t = DeltaT.at(julian_date)
59
76
  terrestrial_time = julian_date +
60
77
  Rational(delta_t, Constants::SECONDS_PER_DAY)
61
78
  from_terrestrial_time(terrestrial_time)
62
79
  end
63
80
  end
64
81
 
82
+ # @return [Numeric] the Terrestrial Time as a Julian Date
65
83
  attr_reader :terrestrial_time
66
84
  alias_method :tt, :terrestrial_time
67
85
  alias_method :julian_date, :terrestrial_time
@@ -77,6 +95,7 @@ module Astronoby
77
95
  end
78
96
 
79
97
  @terrestrial_time = terrestrial_time
98
+ @memo = {}
80
99
  freeze
81
100
  end
82
101
 
@@ -92,7 +111,7 @@ module Astronoby
92
111
  #
93
112
  # @return [DateTime] the UTC time as DateTime
94
113
  def to_datetime
95
- DateTime.jd(
114
+ @memo[:to_datetime] ||= DateTime.jd(
96
115
  @terrestrial_time -
97
116
  Rational(delta_t, Constants::SECONDS_PER_DAY) +
98
117
  DATETIME_JD_EPOCH_ADJUSTMENT
@@ -110,7 +129,7 @@ module Astronoby
110
129
  #
111
130
  # @return [Time] the UTC time
112
131
  def to_time
113
- to_datetime.to_time.utc
132
+ @memo[:to_time] ||= to_datetime.to_time.utc
114
133
  end
115
134
 
116
135
  # Get the ΔT (Delta T) value for this instant
@@ -118,21 +137,21 @@ module Astronoby
118
137
  #
119
138
  # @return [Numeric] Delta T in seconds
120
139
  def delta_t
121
- Util::Time.terrestrial_universal_time_delta(@terrestrial_time)
140
+ @memo[:delta_t] ||= DeltaT.at(@terrestrial_time)
122
141
  end
123
142
 
124
143
  # Get the Greenwich Mean Sidereal Time
125
144
  #
126
145
  # @return [Numeric] the sidereal time in hours
127
146
  def gmst
128
- GreenwichMeanSiderealTime.from_utc(to_time).time
147
+ @memo[:gmst] ||= GreenwichMeanSiderealTime.from_utc(to_time).time
129
148
  end
130
149
 
131
150
  # Get the Greenwich Apparent Sidereal Time
132
151
  #
133
152
  # @return [Numeric] the sidereal time in hours
134
153
  def gast
135
- GreenwichApparentSiderealTime.from_utc(to_time).time
154
+ @memo[:gast] ||= GreenwichApparentSiderealTime.from_utc(to_time).time
136
155
  end
137
156
 
138
157
  # Get the Local Mean Sidereal Time
@@ -153,21 +172,26 @@ module Astronoby
153
172
 
154
173
  # Get the International Atomic Time (TAI)
155
174
  #
156
- # @return [Numeric] TAI as Julian Date
175
+ # Read as a Rational rather than a Float. A Julian Date is around 2.46
176
+ # million, so a single Float rounds to about 47 microseconds of the day,
177
+ # which would coarsen an instant that TT carries exactly.
178
+ #
179
+ # @return [Rational] TAI as Julian Date
157
180
  def tai
158
- @terrestrial_time -
159
- Rational(Constants::TAI_TT_OFFSET, Constants::SECONDS_PER_DAY)
181
+ @memo[:tai] ||= scale_free_instant
182
+ .as(:julian_date, scale: :tai, as: :rational)
160
183
  end
161
184
 
162
185
  # Get the Barycentric Dynamical Time (TDB)
163
- # Note: Currently approximated as equal to TT
164
186
  #
165
- # @return [Numeric] TDB as Julian Date
187
+ # TDB - TT comes from the Fairhead and Bretagnon series ERFA uses. It
188
+ # peaks around 1.7 milliseconds, so a Float Julian Date would quantise
189
+ # most of the correction away; see {#tai} on the shape.
190
+ #
191
+ # @return [Rational] TDB as Julian Date
166
192
  def tdb
167
- # This is technically false, there is a slight difference between TT and
168
- # TDB. However, this difference is so small that currenly Astronoby
169
- # doesn't support it and consider they are the same value.
170
- @terrestrial_time
193
+ @memo[:tdb] ||= scale_free_instant
194
+ .as(:julian_date, scale: :tdb, as: :rational)
171
195
  end
172
196
 
173
197
  # Get the offset between TT and UTC for this instant
@@ -195,5 +219,21 @@ module Astronoby
195
219
  @terrestrial_time <=> other.terrestrial_time
196
220
  end
197
221
  alias_method :eql?, :==
222
+
223
+ private
224
+
225
+ # The instant as horologium holds it: a point on the timeline with no
226
+ # scale of its own. Building it costs more than reading a Julian Date back
227
+ # out of it, so it is built on the first scale conversion that needs it
228
+ # and never on the paths that only want TT.
229
+ #
230
+ # @return [Horologium::Instant]
231
+ def scale_free_instant
232
+ @memo[:scale_free_instant] ||= Horologium::Instant.from_julian_date(
233
+ @terrestrial_time,
234
+ scale: :tt,
235
+ precision: PRECISION
236
+ )
237
+ end
198
238
  end
199
239
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # Geocentric libration of the Moon: the small oscillations that let an
5
+ # observer on Earth see slightly more than half of the lunar surface over
6
+ # time. Holds the total (optical + physical) libration in longitude and
7
+ # latitude.
8
+ class Libration
9
+ # @return [Astronoby::Angle] libration in longitude (l), positive towards
10
+ # the Moon's Mare Crisium (east limb)
11
+ attr_reader :longitude
12
+
13
+ # @return [Astronoby::Angle] libration in latitude (b), positive towards
14
+ # the Moon's north pole
15
+ attr_reader :latitude
16
+
17
+ # @param longitude [Astronoby::Angle] libration in longitude
18
+ # @param latitude [Astronoby::Angle] libration in latitude
19
+ def initialize(longitude:, latitude:)
20
+ @longitude = longitude
21
+ @latitude = latitude
22
+ freeze
23
+ end
24
+ end
25
+ end
@@ -4,6 +4,7 @@
4
4
  # as these coefficients work with TT (Terrestrial Time).
5
5
 
6
6
  module Astronoby
7
+ # Computes the mean obliquity of the ecliptic using the IAU 2006 P03 model.
7
8
  class MeanObliquity
8
9
  # Source:
9
10
  # IAU resolution in 2006 in favor of the P03 astronomical model
@@ -12,6 +13,10 @@ module Astronoby
12
13
  EPOCH_OF_REFERENCE = JulianDate::DEFAULT_EPOCH
13
14
  OBLIQUITY_OF_REFERENCE = 23.4392794
14
15
 
16
+ # Computes the mean obliquity of the ecliptic at the given instant.
17
+ #
18
+ # @param instant [Astronoby::Instant] the time instant
19
+ # @return [Astronoby::Angle] the mean obliquity
15
20
  def self.at(instant)
16
21
  return obliquity_of_reference if instant.julian_date == EPOCH_OF_REFERENCE
17
22
 
@@ -32,10 +37,13 @@ module Astronoby
32
37
  )
33
38
  end
34
39
 
40
+ # @return [Astronoby::Angle] the mean obliquity at the reference epoch
41
+ # (J2000.0)
35
42
  def self.obliquity_of_reference
36
43
  Angle.from_degree_arcseconds(obliquity_of_reference_in_arcseconds)
37
44
  end
38
45
 
46
+ # @return [Float] the mean obliquity at J2000.0 in arcseconds
39
47
  def self.obliquity_of_reference_in_arcseconds
40
48
  84381.406
41
49
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ # Computes the Moon's libration and the position angle of its axis from a
5
+ # binary PCK lunar orientation kernel, as the arcsecond-accurate counterpart
6
+ # to the analytic Meeus series in MoonPhysicalEphemeris.
7
+ #
8
+ # The libration is the selenographic longitude and latitude of the sub-Earth
9
+ # point, expressed in the Moon's mean-Earth body-fixed frame.
10
+ class MoonOrientationEphemeris
11
+ # @param moon [Astronoby::Moon] the Moon, carrying an orientation kernel
12
+ def initialize(moon)
13
+ @moon = moon
14
+ @instant = moon.instant
15
+ @orientation = moon.orientation
16
+ end
17
+
18
+ # @return [Astronoby::Libration] the libration in longitude and latitude
19
+ def libration
20
+ sub_earth = rotation * moon_to_earth
21
+ Libration.new(
22
+ longitude: Angle.from_radians(Math.atan2(sub_earth[1], sub_earth[0])),
23
+ latitude: Angle.from_radians(
24
+ Math.asin(sub_earth[2] / sub_earth.magnitude)
25
+ )
26
+ )
27
+ end
28
+
29
+ # Position angle of the Moon's axis of rotation, measured eastward from the
30
+ # north point of the disk.
31
+ # @return [Astronoby::Angle] the position angle of the axis
32
+ def position_angle_of_axis
33
+ @moon.apparent.equatorial.position_angle_to(north_pole)
34
+ end
35
+
36
+ private
37
+
38
+ def rotation
39
+ @rotation ||= @orientation.rotation_for(retarded_instant)
40
+ end
41
+
42
+ def retarded_instant
43
+ Instant.from_terrestrial_time(
44
+ @instant.terrestrial_time - light_time_in_days
45
+ )
46
+ end
47
+
48
+ def light_time_in_days
49
+ @moon.astrometric.distance.km /
50
+ Velocity.light_speed.kmps /
51
+ Constants::SECONDS_PER_DAY
52
+ end
53
+
54
+ def moon_to_earth
55
+ x, y, z = @moon.astrometric.position.map(&:m).to_a
56
+ ::Vector[-x, -y, -z]
57
+ end
58
+
59
+ def north_pole
60
+ @north_pole ||= Coordinates::Equatorial.from_position_vector(
61
+ Distance.vector_from_meters(
62
+ Nutation.matrix_for(@instant) *
63
+ Precession.matrix_for(@instant) *
64
+ (rotation.transpose * ::Vector[0, 0, 1])
65
+ )
66
+ )
67
+ end
68
+ end
69
+ end