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,7 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Topocentric reference frame. Represents a body's position as seen from a
5
+ # specific observer on Earth's surface, accounting for the observer's
6
+ # geocentric position and Earth rotation.
4
7
  class Topocentric < ReferenceFrame
8
+ # Builds a topocentric frame from an apparent frame and observer.
9
+ #
10
+ # @param apparent [Astronoby::Apparent] the apparent frame
11
+ # @param observer [Astronoby::Observer] the observer
12
+ # @param instant [Astronoby::Instant] the time instant
13
+ # @param target_body [Astronoby::Body, nil] the target body
14
+ # @return [Astronoby::Topocentric] a new topocentric frame
5
15
  def self.build_from_apparent(
6
16
  apparent:,
7
17
  observer:,
@@ -24,17 +34,23 @@ module Astronoby
24
34
  position: position,
25
35
  velocity: velocity,
26
36
  instant: instant,
27
- center_identifier: [observer.longitude, observer.latitude],
37
+ center: Center.topocentric(observer),
28
38
  target_body: target_body,
29
39
  observer: observer
30
40
  )
31
41
  end
32
42
 
43
+ # @param position [Astronoby::Vector<Astronoby::Distance>] position vector
44
+ # @param velocity [Astronoby::Vector<Astronoby::Velocity>] velocity vector
45
+ # @param instant [Astronoby::Instant] the time instant
46
+ # @param center [Astronoby::Center] the center of the frame
47
+ # @param target_body [Astronoby::Body, nil] the target body
48
+ # @param observer [Astronoby::Observer] the observer
33
49
  def initialize(
34
50
  position:,
35
51
  velocity:,
36
52
  instant:,
37
- center_identifier:,
53
+ center:,
38
54
  target_body:,
39
55
  observer:
40
56
  )
@@ -42,20 +58,36 @@ module Astronoby
42
58
  position: position,
43
59
  velocity: velocity,
44
60
  instant: instant,
45
- center_identifier: center_identifier,
61
+ center: center,
46
62
  target_body: target_body
47
63
  )
48
64
  @observer = observer
49
65
  end
50
66
 
67
+ # @return [Numeric] Julian Date epoch of the frame's coordinates, which
68
+ # are referred to the true equator and equinox of date
69
+ def epoch
70
+ instant.tt
71
+ end
72
+
73
+ # @return [Astronoby::Coordinates::Ecliptic] ecliptic coordinates at the
74
+ # current instant (true ecliptic and equinox of date)
51
75
  def ecliptic
52
76
  @ecliptic ||= begin
53
77
  return Coordinates::Ecliptic.zero if distance.zero?
54
78
 
55
- equatorial.to_ecliptic(instant: @instant)
79
+ equatorial.to_ecliptic(
80
+ instant: @instant,
81
+ obliquity: TrueObliquity.at(@instant)
82
+ )
56
83
  end
57
84
  end
58
85
 
86
+ # Converts to horizontal coordinates (azimuth/altitude).
87
+ #
88
+ # @param refraction [Boolean] whether to apply atmospheric refraction
89
+ # correction (default: false)
90
+ # @return [Astronoby::Coordinates::Horizontal] horizontal coordinates
59
91
  def horizontal(refraction: false)
