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,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class GreatestElongation
|
|
5
|
+
EASTERN = :eastern
|
|
6
|
+
WESTERN = :western
|
|
7
|
+
|
|
8
|
+
# @param instant [Astronoby::Instant] when the greatest elongation occurs
|
|
9
|
+
# @param body [Astronoby::Body] the body reaching greatest elongation
|
|
10
|
+
# @param angle [Astronoby::Angle] the Sun-Earth-body angle at that instant
|
|
11
|
+
# @return [Astronoby::GreatestElongation] a greatest eastern elongation
|
|
12
|
+
def self.eastern(instant:, body:, angle:)
|
|
13
|
+
new(instant: instant, body: body, angle: angle, direction: EASTERN)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param instant [Astronoby::Instant] when the greatest elongation occurs
|
|
17
|
+
# @param body [Astronoby::Body] the body reaching greatest elongation
|
|
18
|
+
# @param angle [Astronoby::Angle] the Sun-Earth-body angle at that instant
|
|
19
|
+
# @return [Astronoby::GreatestElongation] a greatest western elongation
|
|
20
|
+
def self.western(instant:, body:, angle:)
|
|
21
|
+
new(instant: instant, body: body, angle: angle, direction: WESTERN)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Astronoby::Instant] when the greatest elongation occurs
|
|
25
|
+
attr_reader :instant
|
|
26
|
+
|
|
27
|
+
# @return [Astronoby::Body] the body reaching greatest elongation
|
|
28
|
+
attr_reader :body
|
|
29
|
+
|
|
30
|
+
# @return [Astronoby::Angle] the Sun-Earth-body angle at that instant
|
|
31
|
+
attr_reader :angle
|
|
32
|
+
|
|
33
|
+
# @return [Symbol] +EASTERN+ or +WESTERN+
|
|
34
|
+
attr_reader :direction
|
|
35
|
+
|
|
36
|
+
# @param instant [Astronoby::Instant] when the greatest elongation occurs
|
|
37
|
+
# @param body [Astronoby::Body] the body reaching greatest elongation
|
|
38
|
+
# @param angle [Astronoby::Angle] the Sun-Earth-body angle at that instant
|
|
39
|
+
# @param direction [Symbol] +EASTERN+ or +WESTERN+
|
|
40
|
+
def initialize(instant:, body:, angle:, direction:)
|
|
41
|
+
@instant = instant
|
|
42
|
+
@body = body
|
|
43
|
+
@angle = angle
|
|
44
|
+
@direction = direction
|
|
45
|
+
freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] true for a greatest eastern elongation
|
|
49
|
+
def eastern?
|
|
50
|
+
@direction == EASTERN
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Boolean] true for a greatest western elongation
|
|
54
|
+
def western?
|
|
55
|
+
@direction == WESTERN
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class GreatestElongationCalculator
|
|
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::GreatestElongation>] greatest elongations in
|
|
17
|
+
# the range
|
|
18
|
+
def greatest_elongation_events_between(start_time, end_time)
|
|
19
|
+
finder.extrema(
|
|
20
|
+
Instant.from_time(start_time).tt,
|
|
21
|
+
Instant.from_time(end_time).tt,
|
|
22
|
+
type: :maximum
|
|
23
|
+
).map { |extremum| build_event(extremum) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def finder
|
|
29
|
+
@finder ||= ExtremumFinder.new(
|
|
30
|
+
value_at: ->(jd) { planet_at(jd).elongation },
|
|
31
|
+
period: synodic_period,
|
|
32
|
+
samples_per_period: @samples_per_period
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_event(extremum)
|
|
37
|
+
instant = Instant.from_terrestrial_time(extremum[:jd])
|
|
38
|
+
angle = extremum[:value]
|
|
39
|
+
|
|
40
|
+
if planet_at(instant.tt).eastern?
|
|
41
|
+
GreatestElongation.eastern(instant: instant, body: @body, angle: angle)
|
|
42
|
+
else
|
|
43
|
+
GreatestElongation.western(instant: instant, body: @body, angle: angle)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def planet_at(jd)
|
|
48
|
+
@body.new(instant: Instant.from_terrestrial_time(jd), ephem: @ephem)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def synodic_period
|
|
52
|
+
@synodic_period ||=
|
|
53
|
+
1.0 / ((1.0 / @body::ORBITAL_PERIOD) - (1.0 / Earth::ORBITAL_PERIOD)).abs
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
# A lunar eclipse: a geocentric passage of the Moon through Earth's shadow.
|
|
5
|
+
# Immutable; built by LunarEclipseCalculator.
|
|
6
|
+
#
|
|
7
|
+
# The penumbral phase is always present. The partial phase is present for
|
|
8
|
+
# partial and total eclipses, and the total phase only for total eclipses.
|
|
9
|
+
class LunarEclipse
|
|
10
|
+
PENUMBRAL = :penumbral
|
|
11
|
+
PARTIAL = :partial
|
|
12
|
+
TOTAL = :total
|
|
13
|
+
|
|
14
|
+
# @return [Astronoby::Instant] greatest eclipse, when the Moon's centre is
|
|
15
|
+
# least distant from the axis of Earth's shadow
|
|
16
|
+
attr_reader :instant
|
|
17
|
+
alias_method :greatest_eclipse, :instant
|
|
18
|
+
|
|
19
|
+
# @return [Symbol] +PENUMBRAL+, +PARTIAL+ or +TOTAL+
|
|
20
|
+
attr_reader :kind
|
|
21
|
+
|
|
22
|
+
# @return [Float] fraction of the Moon's diameter immersed in the umbra at
|
|
23
|
+
# greatest eclipse (negative when the Moon misses the umbra)
|
|
24
|
+
attr_reader :umbral_magnitude
|
|
25
|
+
|
|
26
|
+
# @return [Float] fraction of the Moon's diameter immersed in the penumbra
|
|
27
|
+
# at greatest eclipse
|
|
28
|
+
attr_reader :penumbral_magnitude
|
|
29
|
+
|
|
30
|
+
# @return [Float] least distance of the Moon's centre from the axis of
|
|
31
|
+
# Earth's shadow at greatest eclipse, in Earth radii, positive when the
|
|
32
|
+
# Moon passes north of the axis
|
|
33
|
+
attr_reader :gamma
|
|
34
|
+
|
|
35
|
+
# @return [Astronoby::Distance] least distance of the Moon's centre from the
|
|
36
|
+
# axis of Earth's shadow at greatest eclipse. This is the unsigned length
|
|
37
|
+
# of which gamma is the value in Earth radii.
|
|
38
|
+
attr_reader :shadow_axis_distance
|
|
39
|
+
|
|
40
|
+
# @return [Astronoby::EclipsePhase] the penumbral phase (always present)
|
|
41
|
+
attr_reader :penumbral
|
|
42
|
+
|
|
43
|
+
# @return [Astronoby::EclipsePhase, nil] the partial phase, present for
|
|
44
|
+
# partial and total eclipses
|
|
45
|
+
attr_reader :partial
|
|
46
|
+
|
|
47
|
+
# @return [Astronoby::EclipsePhase, nil] the total phase (totality),
|
|
48
|
+
# present only for total eclipses
|
|
49
|
+
attr_reader :total
|
|
50
|
+
|
|
51
|
+
# @param instant [Astronoby::Instant] greatest eclipse
|
|
52
|
+
# @param kind [Symbol] +PENUMBRAL+, +PARTIAL+ or +TOTAL+
|
|
53
|
+
# @param umbral_magnitude [Float] umbral magnitude at greatest eclipse
|
|
54
|
+
# @param penumbral_magnitude [Float] penumbral magnitude at greatest eclipse
|
|
55
|
+
# @param gamma [Float] least distance from the shadow axis, in Earth radii
|
|
56
|
+
# @param shadow_axis_distance [Astronoby::Distance] least distance from the
|
|
57
|
+
# shadow axis
|
|
58
|
+
# @param penumbral [Astronoby::EclipsePhase] the penumbral phase
|
|
59
|
+
# @param partial [Astronoby::EclipsePhase, nil] the partial phase
|
|
60
|
+
# @param total [Astronoby::EclipsePhase, nil] the total phase
|
|
61
|
+
def initialize(
|
|
62
|
+
instant:,
|
|
63
|
+
kind:,
|
|
64
|
+
umbral_magnitude:,
|
|
65
|
+
penumbral_magnitude:,
|
|
66
|
+
gamma:,
|
|
67
|
+
shadow_axis_distance:,
|
|
68
|
+
penumbral:,
|
|
69
|
+
partial: nil,
|
|
70
|
+
total: nil
|
|
71
|
+
)
|
|
72
|
+
@instant = instant
|
|
73
|
+
@kind = kind
|
|
74
|
+
@umbral_magnitude = umbral_magnitude
|
|
75
|
+
@penumbral_magnitude = penumbral_magnitude
|
|
76
|
+
@gamma = gamma
|
|
77
|
+
@shadow_axis_distance = shadow_axis_distance
|
|
78
|
+
@penumbral = penumbral
|
|
79
|
+
@partial = partial
|
|
80
|
+
@total = total
|
|
81
|
+
freeze
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @return [Boolean] true for a penumbral eclipse (the Moon misses the umbra)
|
|
85
|
+
def penumbral?
|
|
86
|
+
@kind == PENUMBRAL
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [Boolean] true for a partial eclipse
|
|
90
|
+
def partial?
|
|
91
|
+
@kind == PARTIAL
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [Boolean] true for a total eclipse
|
|
95
|
+
def total?
|
|
96
|
+
@kind == TOTAL
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
# Computes lunar eclipses over a time range.
|
|
5
|
+
#
|
|
6
|
+
# A lunar eclipse is a geocentric event, identical for every observer who can
|
|
7
|
+
# see the Moon, so no observer is involved. The geometry is built from the
|
|
8
|
+
# apparent geocentric positions of the Sun and Moon: this matches the standard
|
|
9
|
+
# reduction used by IMCCE, validated against IMCCE (Opale, INPOP19A) where the
|
|
10
|
+
# eclipse kind, greatest eclipse, magnitudes, and contact times all agree to
|
|
11
|
+
# within a second or two.
|
|
12
|
+
#
|
|
13
|
+
# Candidate full moons are seeded analytically from Events::MoonPhases, then
|
|
14
|
+
# refined against the ephemeris: full moons far from a node are skipped, the
|
|
15
|
+
# greatest eclipse is the least distance of the Moon's centre from the shadow
|
|
16
|
+
# axis, and each contact is found by bisecting between greatest eclipse (inside
|
|
17
|
+
# the shadow) and the edge of the search window (outside it).
|
|
18
|
+
#
|
|
19
|
+
# Source:
|
|
20
|
+
# Title: Explanatory Supplement to the Astronomical Almanac
|
|
21
|
+
# Authors: Sean E. Urban and P. Kenneth Seidelmann
|
|
22
|
+
# Chapter: 11 - Eclipses of the Sun and Moon
|
|
23
|
+
class LunarEclipseCalculator
|
|
24
|
+
# Atmospheric enlargement of Earth's shadow (Danjon-style): Earth's radius is
|
|
25
|
+
# enlarged before the shadow cones are built, which propagates into both the
|
|
26
|
+
# umbra and the penumbra. The 1/99 factor is calibrated against IMCCE (which
|
|
27
|
+
# uses the same INPOP19A ephemeris): it reproduces IMCCE's umbra and penumbra
|
|
28
|
+
# angular radii to about 0.1 arcsecond across the 2023-2025 eclipses.
|
|
29
|
+
SHADOW_ENLARGEMENT = 1.0 + 1.0 / 99
|
|
30
|
+
|
|
31
|
+
SUN_RADIUS_KM = Sun::EQUATORIAL_RADIUS.km
|
|
32
|
+
MOON_RADIUS_KM = Moon::EQUATORIAL_RADIUS.km
|
|
33
|
+
EARTH_RADIUS_KM =
|
|
34
|
+
Constants::WGS84_EARTH_EQUATORIAL_RADIUS_IN_METERS / 1000.0
|
|
35
|
+
|
|
36
|
+
# Largest distance of the Moon's centre from the shadow axis, in Earth radii,
|
|
37
|
+
# at which any (penumbral) eclipse is still possible is about 1.57. Full moons
|
|
38
|
+
# whose seed already exceeds this margin cannot be eclipses and skip the
|
|
39
|
+
# minimum search entirely.
|
|
40
|
+
MAX_ECLIPSE_GAMMA = 1.8
|
|
41
|
+
|
|
42
|
+
# Half-window, in days, for the local greatest-eclipse search around a full
|
|
43
|
+
# moon. A lunar eclipse occurs within minutes of full moon.
|
|
44
|
+
GREATEST_HALF_WINDOW = 0.25
|
|
45
|
+
|
|
46
|
+
# Half-window, in days, for the contact search around greatest eclipse. Wide
|
|
47
|
+
# enough to bracket the longest penumbral phase (about 3 hours each side).
|
|
48
|
+
CONTACT_HALF_WINDOW = 0.21
|
|
49
|
+
|
|
50
|
+
SEARCH_SAMPLES = 48
|
|
51
|
+
|
|
52
|
+
# Bisection tolerance, in days, for a contact time. ~1e-7 day is ~8.6 ms,
|
|
53
|
+
# well below the one-second resolution the contacts are reported at.
|
|
54
|
+
CONTACT_TOLERANCE = 1e-7
|
|
55
|
+
|
|
56
|
+
# Geometry of the Sun, Moon and Earth's shadow at one instant, in kilometres
|
|
57
|
+
# in the plane perpendicular to the shadow axis at the Moon's distance.
|
|
58
|
+
class Geometry
|
|
59
|
+
# @return [Float] distance of the Moon's centre from the shadow axis (km)
|
|
60
|
+
attr_reader :axis_distance
|
|
61
|
+
|
|
62
|
+
# @return [Float] radius of the umbra at the Moon's distance (km)
|
|
63
|
+
attr_reader :umbra_radius
|
|
64
|
+
|
|
65
|
+
# @return [Float] radius of the penumbra at the Moon's distance (km)
|
|
66
|
+
attr_reader :penumbra_radius
|
|
67
|
+
|
|
68
|
+
# @return [Float] signed distance from the shadow axis, in Earth radii
|
|
69
|
+
attr_reader :gamma
|
|
70
|
+
|
|
71
|
+
def initialize(axis_distance:, umbra_radius:, penumbra_radius:, gamma:)
|
|
72
|
+
@axis_distance = axis_distance
|
|
73
|
+
@umbra_radius = umbra_radius
|
|
74
|
+
@penumbra_radius = penumbra_radius
|
|
75
|
+
@gamma = gamma
|
|
76
|
+
freeze
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def umbral_magnitude
|
|
80
|
+
(umbra_radius + MOON_RADIUS_KM - axis_distance) / (2 * MOON_RADIUS_KM)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def penumbral_magnitude
|
|
84
|
+
(
|
|
85
|
+
penumbra_radius + MOON_RADIUS_KM - axis_distance
|
|
86
|
+
) / (2 * MOON_RADIUS_KM)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def penumbral_contact_value
|
|
90
|
+
axis_distance - (penumbra_radius + MOON_RADIUS_KM)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def partial_contact_value
|
|
94
|
+
axis_distance - (umbra_radius + MOON_RADIUS_KM)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def total_contact_value
|
|
98
|
+
axis_distance - (umbra_radius - MOON_RADIUS_KM)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @param ephem [::Ephem::SPK] ephemeris data source
|
|
103
|
+
def initialize(ephem:)
|
|
104
|
+
@ephem = ephem
|
|
105
|
+
@geometry_cache = {}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @param start_time [Time] start time
|
|
109
|
+
# @param end_time [Time] end time
|
|
110
|
+
# @return [Array<Astronoby::LunarEclipse>] eclipses whose greatest instant
|
|
111
|
+
# lies in the range, sorted by time
|
|
112
|
+
def events_between(start_time, end_time)
|
|
113
|
+
full_moon_seeds(start_time, end_time)
|
|
114
|
+
.filter_map { |seed_jd| eclipse_near(seed_jd) }
|
|
115
|
+
.select do |eclipse|
|
|
116
|
+
eclipse.instant.to_time.between?(start_time, end_time)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
# Full moons in the range, padded by a day so an eclipse near a boundary is
|
|
123
|
+
# not missed. Seeded analytically (Meeus, chapter 49) at no ephemeris cost.
|
|
124
|
+
def full_moon_seeds(start_time, end_time)
|
|
125
|
+
padded_start = start_time - Constants::SECONDS_PER_DAY
|
|
126
|
+
padded_end = end_time + Constants::SECONDS_PER_DAY
|
|
127
|
+
year_months(padded_start, padded_end)
|
|
128
|
+
.flat_map do |year, month|
|
|
129
|
+
Events::MoonPhases.phases_for(year: year, month: month)
|
|
130
|
+
end
|
|
131
|
+
.select { |phase| phase.phase == :full_moon }
|
|
132
|
+
.map(&:time)
|
|
133
|
+
.select { |time| time.between?(padded_start, padded_end) }
|
|
134
|
+
.map { |time| Instant.from_time(time).tt }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def year_months(from, to)
|
|
138
|
+
cursor = Date.new(from.to_date.year, from.to_date.month, 1)
|
|
139
|
+
last = Date.new(to.to_date.year, to.to_date.month, 1)
|
|
140
|
+
months = []
|
|
141
|
+
while cursor <= last
|
|
142
|
+
months << [cursor.year, cursor.month]
|
|
143
|
+
cursor = cursor.next_month
|
|
144
|
+
end
|
|
145
|
+
months
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Resolves the eclipse for one full-moon seed, if any.
|
|
149
|
+
def eclipse_near(seed_jd)
|
|
150
|
+
if geometry_at(seed_jd).axis_distance > MAX_ECLIPSE_GAMMA * EARTH_RADIUS_KM
|
|
151
|
+
return nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
greatest_jd = greatest_eclipse_jd(seed_jd)
|
|
155
|
+
greatest_jd && eclipse_at(greatest_jd)
|
|
156
|
+
ensure
|
|
157
|
+
@geometry_cache.clear
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Greatest eclipse: the least distance of the Moon's centre from the shadow
|
|
161
|
+
# axis, found by minimising it on a narrow window around the full moon.
|
|
162
|
+
def greatest_eclipse_jd(seed_jd)
|
|
163
|
+
ExtremumFinder
|
|
164
|
+
.new(
|
|
165
|
+
value_at: ->(jd) { geometry_at(jd).axis_distance },
|
|
166
|
+
period: 2 * GREATEST_HALF_WINDOW,
|
|
167
|
+
samples_per_period: SEARCH_SAMPLES
|
|
168
|
+
)
|
|
169
|
+
.extrema(
|
|
170
|
+
seed_jd - GREATEST_HALF_WINDOW,
|
|
171
|
+
seed_jd + GREATEST_HALF_WINDOW,
|
|
172
|
+
type: :minimum
|
|
173
|
+
)
|
|
174
|
+
.min_by { |extremum| extremum[:value] }
|
|
175
|
+
&.fetch(:jd)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def eclipse_at(greatest_jd)
|
|
179
|
+
geometry = geometry_at(greatest_jd)
|
|
180
|
+
penumbral = phase_for(greatest_jd, &:penumbral_contact_value)
|
|
181
|
+
return nil if penumbral.nil?
|
|
182
|
+
|
|
183
|
+
partial = phase_for(greatest_jd, &:partial_contact_value)
|
|
184
|
+
total = phase_for(greatest_jd, &:total_contact_value)
|
|
185
|
+
|
|
186
|
+
LunarEclipse.new(
|
|
187
|
+
instant: Instant.from_terrestrial_time(greatest_jd),
|
|
188
|
+
kind: kind_for(partial, total),
|
|
189
|
+
umbral_magnitude: geometry.umbral_magnitude,
|
|
190
|
+
penumbral_magnitude: geometry.penumbral_magnitude,
|
|
191
|
+
gamma: geometry.gamma,
|
|
192
|
+
shadow_axis_distance: Distance.from_kilometers(geometry.axis_distance),
|
|
193
|
+
penumbral: penumbral,
|
|
194
|
+
partial: partial,
|
|
195
|
+
total: total
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def kind_for(partial, total)
|
|
200
|
+
if total
|
|
201
|
+
LunarEclipse::TOTAL
|
|
202
|
+
elsif partial
|
|
203
|
+
LunarEclipse::PARTIAL
|
|
204
|
+
else
|
|
205
|
+
LunarEclipse::PENUMBRAL
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Builds a phase from a contact function. The phase occurs only when the
|
|
210
|
+
# Moon is inside the boundary at greatest eclipse (contact value negative);
|
|
211
|
+
# each contact is then the single crossing between greatest eclipse and the
|
|
212
|
+
# corresponding edge of the window, found by bisection. This is robust to
|
|
213
|
+
# arbitrarily short phases (a barely-total or grazing eclipse), unlike a
|
|
214
|
+
# fixed-resolution scan that can step over a brief crossing.
|
|
215
|
+
def phase_for(greatest_jd, &contact_value)
|
|
216
|
+
value_at = ->(jd) { contact_value.call(geometry_at(jd)) }
|
|
217
|
+
return nil unless value_at.call(greatest_jd).negative?
|
|
218
|
+
|
|
219
|
+
starting = bisect_contact(
|
|
220
|
+
value_at,
|
|
221
|
+
greatest_jd - CONTACT_HALF_WINDOW,
|
|
222
|
+
greatest_jd
|
|
223
|
+
)
|
|
224
|
+
ending = bisect_contact(
|
|
225
|
+
value_at,
|
|
226
|
+
greatest_jd + CONTACT_HALF_WINDOW,
|
|
227
|
+
greatest_jd
|
|
228
|
+
)
|
|
229
|
+
return nil unless starting && ending
|
|
230
|
+
|
|
231
|
+
EclipsePhase.new(
|
|
232
|
+
starting_instant: Instant.from_terrestrial_time(starting),
|
|
233
|
+
ending_instant: Instant.from_terrestrial_time(ending)
|
|
234
|
+
)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Bisects for the single contact between +outside_jd+ (value positive, the
|
|
238
|
+
# Moon outside the boundary) and +inside_jd+ (value negative, at greatest
|
|
239
|
+
# eclipse). Returns nil if the boundary is not crossed within the window.
|
|
240
|
+
def bisect_contact(value_at, outside_jd, inside_jd)
|
|
241
|
+
return nil unless value_at.call(outside_jd).positive?
|
|
242
|
+
|
|
243
|
+
while (inside_jd - outside_jd).abs > CONTACT_TOLERANCE
|
|
244
|
+
midpoint = (outside_jd + inside_jd) / 2.0
|
|
245
|
+
if value_at.call(midpoint).negative?
|
|
246
|
+
inside_jd = midpoint
|
|
247
|
+
else
|
|
248
|
+
outside_jd = midpoint
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
(outside_jd + inside_jd) / 2.0
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Builds the geometry at a Julian Date (TT) from the apparent geocentric
|
|
255
|
+
# positions of the Sun and Moon. Memoised so repeated evaluations during the
|
|
256
|
+
# searches reuse the same computation.
|
|
257
|
+
def geometry_at(jd)
|
|
258
|
+
@geometry_cache[jd] ||= begin
|
|
259
|
+
instant = Instant.from_terrestrial_time(jd)
|
|
260
|
+
moon = Moon.new(ephem: @ephem, instant: instant).apparent
|
|
261
|
+
sun = Sun.new(ephem: @ephem, instant: instant).apparent
|
|
262
|
+
|
|
263
|
+
moon_distance = moon.distance.km
|
|
264
|
+
sun_distance = sun.distance.km
|
|
265
|
+
axis_angle = Math::PI - sun.separation_from(moon).radians
|
|
266
|
+
axial_distance = moon_distance * Math.cos(axis_angle)
|
|
267
|
+
perpendicular_distance = moon_distance * Math.sin(axis_angle)
|
|
268
|
+
|
|
269
|
+
# Danjon enlargement: enlarge Earth's radius before building the cones.
|
|
270
|
+
earth_radius = EARTH_RADIUS_KM * SHADOW_ENLARGEMENT
|
|
271
|
+
umbra_half_angle_tangent = (SUN_RADIUS_KM - earth_radius) / sun_distance
|
|
272
|
+
penumbra_half_angle_tangent =
|
|
273
|
+
(SUN_RADIUS_KM + earth_radius) / sun_distance
|
|
274
|
+
latitude_sign = moon.ecliptic.latitude.degrees.negative? ? -1 : 1
|
|
275
|
+
|
|
276
|
+
Geometry.new(
|
|
277
|
+
axis_distance: perpendicular_distance,
|
|
278
|
+
umbra_radius: earth_radius - axial_distance * umbra_half_angle_tangent,
|
|
279
|
+
penumbra_radius: earth_radius + axial_distance * penumbra_half_angle_tangent,
|
|
280
|
+
gamma: latitude_sign * perpendicular_distance / EARTH_RADIUS_KM
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astronoby
|
|
4
|
+
class Opposition
|
|
5
|
+
# @return [Astronoby::Instant] when the opposition occurs
|
|
6
|
+
attr_reader :instant
|
|
7
|
+
|
|
8
|
+
# @return [Astronoby::Body] the body in opposition with the Sun
|
|
9
|
+
attr_reader :body
|
|
10
|
+
|
|
11
|
+
# @param instant [Astronoby::Instant] when the opposition occurs
|
|
12
|
+
# @param body [Astronoby::Body] the body in opposition with the Sun
|
|
13
|
+
def initialize(instant:, body:)
|
|
14
|
+
@instant = instant
|
|
15
|
+
@body = body
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Holds the rising, transit, and setting times for a single day.
|
|
4
5
|
class RiseTransitSetEvent
|
|
5
|
-
|
|
6
|
+
# @return [Time, nil] the rising time
|
|
7
|
+
attr_reader :rising_time
|
|
6
8
|
|
|
9
|
+
# @return [Time, nil] the transit (culmination) time
|
|
10
|
+
attr_reader :transit_time
|
|
11
|
+
|
|
12
|
+
# @return [Time, nil] the setting time
|
|
13
|
+
attr_reader :setting_time
|
|
14
|
+
|
|
15
|
+
# @param rising [Time, nil] the rising time
|
|
16
|
+
# @param transit [Time, nil] the transit time
|
|
17
|
+
# @param setting [Time, nil] the setting time
|
|
7
18
|
def initialize(rising, transit, setting)
|
|
8
19
|
@rising_time = rising
|
|
9
20
|
@transit_time = transit
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Holds arrays of rising, transit, and setting times over a time range.
|
|
4
5
|
class RiseTransitSetEvents
|
|
5
|
-
|
|
6
|
+
# @return [Array<Time>] rising times
|
|
7
|
+
attr_reader :rising_times
|
|
6
8
|
|
|
9
|
+
# @return [Array<Time>] transit (culmination) times
|
|
10
|
+
attr_reader :transit_times
|
|
11
|
+
|
|
12
|
+
# @return [Array<Time>] setting times
|
|
13
|
+
attr_reader :setting_times
|
|
14
|
+
|
|
15
|
+
# @param risings [Array<Time>] rising times
|
|
16
|
+
# @param transits [Array<Time>] transit times
|
|
17
|
+
# @param settings [Array<Time>] setting times
|
|
7
18
|
def initialize(risings, transits, settings)
|
|
8
19
|
@rising_times = risings
|
|
9
20
|
@transit_times = transits
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Astronoby
|
|
4
|
+
# Holds twilight times (civil, nautical, astronomical) for a single day.
|
|
4
5
|
class TwilightEvent
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
:morning_nautical_twilight_time,
|
|
8
|
-
:evening_nautical_twilight_time,
|
|
9
|
-
:morning_astronomical_twilight_time,
|
|
10
|
-
:evening_astronomical_twilight_time
|
|
6
|
+
# @return [Time, nil] morning civil twilight time
|
|
7
|
+
attr_reader :morning_civil_twilight_time
|
|
11
8
|
|
|
9
|
+
# @return [Time, nil] evening civil twilight time
|
|
10
|
+
attr_reader :evening_civil_twilight_time
|
|
11
|
+
|
|
12
|
+
# @return [Time, nil] morning nautical twilight time
|
|
13
|
+
attr_reader :morning_nautical_twilight_time
|
|
14
|
+
|
|
15
|
+
# @return [Time, nil] evening nautical twilight time
|
|
16
|
+
attr_reader :evening_nautical_twilight_time
|
|
17
|
+
|
|
18
|
+
# @return [Time, nil] morning astronomical twilight time
|
|
19
|
+
attr_reader :morning_astronomical_twilight_time
|
|
20
|
+
|
|
21
|
+
# @return [Time, nil] evening astronomical twilight time
|
|
22
|
+
attr_reader :evening_astronomical_twilight_time
|
|
23
|
+
|
|
24
|
+
# @param morning_civil_twilight_time [Time, nil]
|
|
25
|
+
# @param evening_civil_twilight_time [Time, nil]
|
|
26
|
+
# @param morning_nautical_twilight_time [Time, nil]
|
|
27
|
+
# @param evening_nautical_twilight_time [Time, nil]
|
|
28
|
+
# @param morning_astronomical_twilight_time [Time, nil]
|
|
29
|
+
# @param evening_astronomical_twilight_time [Time, nil]
|
|
12
30
|
def initialize(
|
|
13
31
|
morning_civil_twilight_time: nil,
|
|
14
32
|
evening_civil_twilight_time: nil,
|
|
@@ -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
|
-
|
|
6
|
-
|
|
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,
|