astronoby 0.9.0 → 0.10.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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +101 -0
- data/README.md +6 -1
- data/UPGRADING.md +84 -0
- data/docs/README.md +80 -15
- data/docs/angles.md +1 -0
- data/docs/configuration.md +20 -17
- data/docs/coordinates.md +72 -12
- data/docs/deep_sky_bodies.md +1 -1
- data/docs/ephem.md +5 -2
- data/docs/equinoxes_solstices_times.md +4 -3
- data/docs/glossary.md +97 -1
- data/docs/iers.md +40 -0
- data/docs/instant.md +20 -15
- data/docs/lunar_eclipses.md +93 -0
- data/docs/lunar_observation.md +87 -0
- data/docs/moon_phases.md +4 -1
- data/docs/observer.md +20 -6
- data/docs/planetary_phenomena.md +78 -0
- data/docs/reference_frames.md +192 -34
- data/docs/rise_transit_set_times.md +6 -4
- data/docs/solar_system_bodies.md +26 -4
- data/docs/twilight_times.md +25 -21
- data/lib/astronoby/angle.rb +63 -2
- data/lib/astronoby/angles/dms.rb +18 -1
- data/lib/astronoby/angles/hms.rb +14 -1
- data/lib/astronoby/angular_velocity.rb +21 -0
- data/lib/astronoby/bodies/deep_sky_object.rb +6 -1
- data/lib/astronoby/bodies/deep_sky_object_position.rb +32 -17
- data/lib/astronoby/bodies/earth.rb +7 -44
- data/lib/astronoby/bodies/jupiter.rb +10 -0
- data/lib/astronoby/bodies/mars.rb +10 -0
- data/lib/astronoby/bodies/mercury.rb +10 -0
- data/lib/astronoby/bodies/moon.rb +158 -32
- data/lib/astronoby/bodies/neptune.rb +10 -0
- data/lib/astronoby/bodies/saturn.rb +10 -0
- data/lib/astronoby/bodies/solar_system_body.rb +240 -61
- data/lib/astronoby/bodies/sun.rb +79 -4
- data/lib/astronoby/bodies/uranus.rb +10 -0
- data/lib/astronoby/bodies/venus.rb +10 -0
- data/lib/astronoby/body.rb +6 -0
- data/lib/astronoby/center.rb +84 -0
- data/lib/astronoby/constellation.rb +9 -1
- data/lib/astronoby/coordinates/ecliptic.rb +10 -1
- data/lib/astronoby/coordinates/equatorial.rb +64 -8
- data/lib/astronoby/coordinates/geodetic.rb +102 -0
- data/lib/astronoby/coordinates/horizontal.rb +13 -1
- data/lib/astronoby/distance.rb +35 -0
- data/lib/astronoby/duration.rb +116 -0
- data/lib/astronoby/earth_rotation.rb +70 -0
- data/lib/astronoby/equinox_solstice.rb +31 -8
- data/lib/astronoby/errors.rb +11 -0
- data/lib/astronoby/events/conjunction.rb +51 -0
- data/lib/astronoby/events/conjunction_opposition_calculator.rb +84 -0
- data/lib/astronoby/events/eclipse_phase.rb +27 -0
- data/lib/astronoby/events/extremum_calculator.rb +23 -176
- data/lib/astronoby/events/greatest_elongation.rb +58 -0
- data/lib/astronoby/events/greatest_elongation_calculator.rb +56 -0
- data/lib/astronoby/events/lunar_eclipse.rb +99 -0
- data/lib/astronoby/events/lunar_eclipse_calculator.rb +285 -0
- data/lib/astronoby/events/opposition.rb +19 -0
- data/lib/astronoby/events/rise_transit_set_event.rb +12 -1
- data/lib/astronoby/events/rise_transit_set_events.rb +12 -1
- data/lib/astronoby/events/twilight_event.rb +24 -6
- data/lib/astronoby/events/twilight_events.rb +26 -6
- data/lib/astronoby/extremum_finder.rb +148 -0
- data/lib/astronoby/instant.rb +10 -7
- data/lib/astronoby/libration.rb +25 -0
- data/lib/astronoby/mean_obliquity.rb +8 -0
- data/lib/astronoby/moon_orientation_ephemeris.rb +69 -0
- data/lib/astronoby/moon_physical_ephemeris.rb +263 -0
- data/lib/astronoby/nutation.rb +10 -20
- data/lib/astronoby/observer.rb +67 -49
- data/lib/astronoby/orientation.rb +107 -0
- data/lib/astronoby/position.rb +16 -0
- data/lib/astronoby/precession.rb +61 -60
- data/lib/astronoby/reference_frame.rb +73 -7
- data/lib/astronoby/reference_frames/apparent.rb +26 -7
- data/lib/astronoby/reference_frames/astrometric.rb +14 -1
- data/lib/astronoby/reference_frames/geometric.rb +7 -1
- data/lib/astronoby/reference_frames/mean_of_date.rb +13 -1
- data/lib/astronoby/reference_frames/teme.rb +153 -0
- data/lib/astronoby/reference_frames/topocentric.rb +30 -4
- data/lib/astronoby/refraction.rb +26 -5
- data/lib/astronoby/root_finder.rb +83 -0
- data/lib/astronoby/rotation.rb +49 -0
- data/lib/astronoby/time/greenwich_apparent_sidereal_time.rb +9 -0
- data/lib/astronoby/time/greenwich_mean_sidereal_time.rb +42 -5
- data/lib/astronoby/time/greenwich_sidereal_time.rb +21 -0
- data/lib/astronoby/time/local_apparent_sidereal_time.rb +21 -0
- data/lib/astronoby/time/local_mean_sidereal_time.rb +21 -0
- data/lib/astronoby/time/local_sidereal_time.rb +24 -0
- data/lib/astronoby/time/sidereal_time.rb +23 -1
- data/lib/astronoby/true_obliquity.rb +4 -0
- data/lib/astronoby/util/maths.rb +8 -0
- data/lib/astronoby/util/time.rb +10 -485
- data/lib/astronoby/vector.rb +10 -0
- data/lib/astronoby/velocity.rb +39 -0
- data/lib/astronoby/version.rb +1 -1
- data/lib/astronoby.rb +22 -0
- metadata +45 -5
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class Duration
|
|
5
|
+
include Comparable
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
# @return [Astronoby::Duration] a zero duration
|
|
9
|
+
def zero
|
|
10
|
+
new(0)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @param seconds [Numeric] the duration in seconds
|
|
14
|
+
# @return [Astronoby::Duration] a new Duration
|
|
15
|
+
def from_seconds(seconds)
|
|
16
|
+
new(seconds)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param minutes [Numeric] the duration in minutes
|
|
20
|
+
# @return [Astronoby::Duration] a new Duration
|
|
21
|
+
def from_minutes(minutes)
|
|
22
|
+
seconds = minutes * Constants::SECONDS_PER_MINUTE
|
|
23
|
+
from_seconds(seconds)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param hours [Numeric] the duration in hours
|
|
27
|
+
# @return [Astronoby::Duration] a new Duration
|
|
28
|
+
def from_hours(hours)
|
|
29
|
+
seconds = hours * Constants::SECONDS_PER_HOUR
|
|
30
|
+
from_seconds(seconds)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param days [Numeric] the duration in days
|
|
34
|
+
# @return [Astronoby::Duration] a new Duration
|
|
35
|
+
def from_days(days)
|
|
36
|
+
seconds = days * Constants::SECONDS_PER_DAY
|
|
37
|
+
from_seconds(seconds)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Numeric] the duration in seconds
|
|
42
|
+
attr_reader :seconds
|
|
43
|
+
|
|
44
|
+
# @param seconds [Numeric] the duration in seconds
|
|
45
|
+
def initialize(seconds)
|
|
46
|
+
@seconds = seconds
|
|
47
|
+
freeze
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Float] the duration in minutes
|
|
51
|
+
def minutes
|
|
52
|
+
@seconds / Constants::SECONDS_PER_MINUTE
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Float] the duration in hours
|
|
56
|
+
def hours
|
|
57
|
+
@seconds / Constants::SECONDS_PER_HOUR
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Float] the duration in days
|
|
61
|
+
def days
|
|
62
|
+
@seconds / Constants::SECONDS_PER_DAY
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param other [Astronoby::Duration] duration to add
|
|
66
|
+
# @return [Astronoby::Duration] the sum
|
|
67
|
+
def +(other)
|
|
68
|
+
self.class.from_seconds(@seconds + other.seconds)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param other [Astronoby::Duration] duration to subtract
|
|
72
|
+
# @return [Astronoby::Duration] the difference
|
|
73
|
+
def -(other)
|
|
74
|
+
self.class.from_seconds(@seconds - other.seconds)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [Astronoby::Duration] the negated duration
|
|
78
|
+
def -@
|
|
79
|
+
self.class.from_seconds(-@seconds)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Astronoby::Duration] the absolute duration
|
|
83
|
+
def abs
|
|
84
|
+
self.class.from_seconds(@seconds.abs)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Boolean] true if the duration is positive
|
|
88
|
+
def positive?
|
|
89
|
+
@seconds > 0
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Boolean] true if the duration is negative
|
|
93
|
+
def negative?
|
|
94
|
+
@seconds < 0
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [Boolean] true if the duration is zero
|
|
98
|
+
def zero?
|
|
99
|
+
@seconds.zero?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Integer] hash value
|
|
103
|
+
def hash
|
|
104
|
+
[@seconds, self.class].hash
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @param other [Astronoby::Duration] duration to compare with
|
|
108
|
+
# @return [Integer, nil] -1, 0, or 1; nil if not comparable
|
|
109
|
+
def <=>(other)
|
|
110
|
+
return unless other.is_a?(self.class)
|
|
111
|
+
|
|
112
|
+
seconds <=> other.seconds
|
|
113
|
+
end
|
|
114
|
+
alias_method :eql?, :==
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
# Computes Earth rotation matrices using Greenwich Sidereal Time.
|
|
5
|
+
# Provides both apparent (GAST-based) and mean (GMST-based) rotation
|
|
6
|
+
# matrices for transforming between Earth-fixed and celestial frames.
|
|
7
|
+
class EarthRotation
|
|
8
|
+
# Computes the apparent Earth rotation matrix R₃(GAST) for a given
|
|
9
|
+
# instant.
|
|
10
|
+
#
|
|
11
|
+
# @param instant [Astronoby::Instant] the time instant
|
|
12
|
+
# @return [Matrix] 3x3 Earth rotation matrix
|
|
13
|
+
def self.matrix_for(instant)
|
|
14
|
+
new(instant: instant).matrix
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Computes the mean Earth rotation matrix R₃(GMST) for a given
|
|
18
|
+
# instant.
|
|
19
|
+
#
|
|
20
|
+
# @param instant [Astronoby::Instant] the time instant
|
|
21
|
+
# @return [Matrix] 3x3 mean Earth rotation matrix
|
|
22
|
+
def self.mean_matrix_for(instant)
|
|
23
|
+
new(instant: instant).mean_matrix
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param instant [Astronoby::Instant] the time instant
|
|
27
|
+
def initialize(instant:)
|
|
28
|
+
@instant = instant
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Computes R₃(GAST), the apparent Earth rotation matrix.
|
|
32
|
+
# GAST = GMST + equation of the equinoxes (Δψ cos ε₀).
|
|
33
|
+
#
|
|
34
|
+
# @return [Matrix] 3x3 rotation matrix
|
|
35
|
+
def matrix
|
|
36
|
+
rotation_matrix_for(gast)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Computes R₃(GMST), the mean Earth rotation matrix.
|
|
40
|
+
#
|
|
41
|
+
# @return [Matrix] 3x3 rotation matrix
|
|
42
|
+
def mean_matrix
|
|
43
|
+
rotation_matrix_for(gmst)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def gmst
|
|
49
|
+
Angle.from_hours(@instant.gmst)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def gast
|
|
53
|
+
nutation = Nutation.new(instant: @instant)
|
|
54
|
+
dpsi = nutation.nutation_in_longitude
|
|
55
|
+
mean_obliquity = MeanObliquity.at(@instant)
|
|
56
|
+
Angle.from_radians(
|
|
57
|
+
gmst.radians + dpsi.radians * mean_obliquity.cos
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def rotation_matrix_for(angle)
|
|
62
|
+
c, s = angle.cos, angle.sin
|
|
63
|
+
Matrix[
|
|
64
|
+
[c, -s, 0],
|
|
65
|
+
[s, c, 0],
|
|
66
|
+
[0, 0, 1]
|
|
67
|
+
]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Computes the time of equinoxes and solstices for a given year.
|
|
5
|
+
#
|
|
6
|
+
# Source:
|
|
7
|
+
# Title: Astronomical Algorithms
|
|
8
|
+
# Author: Jean Meeus
|
|
9
|
+
# Edition: 2nd edition
|
|
10
|
+
# Chapter: 27 - Equinoxes and Soltices
|
|
4
11
|
class EquinoxSolstice
|
|
5
|
-
# Source:
|
|
6
|
-
# Title: Astronomical Algorithms
|
|
7
|
-
# Author: Jean Meeus
|
|
8
|
-
# Edition: 2nd edition
|
|
9
|
-
# Chapter: 27 - Equinoxes and Soltices
|
|
10
|
-
|
|
11
12
|
EVENTS = [
|
|
12
13
|
MARCH_EQUINOX = 0,
|
|
13
14
|
JUNE_SOLSTICE = 1,
|
|
@@ -73,22 +74,39 @@ module Astronoby
|
|
|
73
74
|
[8, 15.45, 16859.074]
|
|
74
75
|
].freeze
|
|
75
76
|
|
|
77
|
+
# @param year [Integer] the year
|
|
78
|
+
# @param ephem [::Ephem::SPK] ephemeris data source
|
|
79
|
+
# @return [Time] time of the March equinox
|
|
76
80
|
def self.march_equinox(year, ephem)
|
|
77
81
|
new(year, MARCH_EQUINOX, ephem).time
|
|
78
82
|
end
|
|
79
83
|
|
|
84
|
+
# @param year [Integer] the year
|
|
85
|
+
# @param ephem [::Ephem::SPK] ephemeris data source
|
|
86
|
+
# @return [Time] time of the June solstice
|
|
80
87
|
def self.june_solstice(year, ephem)
|
|
81
88
|
new(year, JUNE_SOLSTICE, ephem).time
|
|
82
89
|
end
|
|
83
90
|
|
|
91
|
+
# @param year [Integer] the year
|
|
92
|
+
# @param ephem [::Ephem::SPK] ephemeris data source
|
|
93
|
+
# @return [Time] time of the September equinox
|
|
84
94
|
def self.september_equinox(year, ephem)
|
|
85
95
|
new(year, SEPTEMBER_EQUINOX, ephem).time
|
|
86
96
|
end
|
|
87
97
|
|
|
98
|
+
# @param year [Integer] the year
|
|
99
|
+
# @param ephem [::Ephem::SPK] ephemeris data source
|
|
100
|
+
# @return [Time] time of the December solstice
|
|
88
101
|
def self.december_solstice(year, ephem)
|
|
89
102
|
new(year, DECEMBER_SOLSTICE, ephem).time
|
|
90
103
|
end
|
|
91
104
|
|
|
105
|
+
# @param year [Integer] the year
|
|
106
|
+
# @param event [Integer] one of MARCH_EQUINOX, JUNE_SOLSTICE,
|
|
107
|
+
# SEPTEMBER_EQUINOX, or DECEMBER_SOLSTICE
|
|
108
|
+
# @param ephem [::Ephem::SPK] ephemeris data source
|
|
109
|
+
# @raise [Astronoby::UnsupportedEventError] if event is invalid
|
|
92
110
|
def initialize(year, event, ephem)
|
|
93
111
|
unless EVENTS.include?(event)
|
|
94
112
|
raise UnsupportedEventError.new(
|
|
@@ -97,12 +115,13 @@ module Astronoby
|
|
|
97
115
|
end
|
|
98
116
|
|
|
99
117
|
@event = event
|
|
118
|
+
@ephem = ephem
|
|
100
119
|
@year = (year.to_i - 2000) / 1000.0
|
|
101
120
|
uncorrected_time = compute
|
|
102
121
|
@instant = Instant.from_time(uncorrected_time)
|
|
103
|
-
@sun = Sun.new(ephem: ephem, instant: @instant)
|
|
104
122
|
end
|
|
105
123
|
|
|
124
|
+
# @return [Time] the corrected UTC time of the event
|
|
106
125
|
def time
|
|
107
126
|
Instant.from_terrestrial_time(@instant.tt + corrected).to_time.round
|
|
108
127
|
end
|
|
@@ -135,8 +154,12 @@ module Astronoby
|
|
|
135
154
|
component[4] * @year**4
|
|
136
155
|
end
|
|
137
156
|
|
|
157
|
+
def sun
|
|
158
|
+
@sun ||= Sun.new(ephem: @ephem, instant: @instant)
|
|
159
|
+
end
|
|
160
|
+
|
|
138
161
|
def corrected
|
|
139
|
-
longitude =
|
|
162
|
+
longitude = sun.apparent.ecliptic.longitude
|
|
140
163
|
58 * Angle.from_degrees(@event * 90 - longitude.degrees).sin
|
|
141
164
|
end
|
|
142
165
|
end
|
data/lib/astronoby/errors.rb
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
module Astronoby
|
|
2
|
+
# Raised when arguments are mutually incompatible.
|
|
2
3
|
class IncompatibleArgumentsError < ArgumentError; end
|
|
3
4
|
|
|
5
|
+
# Raised when an unsupported format is requested.
|
|
4
6
|
class UnsupportedFormatError < ArgumentError; end
|
|
5
7
|
|
|
8
|
+
# Raised when an unsupported event type is requested.
|
|
6
9
|
class UnsupportedEventError < ArgumentError; end
|
|
7
10
|
|
|
11
|
+
# Raised when an astronomical calculation fails.
|
|
8
12
|
class CalculationError < StandardError; end
|
|
9
13
|
|
|
14
|
+
# Raised when there is an error with ephemeris data.
|
|
10
15
|
class EphemerisError < StandardError; end
|
|
16
|
+
|
|
17
|
+
# Raised when an orientation kernel is not a supported orientation source.
|
|
18
|
+
class OrientationError < StandardError; end
|
|
19
|
+
|
|
20
|
+
# Raised when an orientation kernel does not cover the requested instant.
|
|
21
|
+
class OrientationOutOfRangeError < StandardError; end
|
|
11
22
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class Conjunction
|
|
5
|
+
INFERIOR = :inferior
|
|
6
|
+
SUPERIOR = :superior
|
|
7
|
+
|
|
8
|
+
# @param instant [Astronoby::Instant] when the conjunction occurs
|
|
9
|
+
# @param body [Astronoby::Body] the body in conjunction with the Sun
|
|
10
|
+
# @return [Astronoby::Conjunction] an inferior conjunction
|
|
11
|
+
def self.inferior(instant:, body:)
|
|
12
|
+
new(instant: instant, body: body, subtype: INFERIOR)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param instant [Astronoby::Instant] when the conjunction occurs
|
|
16
|
+
# @param body [Astronoby::Body] the body in conjunction with the Sun
|
|
17
|
+
# @return [Astronoby::Conjunction] a superior conjunction
|
|
18
|
+
def self.superior(instant:, body:)
|
|
19
|
+
new(instant: instant, body: body, subtype: SUPERIOR)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Astronoby::Instant] when the conjunction occurs
|
|
23
|
+
attr_reader :instant
|
|
24
|
+
|
|
25
|
+
# @return [Astronoby::Body] the body in conjunction with the Sun
|
|
26
|
+
attr_reader :body
|
|
27
|
+
|
|
28
|
+
# @return [Symbol] +INFERIOR+ or +SUPERIOR+
|
|
29
|
+
attr_reader :subtype
|
|
30
|
+
|
|
31
|
+
# @param instant [Astronoby::Instant] when the conjunction occurs
|
|
32
|
+
# @param body [Astronoby::Body] the body in conjunction with the Sun
|
|
33
|
+
# @param subtype [Symbol] +INFERIOR+ or +SUPERIOR+
|
|
34
|
+
def initialize(instant:, body:, subtype:)
|
|
35
|
+
@instant = instant
|
|
36
|
+
@body = body
|
|
37
|
+
@subtype = subtype
|
|
38
|
+
freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Boolean] true for an inferior conjunction
|
|
42
|
+
def inferior?
|
|
43
|
+
@subtype == INFERIOR
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Boolean] true for a superior conjunction
|
|
47
|
+
def superior?
|
|
48
|
+
@subtype == SUPERIOR
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class ConjunctionOppositionCalculator
|
|
5
|
+
# @param body [Astronoby::SolarSystemBody] the planet to track
|
|
6
|
+
# @param ephem [::Ephem::SPK] ephemeris data source
|
|
7
|
+
# @param samples_per_period [Integer] number of samples per synodic period
|
|
8
|
+
def initialize(body:, ephem:, samples_per_period: 60)
|
|
9
|
+
@body = body
|
|
10
|
+
@ephem = ephem
|
|
11
|
+
@samples_per_period = samples_per_period
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param start_time [Time] start time
|
|
15
|
+
# @param end_time [Time] end time
|
|
16
|
+
# @return [Array<Astronoby::Conjunction>] conjunctions in the range
|
|
17
|
+
def conjunction_events_between(start_time, end_time)
|
|
18
|
+
roots_between(start_time, end_time, accept: method(:conjunction?))
|
|
19
|
+
.map { |jd| conjunction_at(Instant.from_terrestrial_time(jd)) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param start_time [Time] start time
|
|
23
|
+
# @param end_time [Time] end time
|
|
24
|
+
# @return [Array<Astronoby::Opposition>] oppositions in the range
|
|
25
|
+
def opposition_events_between(start_time, end_time)
|
|
26
|
+
roots_between(start_time, end_time, accept: method(:opposition?))
|
|
27
|
+
.map do |jd|
|
|
28
|
+
Opposition.new(
|
|
29
|
+
instant: Instant.from_terrestrial_time(jd),
|
|
30
|
+
body: @body
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def roots_between(start_time, end_time, accept:)
|
|
38
|
+
finder.roots(
|
|
39
|
+
Instant.from_time(start_time).tt,
|
|
40
|
+
Instant.from_time(end_time).tt,
|
|
41
|
+
accept: accept
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def finder
|
|
46
|
+
@finder ||= RootFinder.new(
|
|
47
|
+
value_at: ->(jd) { delta_longitude_at(jd).sin },
|
|
48
|
+
period: synodic_period,
|
|
49
|
+
samples_per_period: @samples_per_period
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def conjunction?(jd)
|
|
54
|
+
delta_longitude_at(jd).cos.positive?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def opposition?(jd)
|
|
58
|
+
delta_longitude_at(jd).cos.negative?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def delta_longitude_at(jd)
|
|
62
|
+
instant = Instant.from_terrestrial_time(jd)
|
|
63
|
+
planet = @body.new(instant: instant, ephem: @ephem)
|
|
64
|
+
sun = Sun.new(instant: instant, ephem: @ephem)
|
|
65
|
+
planet.apparent.ecliptic.longitude - sun.apparent.ecliptic.longitude
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def conjunction_at(instant)
|
|
69
|
+
planet = @body.new(instant: instant, ephem: @ephem)
|
|
70
|
+
sun = Sun.new(instant: instant, ephem: @ephem)
|
|
71
|
+
|
|
72
|
+
if planet.apparent.distance < sun.apparent.distance
|
|
73
|
+
Conjunction.inferior(instant: instant, body: @body)
|
|
74
|
+
else
|
|
75
|
+
Conjunction.superior(instant: instant, body: @body)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def synodic_period
|
|
80
|
+
@synodic_period ||=
|
|
81
|
+
1.0 / ((1.0 / @body::ORBITAL_PERIOD) - (1.0 / Earth::ORBITAL_PERIOD)).abs
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
# A bounded phase of an eclipse, delimited by its two boundary instants.
|
|
5
|
+
class EclipsePhase
|
|
6
|
+
# @return [Astronoby::Instant] when the phase begins
|
|
7
|
+
attr_reader :starting_instant
|
|
8
|
+
|
|
9
|
+
# @return [Astronoby::Instant] when the phase ends
|
|
10
|
+
attr_reader :ending_instant
|
|
11
|
+
|
|
12
|
+
# @param starting_instant [Astronoby::Instant] when the phase begins
|
|
13
|
+
# @param ending_instant [Astronoby::Instant] when the phase ends
|
|
14
|
+
def initialize(starting_instant:, ending_instant:)
|
|
15
|
+
@starting_instant = starting_instant
|
|
16
|
+
@ending_instant = ending_instant
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Astronoby::Duration] phase duration
|
|
21
|
+
def duration
|
|
22
|
+
Duration.from_seconds(
|
|
23
|
+
(@ending_instant.to_time - @starting_instant.to_time).round
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,49 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
-
# Calculates extrema (minima and maxima) in the distance between two
|
|
5
|
-
# celestial bodies over a given time range using adaptive sampling and golden
|
|
6
|
-
# section search refinement.
|
|
7
4
|
class ExtremumCalculator
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# Algorithm parameters
|
|
14
|
-
MIN_SAMPLES_PER_PERIOD = 20
|
|
15
|
-
DUPLICATE_THRESHOLD_DAYS = 0.5
|
|
16
|
-
BOUNDARY_BUFFER_DAYS = 0.01
|
|
17
|
-
|
|
18
|
-
# Orbital periods
|
|
19
|
-
ORBITAL_PERIODS = {
|
|
20
|
-
"Astronoby::Moon" => 27.504339,
|
|
21
|
-
"Astronoby::Mercury" => 87.969,
|
|
22
|
-
"Astronoby::Venus" => 224.701,
|
|
23
|
-
"Astronoby::Earth" => 365.256,
|
|
24
|
-
"Astronoby::Mars" => 686.98,
|
|
25
|
-
"Astronoby::Jupiter" => 4332.59,
|
|
26
|
-
"Astronoby::Saturn" => 10759.22,
|
|
27
|
-
"Astronoby::Uranus" => 30688.5,
|
|
28
|
-
"Astronoby::Neptune" => 60182.0
|
|
29
|
-
}.freeze
|
|
30
|
-
|
|
31
|
-
# @param ephem [::Ephem::SPK] Ephemeris data source
|
|
32
|
-
# @param body [Astronoby::SolarSystemBody] The celestial body to track
|
|
33
|
-
# @param primary_body [Astronoby::SolarSystemBody] The reference body
|
|
34
|
-
# (e.g., Sun for planetary orbits)
|
|
35
|
-
# @param samples_per_period [Integer] Number of samples to take per orbital
|
|
36
|
-
# period
|
|
5
|
+
# @param body [Astronoby::SolarSystemBody] the celestial body to track
|
|
6
|
+
# @param primary_body [Astronoby::SolarSystemBody] the reference body
|
|
7
|
+
# @param ephem [::Ephem::SPK] ephemeris data source
|
|
8
|
+
# @param samples_per_period [Integer] number of samples per orbital period
|
|
37
9
|
def initialize(
|
|
38
10
|
body:,
|
|
39
11
|
primary_body:,
|
|
40
12
|
ephem:,
|
|
41
13
|
samples_per_period: 60
|
|
42
14
|
)
|
|
43
|
-
@ephem = ephem
|
|
44
15
|
@body = body
|
|
45
16
|
@primary_body = primary_body
|
|
46
|
-
@
|
|
17
|
+
@ephem = ephem
|
|
47
18
|
@samples_per_period = samples_per_period
|
|
48
19
|
end
|
|
49
20
|
|
|
@@ -53,8 +24,8 @@ module Astronoby
|
|
|
53
24
|
# @return [Array<Astronoby::ExtremumEvent>] Array of apoapsis events
|
|
54
25
|
def apoapsis_events_between(start_time, end_time)
|
|
55
26
|
find_extrema(
|
|
56
|
-
|
|
57
|
-
|
|
27
|
+
Instant.from_time(start_time).tt,
|
|
28
|
+
Instant.from_time(end_time).tt,
|
|
58
29
|
type: :maximum
|
|
59
30
|
)
|
|
60
31
|
end
|
|
@@ -65,8 +36,8 @@ module Astronoby
|
|
|
65
36
|
# @return [Array<Astronoby::ExtremumEvent>] Array of periapsis events
|
|
66
37
|
def periapsis_events_between(start_time, end_time)
|
|
67
38
|
find_extrema(
|
|
68
|
-
|
|
69
|
-
|
|
39
|
+
Instant.from_time(start_time).tt,
|
|
40
|
+
Instant.from_time(end_time).tt,
|
|
70
41
|
type: :minimum
|
|
71
42
|
)
|
|
72
43
|
end
|
|
@@ -78,21 +49,24 @@ module Astronoby
|
|
|
78
49
|
# @param type [Symbol] :maximum or :minimum
|
|
79
50
|
# @return [Array<Astronoby::ExtremumEvent>] Array of extrema events
|
|
80
51
|
def find_extrema(start_jd, end_jd, type: :maximum)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
.compact
|
|
88
|
-
|
|
89
|
-
# 3: Remove duplicates and boundary artifacts
|
|
90
|
-
refined_extrema = remove_duplicates(refined_extrema)
|
|
91
|
-
filter_boundary_artifacts(refined_extrema, start_jd, end_jd)
|
|
52
|
+
finder.extrema(start_jd, end_jd, type: type).map do |extremum|
|
|
53
|
+
ExtremumEvent.new(
|
|
54
|
+
Instant.from_terrestrial_time(extremum[:jd]),
|
|
55
|
+
extremum[:value]
|
|
56
|
+
)
|
|
57
|
+
end
|
|
92
58
|
end
|
|
93
59
|
|
|
94
60
|
private
|
|
95
61
|
|
|
62
|
+
def finder
|
|
63
|
+
@finder ||= ExtremumFinder.new(
|
|
64
|
+
value_at: method(:distance_at),
|
|
65
|
+
period: @body::ORBITAL_PERIOD,
|
|
66
|
+
samples_per_period: @samples_per_period
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
96
70
|
def distance_at(jd)
|
|
97
71
|
instant = Instant.from_terrestrial_time(jd)
|
|
98
72
|
body_geometric = @body.geometric(ephem: @ephem, instant: instant)
|
|
@@ -102,132 +76,5 @@ module Astronoby
|
|
|
102
76
|
distance_vector = body_geometric.position - primary_geometric.position
|
|
103
77
|
distance_vector.magnitude
|
|
104
78
|
end
|
|
105
|
-
|
|
106
|
-
def find_extrema_candidates(start_jd, end_jd, type)
|
|
107
|
-
samples = collect_samples(start_jd, end_jd)
|
|
108
|
-
find_local_extrema_in_samples(samples, type)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def collect_samples(start_jd, end_jd)
|
|
112
|
-
duration = end_jd - start_jd
|
|
113
|
-
sample_count = calculate_sample_count(duration)
|
|
114
|
-
step = duration / sample_count
|
|
115
|
-
|
|
116
|
-
(0..sample_count).map do |i|
|
|
117
|
-
jd = start_jd + (i * step)
|
|
118
|
-
{jd: jd, value: distance_at(jd)}
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def calculate_sample_count(duration)
|
|
123
|
-
# Adaptive sampling: scale with duration and orbital period
|
|
124
|
-
periods_in_range = duration / @orbital_period
|
|
125
|
-
base_samples = (periods_in_range * @samples_per_period).to_i
|
|
126
|
-
|
|
127
|
-
# Ensure minimum sample density for short ranges
|
|
128
|
-
[base_samples, MIN_SAMPLES_PER_PERIOD].max
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def find_local_extrema_in_samples(samples, type)
|
|
132
|
-
candidates = []
|
|
133
|
-
|
|
134
|
-
# Check each interior point for local extrema
|
|
135
|
-
(1...samples.length - 1).each do |i|
|
|
136
|
-
if local_extremum?(samples, i, type)
|
|
137
|
-
candidates << {
|
|
138
|
-
start_jd: samples[i - 1][:jd],
|
|
139
|
-
end_jd: samples[i + 1][:jd],
|
|
140
|
-
center_jd: samples[i][:jd]
|
|
141
|
-
}
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
candidates
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def local_extremum?(samples, index, type)
|
|
149
|
-
current_val = samples[index][:value].m
|
|
150
|
-
prev_val = samples[index - 1][:value].m
|
|
151
|
-
next_val = samples[index + 1][:value].m
|
|
152
|
-
|
|
153
|
-
if type == :maximum
|
|
154
|
-
current_val > prev_val && current_val > next_val
|
|
155
|
-
else
|
|
156
|
-
current_val < prev_val && current_val < next_val
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def refine_extremum(candidate, type)
|
|
161
|
-
golden_section_search(candidate[:start_jd], candidate[:end_jd], type)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def golden_section_search(a, b, type)
|
|
165
|
-
return nil if b <= a
|
|
166
|
-
|
|
167
|
-
tol = GOLDEN_SECTION_TOLERANCE * (b - a).abs
|
|
168
|
-
|
|
169
|
-
# Initial points using golden ratio
|
|
170
|
-
x1 = a + (1 - INVPHI) * (b - a)
|
|
171
|
-
x2 = a + INVPHI * (b - a)
|
|
172
|
-
|
|
173
|
-
f1 = distance_at(x1).m
|
|
174
|
-
f2 = distance_at(x2).m
|
|
175
|
-
|
|
176
|
-
while (b - a).abs > tol
|
|
177
|
-
should_keep_left = (type == :maximum) ? (f1 > f2) : (f1 < f2)
|
|
178
|
-
|
|
179
|
-
if should_keep_left
|
|
180
|
-
b = x2
|
|
181
|
-
x2 = x1
|
|
182
|
-
f2 = f1
|
|
183
|
-
x1 = a + (1 - INVPHI) * (b - a)
|
|
184
|
-
f1 = distance_at(x1).m
|
|
185
|
-
else
|
|
186
|
-
a = x1
|
|
187
|
-
x1 = x2
|
|
188
|
-
f1 = f2
|
|
189
|
-
x2 = a + INVPHI * (b - a)
|
|
190
|
-
f2 = distance_at(x2).m
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
mid = (a + b) / 2
|
|
195
|
-
ExtremumEvent.new(
|
|
196
|
-
Instant.from_terrestrial_time(mid),
|
|
197
|
-
distance_at(mid)
|
|
198
|
-
)
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def remove_duplicates(extrema)
|
|
202
|
-
return extrema if extrema.length <= 1
|
|
203
|
-
|
|
204
|
-
cleaned = [extrema.first]
|
|
205
|
-
|
|
206
|
-
extrema.each_with_index do |current, i|
|
|
207
|
-
next if i == 0
|
|
208
|
-
|
|
209
|
-
is_duplicate = cleaned.any? do |existing|
|
|
210
|
-
time_diff = (
|
|
211
|
-
current.instant.tt - existing.instant.tt
|
|
212
|
-
).abs
|
|
213
|
-
time_diff < DUPLICATE_THRESHOLD_DAYS
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
cleaned << current unless is_duplicate
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
cleaned
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def filter_boundary_artifacts(extrema, start_jd, end_jd)
|
|
223
|
-
extrema.reject do |extreme|
|
|
224
|
-
start_diff = (extreme.instant.tt - start_jd).abs
|
|
225
|
-
end_diff = (extreme.instant.tt - end_jd).abs
|
|
226
|
-
|
|
227
|
-
too_close_to_start = start_diff < BOUNDARY_BUFFER_DAYS
|
|
228
|
-
too_close_to_end = end_diff < BOUNDARY_BUFFER_DAYS
|
|
229
|
-
too_close_to_start || too_close_to_end
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
79
|
end
|
|
233
80
|
end
|