60
92
  horizontal = equatorial.to_horizontal(
61
93
  time: @instant.to_time,
@@ -1,27 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Computes atmospheric refraction corrections for horizontal coordinates.
5
+ #
6
+ # Source:
7
+ # Title: Practical Astronomy with your Calculator or Spreadsheet
8
+ # Authors: Peter Duffett-Smith and Jonathan Zwart
9
+ # Edition: Cambridge University Press
10
+ # Chapter: 37 - Refraction
4
11
  class Refraction
5
12
  LOW_ALTITUDE_BODY_ANGLE = Angle.from_degrees(15)
6
13
  ZENITH = Angle.from_degrees(90)
7
14
 
15
+ # Computes the refraction angle for the given horizontal coordinates.
16
+ #
17
+ # @param coordinates [Astronoby::Coordinates::Horizontal] horizontal
18
+ # coordinates
19
+ # @return [Astronoby::Angle] the refraction angle
8
20
  def self.angle(coordinates:)
9
21
  new(coordinates).refraction_angle
10
22
  end
11
23
 
24
+ # Returns horizontal coordinates corrected for atmospheric refraction.
25
+ #
26
+ # @param coordinates [Astronoby::Coordinates::Horizontal] horizontal
27
+ # coordinates
28
+ # @return [Astronoby::Coordinates::Horizontal] corrected coordinates
12
29
  def self.correct_horizontal_coordinates(coordinates:)
13
30
  new(coordinates).refract
14
31
  end
15
32
 
33
+ # @param coordinates [Astronoby::Coordinates::Horizontal] horizontal
34
+ # coordinates
16
35
  def initialize(coordinates)
17
36
  @coordinates = coordinates
18
37
  end
19
38
 
20
- # Source:
21
- # Title: Practical Astronomy with your Calculator or Spreadsheet
22
- # Authors: Peter Duffett-Smith and Jonathan Zwart
23
- # Edition: Cambridge University Press
24
- # Chapter: 37 - Refraction
39
+ # Returns horizontal coordinates with refraction applied to the altitude.
40
+ #
41
+ # @return [Astronoby::Coordinates::Horizontal] corrected coordinates
25
42
  def refract
26
43
  Coordinates::Horizontal.new(
27
44
  azimuth: @coordinates.azimuth,
@@ -30,6 +47,10 @@ module Astronoby
30
47
  )
31
48
  end
32
49
 
50
+ # Computes the refraction angle based on the observer's atmospheric
51
+ # conditions and the body's altitude.
52
+ #
53
+ # @return [Astronoby::Angle] the refraction angle
33
54
  def refraction_angle
34
55
  if @coordinates.altitude > LOW_ALTITUDE_BODY_ANGLE
35
56
  high_altitude_angle
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Astronoby
4
+ class RootFinder
5
+ MIN_SAMPLES_PER_PERIOD = 20
6
+ BISECTION_TOLERANCE_DAYS = 1e-8
7
+
8
+ # @param value_at [#call] callable mapping a Julian Date (Terrestrial Time)
9
+ # to a Float
10
+ # @param period [Float] the characteristic period of the quantity in days
11
+ # @param samples_per_period [Integer] number of samples per period
12
+ def initialize(value_at:, period:, samples_per_period: 60)
13
+ @value_at = value_at
14
+ @period = period
15
+ @samples_per_period = samples_per_period
16
+ end
17
+
18
+ # @param start_jd [Float] start time in Julian Date (Terrestrial Time)
19
+ # @param end_jd [Float] end time in Julian Date (Terrestrial Time)
20
+ # @param accept [#call, nil] optional predicate on a located root (a Julian
21
+ # Date)
22
+ # @return [Array<Float>] root times in Julian Date (Terrestrial Time),
23
+ # sorted by time
24
+ def roots(start_jd, end_jd, accept: nil)
25
+ samples = collect_samples(start_jd, end_jd)
26
+ located = (
27
+ samples_on_root(samples) +
28
+ find_brackets(samples).map { |a, b, f_a| bisect(a, b, f_a) }
29
+ ).uniq.sort
30
+ located.select { |jd| accept.nil? || accept.call(jd) }
31
+ end
32
+
33
+ private
34
+
35
+ def samples_on_root(samples)
36
+ samples.filter_map { |sample| sample[:jd] if sample[:value].zero? }
37
+ end
38
+
39
+ def find_brackets(samples)
40
+ (0...samples.length - 1).filter_map do |i|
41
+ current = samples[i]
42
+ following = samples[i + 1]
43
+
44
+ if sign_change?(current, following)
45
+ [current[:jd], following[:jd], current[:value]]
46
+ end
47
+ end
48
+ end
49
+
50
+ def sign_change?(current, following)
51
+ current[:value] * following[:value] < 0
52
+ end
53
+
54
+ def collect_samples(start_jd, end_jd)
55
+ duration = end_jd - start_jd
56
+ sample_count = sample_count_for(duration)
57
+ step = duration / sample_count
58
+
59
+ (0..sample_count).map do |i|
60
+ jd = start_jd + (i * step)
61
+ {jd: jd, value: @value_at.call(jd)}
62
+ end
63
+ end
64
+
65
+ def sample_count_for(duration)
66
+ periods_in_range = duration / @period
67
+ base_samples = (periods_in_range * @samples_per_period).to_i
68
+ [base_samples, MIN_SAMPLES_PER_PERIOD].max
69
+ end
70
+
71
+ def bisect(a, b, f_a)
72
+ while (b - a).abs > BISECTION_TOLERANCE_DAYS
73
+ midpoint = (a + b) / 2
74
+ break if midpoint == a || midpoint == b
75
+
76
+ f_midpoint = @value_at.call(midpoint)
77
+ return midpoint if f_midpoint.zero?
78
+
79
+ if f_a.negative? == f_midpoint.negative?
80
+ a = midpoint
81
+ f_a = f_midpoint
82
+ else
83
+ b = midpoint
84
+ end
85
+ end
86
+
87
+ (a + b) / 2
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "matrix"
4
+
5
+ module Astronoby
6
+ # Builds elementary rotation matrices about the coordinate axes from an
7
+ # +Astronoby::Angle+.
8
+ #
9
+ # These use the passive (frame) convention: the matrix expresses a fixed
10
+ # vector in a frame rotated by +angle+ about the axis. This is the convention
11
+ # used throughout the reference-frame chain (precession, nutation, body
12
+ # orientation).
13
+ module Rotation
14
+ module_function
15
+
16
+ # @param angle [Astronoby::Angle] the rotation angle
17
+ # @return [Matrix] rotation about the x-axis
18
+ def about_x(angle)
19
+ cosine, sine = angle.cos, angle.sin
20
+ Matrix[
21
+ [1, 0, 0],
22
+ [0, cosine, sine],
23
+ [0, -sine, cosine]
24
+ ]
25
+ end
26
+
27
+ # @param angle [Astronoby::Angle] the rotation angle
28
+ # @return [Matrix] rotation about the y-axis
29
+ def about_y(angle)
30
+ cosine, sine = angle.cos, angle.sin
31
+ Matrix[
32
+ [cosine, 0, -sine],
33
+ [0, 1, 0],
34
+ [sine, 0, cosine]
35
+ ]
36
+ end
37
+
38
+ # @param angle [Astronoby::Angle] the rotation angle
39
+ # @return [Matrix] rotation about the z-axis
40
+ def about_z(angle)
41
+ cosine, sine = angle.cos, angle.sin
42
+ Matrix[
43
+ [cosine, sine, 0],
44
+ [-sine, cosine, 0],
45
+ [0, 0, 1]
46
+ ]
47
+ end
48
+ end
49
+ end
@@ -1,20 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Greenwich Apparent Sidereal Time (GAST). Derived from GMST by applying
5
+ # the equation of the equinoxes (nutation correction).
4
6
  class GreenwichApparentSiderealTime < GreenwichSiderealTime
7
+ # Creates GAST from UTC by computing GMST and applying the equation
8
+ # of the equinoxes.
9
+ #
10
+ # @param utc [Time] the UTC time
11
+ # @return [Astronoby::GreenwichApparentSiderealTime]
5
12
  def self.from_utc(utc)
6
- gmst = GreenwichMeanSiderealTime.from_utc(utc)
7
- instant = Instant.from_time(utc)
8
- nutation = Nutation.new(instant: instant)
9
- mean_obliquity = MeanObliquity.at(instant)
13
+ Astronoby.cache.fetch(CacheKey.generate_for_utc(:gast, utc)) do
14
+ gmst = GreenwichMeanSiderealTime.from_utc(utc)
15
+ instant = Instant.from_time(utc)
16
+ nutation = Nutation.new(instant: instant)
17
+ mean_obliquity = MeanObliquity.at(instant)
10
18
 
11
- equation_of_equinoxes = nutation.nutation_in_longitude.hours *
12
- mean_obliquity.cos
13
- gast_time = normalize_time(gmst.time + equation_of_equinoxes)
19
+ equation_of_equinoxes = nutation.nutation_in_longitude.hours *
20
+ mean_obliquity.cos
21
+ gast_time = normalize_time(gmst.time + equation_of_equinoxes)
14
22
 
15
- new(date: gmst.date, time: gast_time)
23
+ new(date: gmst.date, time: gast_time)
24
+ end
16
25
  end
17
26
 
27
+ # @param date [Date] the calendar date
28
+ # @param time [Numeric] GAST in hours
18
29
  def initialize(date:, time:)
19
30
  super(date: date, time: time, type: APPARENT)
20
31
  end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "iers"
4
+
3
5
  module Astronoby
6
+ # Greenwich Mean Sidereal Time (GMST). Computed from UTC using the IERS
7
+ # Conventions 2010 ERA-based expression, with a polynomial fallback for
8
+ # dates outside the IERS EOP data range.
4
9
  class GreenwichMeanSiderealTime < GreenwichSiderealTime
5
10
  JULIAN_CENTURIES_EXPONENTS = [
6
11
  6.697374558,
@@ -10,13 +15,43 @@ module Astronoby
10
15
 
11
16
  SIDEREAL_MINUTE_IN_UT_MINUTE = 0.9972695663
12
17
 
18
+ UT_TO_SIDEREAL_RATIO = 1.002737909
19
+
20
+ # Creates GMST from UTC using the IERS Conventions 2010 ERA-based
21
+ # expression.
22
+ #
23
+ # Source:
24
+ # Title: IERS Conventions (2010)
25
+ # Chapter: 5.5.7 - ERA based expressions for Greenwich Sidereal Time
26
+ #
27
+ # @param utc [Time] the UTC time
28
+ # @return [Astronoby::GreenwichMeanSiderealTime]
29
+ def self.from_utc(utc)
30
+ Astronoby.cache.fetch(CacheKey.generate_for_utc(:gmst, utc)) do
31
+ date = utc.to_date
32
+ gmst_hours = begin
33
+ gmst_radians = IERS::GMST.at(utc)
34
+ gmst_radians * 12.0 / Math::PI
35
+ rescue IERS::OutOfRangeError
36
+ from_utc_polynomial(utc)
37
+ end
38
+ gmst_hours = normalize_time(gmst_hours)
39
+
40
+ new(date: date, time: gmst_hours)
41
+ end
42
+ end
43
+
44
+ # Polynomial fallback for dates outside the IERS EOP data range.
45
+ #
13
46
  # Source:
14
47
  # Title: Practical Astronomy with your Calculator or Spreadsheet
15
48
  # Authors: Peter Duffett-Smith and Jonathan Zwart
16
49
  # Edition: Cambridge University Press
17
50
  # Chapter: 12 - Conversion of UT to Greenwich sidereal time (GST)
18
- def self.from_utc(utc)
19
- date = utc.to_date
51
+ #
52
+ # @param utc [Time] the UTC time
53
+ # @return [Numeric] GMST in hours
54
+ def self.from_utc_polynomial(utc)
20
55
  julian_day = utc.to_date.ajd
21
56
  t = (julian_day - JulianDate::J2000) / Constants::DAYS_PER_JULIAN_CENTURY
22
57
  t0 = (
@@ -29,20 +64,24 @@ module Astronoby
29
64
  utc.min / Constants::MINUTES_PER_HOUR +
30
65
  (utc.sec + utc.subsec) / Constants::SECONDS_PER_HOUR
31
66
 
32
- gmst = normalize_time(1.002737909 * ut_in_hours + t0)
33
-
34
- new(date: date, time: gmst)
67
+ UT_TO_SIDEREAL_RATIO * ut_in_hours + t0
35
68
  end
36
69
 
70
+ # @param date [Date] the calendar date
71
+ # @param time [Numeric] GMST in hours
37
72
  def initialize(date:, time:)
38
73
  super(date: date, time: time, type: MEAN)
39
74
  end
40
75
 
76
+ # Converts GMST back to UTC.
77
+ #
41
78
  # Source:
42
79
  # Title: Practical Astronomy with your Calculator or Spreadsheet
43
80
  # Authors: Peter Duffett-Smith and Jonathan Zwart
44
81
  # Edition: Cambridge University Press
45
82
  # Chapter: 13 - Conversion of GST to UT
83
+ #
84
+ # @return [Time] the UTC time
46
85
  def to_utc
47
86
  date = @date
48
87
  julian_day = @date.ajd
@@ -1,7 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Greenwich Sidereal Time base class. Dispatches to mean or apparent
5
+ # subclasses.
4
6
  class GreenwichSiderealTime < SiderealTime
7
+ # Creates a Greenwich Sidereal Time from UTC.
8
+ #
9
+ # @param utc [Time] the UTC time
10
+ # @param type [Symbol] :mean or :apparent
11
+ # @return [Astronoby::GreenwichMeanSiderealTime,
12
+ # Astronoby::GreenwichApparentSiderealTime]
5
13
  def self.from_utc(utc, type: MEAN)
6
14
  validate_type!(type)
7
15
  case type
@@ -12,14 +20,22 @@ module Astronoby
12
20
  end
13
21
  end
14
22
 
23
+ # @param utc [Time] the UTC time
24
+ # @return [Astronoby::GreenwichMeanSiderealTime]
15
25
  def self.mean_from_utc(utc)
16
26
  GreenwichMeanSiderealTime.from_utc(utc)
17
27
  end
18
28
 
29
+ # @param utc [Time] the UTC time
30
+ # @return [Astronoby::GreenwichApparentSiderealTime]
19
31
  def self.apparent_from_utc(utc)
20
32
  GreenwichApparentSiderealTime.from_utc(utc)
21
33
  end
22
34
 
35
+ # Converts to UTC.
36
+ #
37
+ # @return [Time] the UTC time
38
+ # @raise [NotImplementedError] if this is apparent sidereal time
23
39
  def to_utc
24
40
  unless mean?
25
41
  raise NotImplementedError,
@@ -30,6 +46,11 @@ module Astronoby
30
46
  gmst.to_utc
31
47
  end
32
48
 
49
+ # Converts to Local Sidereal Time for a given longitude.
50
+ #
51
+ # @param longitude [Astronoby::Angle] the observer's longitude
52
+ # @return [Astronoby::LocalMeanSiderealTime,
53
+ # Astronoby::LocalApparentSiderealTime]
33
54
  def to_lst(longitude:)
34
55
  LocalSiderealTime.from_gst(gst: self, longitude: longitude)
35
56
  end
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Local Apparent Sidereal Time (LAST). Derived from GAST by adding the
5
+ # observer's longitude.
4
6
  class LocalApparentSiderealTime < LocalSiderealTime
7
+ # Creates LAST from a Greenwich Apparent Sidereal Time and longitude.
8
+ #
5
9
  # Source:
6
10
  # Title: Practical Astronomy with your Calculator or Spreadsheet
7
11
  # Authors: Peter Duffett-Smith and Jonathan Zwart
8
12
  # Edition: Cambridge University Press
9
13
  # Chapter: 14 - Local sidereal time (LST)
14
+ #
15
+ # @param gst [Astronoby::GreenwichApparentSiderealTime] GAST
16
+ # @param longitude [Astronoby::Angle] the observer's longitude
17
+ # @return [Astronoby::LocalApparentSiderealTime]
18
+ # @raise [ArgumentError] if gst is not apparent sidereal time
10
19
  def self.from_gst(gst:, longitude:)
11
20
  unless gst.apparent?
12
21
  raise ArgumentError, "GST must be apparent sidereal time"
@@ -18,20 +27,32 @@ module Astronoby
18
27
  new(date: date, time: time, longitude: longitude)
19
28
  end
20
29
 
30
+ # Creates LAST from UTC and longitude.
31
+ #
32
+ # @param utc [Time] the UTC time
33
+ # @param longitude [Astronoby::Angle] the observer's longitude
34
+ # @return [Astronoby::LocalApparentSiderealTime]
21
35
  def self.from_utc(utc, longitude:)
22
36
  gast = GreenwichApparentSiderealTime.from_utc(utc)
23
37
  from_gst(gst: gast, longitude: longitude)
24
38
  end
25
39
 
40
+ # @param date [Date] the calendar date
41
+ # @param time [Numeric] LAST in hours
42
+ # @param longitude [Astronoby::Angle] the observer's longitude
26
43
  def initialize(date:, time:, longitude:)
27
44
  super(date: date, time: time, longitude: longitude, type: APPARENT)
28
45
  end
29
46
 
47
+ # Converts to Greenwich Apparent Sidereal Time.
48
+ #
30
49
  # Source:
31
50
  # Title: Practical Astronomy with your Calculator or Spreadsheet
32
51
  # Authors: Peter Duffett-Smith and Jonathan Zwart
33
52
  # Edition: Cambridge University Press
34
53
  # Chapter: 15 - Converting LST to GST
54
+ #
55
+ # @return [Astronoby::GreenwichApparentSiderealTime]
35
56
  def to_gst
36
57
  date = @date
37
58
  time = normalize_time(@time - @longitude.hours)
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Local Mean Sidereal Time (LMST). Derived from GMST by adding the
5
+ # observer's longitude.
4
6
  class LocalMeanSiderealTime < LocalSiderealTime
7
+ # Creates LMST from a Greenwich Mean Sidereal Time and longitude.
8
+ #
5
9
  # Source:
6
10
  # Title: Practical Astronomy with your Calculator or Spreadsheet
7
11
  # Authors: Peter Duffett-Smith and Jonathan Zwart
8
12
  # Edition: Cambridge University Press
9
13
  # Chapter: 14 - Local sidereal time (LST)
14
+ #
15
+ # @param gst [Astronoby::GreenwichMeanSiderealTime] GMST
16
+ # @param longitude [Astronoby::Angle] the observer's longitude
17
+ # @return [Astronoby::LocalMeanSiderealTime]
18
+ # @raise [ArgumentError] if gst is not mean sidereal time
10
19
  def self.from_gst(gst:, longitude:)
11
20
  unless gst.mean?
12
21
  raise ArgumentError, "GST must be mean sidereal time"
@@ -18,20 +27,32 @@ module Astronoby
18
27
  new(date: date, time: time, longitude: longitude)
19
28
  end
20
29
 
30
+ # Creates LMST from UTC and longitude.
31
+ #
32
+ # @param utc [Time] the UTC time
33
+ # @param longitude [Astronoby::Angle] the observer's longitude
34
+ # @return [Astronoby::LocalMeanSiderealTime]
21
35
  def self.from_utc(utc, longitude:)
22
36
  gmst = GreenwichMeanSiderealTime.from_utc(utc)
23
37
  from_gst(gst: gmst, longitude: longitude)
24
38
  end
25
39
 
40
+ # @param date [Date] the calendar date
41
+ # @param time [Numeric] LMST in hours
42
+ # @param longitude [Astronoby::Angle] the observer's longitude
26
43
  def initialize(date:, time:, longitude:)
27
44
  super(date: date, time: time, longitude: longitude, type: MEAN)
28
45
  end
29
46
 
47
+ # Converts to Greenwich Mean Sidereal Time.
48
+ #
30
49
  # Source:
31
50
  # Title: Practical Astronomy with your Calculator or Spreadsheet
32
51
  # Authors: Peter Duffett-Smith and Jonathan Zwart
33
52
  # Edition: Cambridge University Press
34
53
  # Chapter: 15 - Converting LST to GST
54
+ #
55
+ # @return [Astronoby::GreenwichMeanSiderealTime]
35
56
  def to_gst
36
57
  date = @date
37
58
  time = normalize_time(@time - @longitude.hours)
@@ -1,9 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Local Sidereal Time base class. Dispatches to mean or apparent subclasses
5
+ # based on the type of the source GST.
4
6
  class LocalSiderealTime < SiderealTime
7
+ # @return [Astronoby::Angle] the observer's longitude
5
8
  attr_reader :longitude
6
9
 
10
+ # Creates an LST from a Greenwich Sidereal Time and longitude.
11
+ #
12
+ # @param gst [Astronoby::GreenwichSiderealTime] Greenwich Sidereal Time
13
+ # @param longitude [Astronoby::Angle] the observer's longitude
14
+ # @return [Astronoby::LocalMeanSiderealTime,
15
+ # Astronoby::LocalApparentSiderealTime]
7
16
  def self.from_gst(gst:, longitude:)
8
17
  case gst.type
9
18
  when MEAN
@@ -13,6 +22,13 @@ module Astronoby
13
22
  end
14
23
  end
15
24
 
25
+ # Creates an LST from UTC and longitude.
26
+ #
27
+ # @param utc [Time] the UTC time
28
+ # @param longitude [Astronoby::Angle] the observer's longitude
29
+ # @param type [Symbol] :mean or :apparent
30
+ # @return [Astronoby::LocalMeanSiderealTime,
31
+ # Astronoby::LocalApparentSiderealTime]
16
32
  def self.from_utc(utc, longitude:, type: MEAN)
17
33
  validate_type!(type)
18
34
  case type
@@ -23,11 +39,19 @@ module Astronoby
23
39
  end
24
40
  end
25
41
 
42
+ # @param date [Date] the calendar date
43
+ # @param time [Numeric] the sidereal time in hours
44
+ # @param longitude [Astronoby::Angle] the observer's longitude
45
+ # @param type [Symbol] :mean or :apparent
26
46
  def initialize(date:, time:, longitude:, type: MEAN)
27
47
  super(date: date, time: time, type: type)
28
48
  @longitude = longitude
29
49
  end
30
50
 
51
+ # Converts to Greenwich Sidereal Time.
52
+ #
53
+ # @return [Astronoby::GreenwichMeanSiderealTime,
54
+ # Astronoby::GreenwichApparentSiderealTime]
31
55
  def to_gst
32
56
  case @type
33
57
  when MEAN
@@ -1,40 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astronoby
4
+ # Base class for sidereal time representations. Sidereal time measures the
5
+ # rotation of the Earth relative to the vernal equinox.
4
6
  class SiderealTime
5
7
  TYPES = [
6
8
  MEAN = :mean,
7
9
  APPARENT = :apparent
8
10
  ].freeze
9
11
 
10
- attr_reader :date, :time, :type
12
+ # @return [Date] the calendar date
13
+ attr_reader :date
11
14
 
15
+ # @return [Numeric] the sidereal time in hours
16
+ attr_reader :time
17
+
18
+ # @return [Symbol] :mean or :apparent
19
+ attr_reader :type
20
+
21
+ # Normalizes a time value to the range [0, 24) hours.
22
+ #
23
+ # @param time [Numeric] time in hours
24
+ # @return [Numeric] normalized time in hours
12
25
  def self.normalize_time(time)
13
26
  time += Constants::HOURS_PER_DAY if time.negative?
14
27
  time -= Constants::HOURS_PER_DAY if time > Constants::HOURS_PER_DAY
15
28
  time
16
29
  end
17
30
 
31
+ # @param type [Symbol] :mean or :apparent
32
+ # @raise [ArgumentError] if type is invalid
18
33
  def self.validate_type!(type)
19
34
  unless TYPES.include?(type)
20
35
  raise ArgumentError, "Invalid type: #{type}. Must be one of #{TYPES}"
21
36
  end
22
37
  end
23
38
 
39
+ # @param date [Date] the calendar date
40
+ # @param time [Numeric] the sidereal time in hours
41
+ # @param type [Symbol] :mean or :apparent
24
42
  def initialize(date:, time:, type: MEAN)
25
43
  @date = date
26
44
  @time = time
27
45
  @type = type
28
46
  end
29
47
 
48
+ # @return [Boolean] true if this is mean sidereal time
30
49
  def mean?
31
50
  @type == MEAN
32
51
  end
33
52
 
53
+ # @return [Boolean] true if this is apparent sidereal time
34
54
  def apparent?
35
55
  @type == APPARENT
36
56
  end
37
57
 
58
+ # @param time [Numeric] time in hours
59
+ # @return [Numeric] normalized time in hours
38
60
  def normalize_time(time)
39
61
  self.class.normalize_time(time)
40
62
  end