mirror of
https://github.com/cosinekitty/astronomy.git
synced 2026-05-19 14:27:52 -04:00
Python: Implemented GlobalSolarEclipse.
This commit is contained in:
@@ -6598,34 +6598,55 @@ class EclipseKind(enum.Enum):
|
||||
Total = 4
|
||||
|
||||
|
||||
class _EarthShadowInfo:
|
||||
def __init__(self, time, u, r, k, p):
|
||||
class _ShadowInfo:
|
||||
def __init__(self, time, u, r, k, p, target, dir):
|
||||
self.time = time
|
||||
self.u = u # dot product of (heliocentric earth) and (geocentric moon): defines the shadow plane where the Moon is
|
||||
self.r = r # km distance between center of Moon and the line passing through the centers of the Sun and Earth.
|
||||
self.k = k # umbra radius in km, at the shadow plane
|
||||
self.p = p # penumbra radius in km, at the shadow plane
|
||||
self.target = target # vector from center of shadow-casting body to target location that might receive the shadow
|
||||
self.dir = dir # vector from center of Sun to center of shadow-casting body
|
||||
|
||||
|
||||
def _CalcShadow(body_radius_km, time, target, dir):
|
||||
u = (dir.x*target.x + dir.y*target.y + dir.z*target.z) / (dir.x*dir.x + dir.y*dir.y + dir.z*dir.z)
|
||||
dx = (u * dir.x) - target.x
|
||||
dy = (u * dir.y) - target.y
|
||||
dz = (u * dir.z) - target.z
|
||||
r = _KM_PER_AU * math.sqrt(dx*dx + dy*dy + dz*dz)
|
||||
k = +_SUN_RADIUS_KM - (1.0 + u)*(_SUN_RADIUS_KM - body_radius_km)
|
||||
p = -_SUN_RADIUS_KM + (1.0 + u)*(_SUN_RADIUS_KM + body_radius_km)
|
||||
return _ShadowInfo(time, u, r, k, p, target, dir)
|
||||
|
||||
|
||||
def _EarthShadow(time):
|
||||
e = _CalcEarth(time)
|
||||
m = GeoMoon(time)
|
||||
u = (e.x*m.x + e.y*m.y + e.z*m.z) / (e.x*e.x + e.y*e.y + e.z*e.z)
|
||||
dx = (u * e.x) - m.x
|
||||
dy = (u * e.y) - m.y
|
||||
dz = (u * e.z) - m.z
|
||||
r = _KM_PER_AU * math.sqrt(dx*dx + dy*dy + dz*dz)
|
||||
k = +_SUN_RADIUS_KM - (1.0 + u)*(_SUN_RADIUS_KM - _EARTH_ECLIPSE_RADIUS_KM)
|
||||
p = -_SUN_RADIUS_KM + (1.0 + u)*(_SUN_RADIUS_KM + _EARTH_ECLIPSE_RADIUS_KM)
|
||||
return _EarthShadowInfo(time, u, r, k, p)
|
||||
return _CalcShadow(_EARTH_ECLIPSE_RADIUS_KM, time, m, e)
|
||||
|
||||
|
||||
def _EarthShadowSlope(context, time):
|
||||
def _MoonShadow(time):
|
||||
# This is a variation on the logic in _EarthShadow().
|
||||
# Instead of a heliocentric Earth and a geocentric Moon,
|
||||
# we want a heliocentric Moon and a lunacentric Earth.
|
||||
h = _CalcEarth(time) # heliocentric Earth
|
||||
m = GeoMoon(time) # geocentric Moon
|
||||
# Calculate lunacentric Earth.
|
||||
e = Vector(-m.x, -m.y, -m.z, m.t)
|
||||
# Convert geocentric moon to heliocentric Moon.
|
||||
m.x += h.x
|
||||
m.y += h.y
|
||||
m.z += h.z
|
||||
return _CalcShadow(_MOON_MEAN_RADIUS_KM, time, e, m)
|
||||
|
||||
|
||||
def _ShadowDistanceSlope(shadowfunc, time):
|
||||
dt = 1.0 / 86400.0
|
||||
t1 = time.AddDays(-dt)
|
||||
t2 = time.AddDays(+dt)
|
||||
shadow1 = _EarthShadow(t1)
|
||||
shadow2 = _EarthShadow(t2)
|
||||
shadow1 = shadowfunc(t1)
|
||||
shadow2 = shadowfunc(t2)
|
||||
return (shadow2.r - shadow1.r) / dt
|
||||
|
||||
|
||||
@@ -6633,9 +6654,17 @@ def _PeakEarthShadow(search_center_time):
|
||||
window = 0.03 # initial search window, in days, before/after given time
|
||||
t1 = search_center_time.AddDays(-window)
|
||||
t2 = search_center_time.AddDays(+window)
|
||||
tx = Search(_EarthShadowSlope, None, t1, t2, 1.0)
|
||||
tx = Search(_ShadowDistanceSlope, _EarthShadow, t1, t2, 1.0)
|
||||
return _EarthShadow(tx)
|
||||
|
||||
|
||||
def _PeakMoonShadow(search_center_time):
|
||||
window = 0.03 # initial search window, in days, before/after given time
|
||||
t1 = search_center_time.AddDays(-window)
|
||||
t2 = search_center_time.AddDays(+window)
|
||||
tx = Search(_ShadowDistanceSlope, _MoonShadow, t1, t2, 1.0)
|
||||
return _MoonShadow(tx)
|
||||
|
||||
class _ShadowDiffContext:
|
||||
def __init__(self, radius_limit, direction):
|
||||
self.radius_limit = radius_limit
|
||||
@@ -6688,6 +6717,148 @@ class LunarEclipseInfo:
|
||||
self.sd_total = sd_total
|
||||
|
||||
|
||||
class GlobalSolarEclipseInfo:
|
||||
"""Reports the time and geographic location of the peak of a solar eclipse.
|
||||
|
||||
Returned by #SearchGlobalSolarEclipse or #NextGlobalSolarEclipse
|
||||
to report information about a solar eclipse event.
|
||||
|
||||
Field `peak` holds the date and time of the peak of the eclipse, defined as
|
||||
the instant when the axis of the Moon's shadow cone passes closest to the Earth's center.
|
||||
|
||||
The eclipse is classified as partial, annular, or total, depending on the
|
||||
maximum amount of the Sun's disc obscured, as seen at the peak location
|
||||
on the surface of the Earth.
|
||||
|
||||
The `kind` field thus holds `EclipseKind.Partial`, `EclipseKind.Annular`, or `EclipseKind.Total`.
|
||||
A total eclipse is when the peak observer sees the Sun completely blocked by the Moon.
|
||||
An annular eclipse is like a total eclipse, but the Moon is too far from the Earth's surface
|
||||
to completely block the Sun; instead, the Sun takes on a ring-shaped appearance.
|
||||
A partial eclipse is when the Moon blocks part of the Sun's disc, but nobody on the Earth
|
||||
observes either a total or annular eclipse.
|
||||
|
||||
If `kind` is `EclipseKind.Total` or `EclipseKind.Annular`, the `latitude` and `longitude`
|
||||
fields give the geographic coordinates of the center of the Moon's shadow projected
|
||||
onto the daytime side of the Earth at the instant of the eclipse's peak.
|
||||
If `kind` has any other value, `latitude` and `longitude` are undefined and should
|
||||
not be used.
|
||||
"""
|
||||
def __init__(self, kind, peak, distance, latitude, longitude):
|
||||
self.kind = kind
|
||||
self.peak = peak
|
||||
self.distance = distance
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
|
||||
|
||||
def _EclipseKindFromUmbra(k):
|
||||
# The umbra radius tells us what kind of eclipse the observer sees.
|
||||
# If the umbra radius is positive, this is a total eclipse. Otherwise, it's annular.
|
||||
# HACK: I added a tiny bias (14 meters) to match Espenak test data.
|
||||
if k > 0.014:
|
||||
return EclipseKind.Total
|
||||
return EclipseKind.Annular
|
||||
|
||||
|
||||
def _GeoidIntersect(shadow):
|
||||
#astro_global_solar_eclipse_t eclipse;
|
||||
#astro_rotation_t rot, inv;
|
||||
#astro_vector_t v, e, o;
|
||||
#shadow_t surface;
|
||||
#double A, B, C, radic, u, R;
|
||||
#double px, py, pz, proj;
|
||||
#double gast;
|
||||
|
||||
kind = EclipseKind.Partial
|
||||
peak = shadow.time
|
||||
distance = shadow.r
|
||||
latitude = longitude = math.nan
|
||||
|
||||
# We want to calculate the intersection of the shadow axis with the Earth's geoid.
|
||||
# First we must convert EQJ (equator of J2000) coordinates to EQD (equator of date)
|
||||
# coordinates that are perfectly aligned with the Earth's equator at this
|
||||
# moment in time.
|
||||
rot = Rotation_EQJ_EQD(shadow.time)
|
||||
v = RotateVector(rot, shadow.dir) # shadow-axis vector in equator-of-date coordinates
|
||||
e = RotateVector(rot, shadow.target) # lunacentric Earth in equator-of-date coordinates
|
||||
|
||||
# Convert all distances from AU to km.
|
||||
# But dilate the z-coordinates so that the Earth becomes a perfect sphere.
|
||||
# Then find the intersection of the vector with the sphere.
|
||||
# See p 184 in Montenbruck & Pfleger's "Astronomy on the Personal Computer", second edition.
|
||||
v.x *= _KM_PER_AU
|
||||
v.y *= _KM_PER_AU
|
||||
v.z *= _KM_PER_AU / _EARTH_FLATTENING
|
||||
e.x *= _KM_PER_AU
|
||||
e.y *= _KM_PER_AU
|
||||
e.z *= _KM_PER_AU / _EARTH_FLATTENING
|
||||
|
||||
# Solve the quadratic equation that finds whether and where
|
||||
# the shadow axis intersects with the Earth in the dilated coordinate system.
|
||||
R = _EARTH_EQUATORIAL_RADIUS_KM
|
||||
A = v.x*v.x + v.y*v.y + v.z*v.z
|
||||
B = -2.0 * (v.x*e.x + v.y*e.y + v.z*e.z)
|
||||
C = (e.x*e.x + e.y*e.y + e.z*e.z) - R*R
|
||||
radic = B*B - 4*A*C
|
||||
|
||||
if radic > 0.0:
|
||||
# Calculate the closer of the two intersection points.
|
||||
# This will be on the day side of the Earth.
|
||||
u = (-B - math.sqrt(radic)) / (2 * A)
|
||||
|
||||
# Convert lunacentric dilated coordinates to geocentric coordinates.
|
||||
px = u*v.x - e.x
|
||||
py = u*v.y - e.y
|
||||
pz = (u*v.z - e.z) * _EARTH_FLATTENING
|
||||
|
||||
# Convert cartesian coordinates into geodetic latitude/longitude.
|
||||
proj = math.sqrt(px*px + py*py) * (_EARTH_FLATTENING * _EARTH_FLATTENING)
|
||||
if proj == 0.0:
|
||||
if pz > 0.0:
|
||||
latitude = +90.0
|
||||
else:
|
||||
latitude = -90.0
|
||||
else:
|
||||
latitude = math.degrees(math.atan(pz / proj))
|
||||
|
||||
# Adjust longitude for Earth's rotation at the given UT.
|
||||
gast = _sidereal_time(peak)
|
||||
longitude = math.fmod(math.degrees(math.atan2(py, px)) - (15*gast), 360.0)
|
||||
if longitude <= -180.0:
|
||||
longitude += 360.0
|
||||
elif longitude > +180.0:
|
||||
longitude -= 360.0
|
||||
|
||||
# We want to determine whether the observer sees a total eclipse or an annular eclipse.
|
||||
# We need to perform a series of vector calculations...
|
||||
# Calculate the inverse rotation matrix, so we can convert EQD to EQJ.
|
||||
inv = InverseRotation(rot)
|
||||
|
||||
# Put the EQD geocentric coordinates of the observer into the vector 'o'.
|
||||
# Also convert back from kilometers to astronomical units.
|
||||
o = Vector(px / _KM_PER_AU, py / _KM_PER_AU, pz / _KM_PER_AU, shadow.time)
|
||||
|
||||
# Rotate the observer's geocentric EQD back to the EQJ system.
|
||||
o = RotateVector(inv, o)
|
||||
|
||||
# Convert geocentric vector to lunacentric vector.
|
||||
o.x += shadow.target.x
|
||||
o.y += shadow.target.y
|
||||
o.z += shadow.target.z
|
||||
|
||||
# Recalculate the shadow using a vector from the Moon's center toward the observer.
|
||||
surface = _CalcShadow(_MOON_POLAR_RADIUS_KM, shadow.time, o, shadow.dir)
|
||||
|
||||
# If we did everything right, the shadow distance should be very close to zero.
|
||||
# That's because we already determined the observer 'o' is on the shadow axis!
|
||||
if surface.r > 1.0e-9 or surface.r < 0.0:
|
||||
raise Error('Unexpected shadow distance from geoid intersection = {}'.format(surface.r))
|
||||
|
||||
kind = _EclipseKindFromUmbra(surface.k)
|
||||
|
||||
return GlobalSolarEclipseInfo(kind, peak, distance, latitude, longitude)
|
||||
|
||||
|
||||
def _ShadowSemiDurationMinutes(center_time, radius_limit, window_minutes):
|
||||
# Search backwards and forwards from the center time until shadow axis distance crosses radius limit.
|
||||
window = window_minutes / (24.0 * 60.0)
|
||||
@@ -6780,4 +6951,73 @@ def NextLunarEclipse(prevEclipseTime):
|
||||
LunarEclipseInfo
|
||||
"""
|
||||
startTime = prevEclipseTime.AddDays(10.0)
|
||||
return SearchLunarEclipse(startTime)
|
||||
return SearchLunarEclipse(startTime)
|
||||
|
||||
|
||||
def SearchGlobalSolarEclipse(startTime):
|
||||
"""Searches for a solar eclipse visible anywhere on the Earth's surface.
|
||||
|
||||
This function finds the first solar eclipse that occurs after `startTime`.
|
||||
A solar eclipse found may be partial, annular, or total.
|
||||
See #GlobalSolarEclipseInfo for more information.
|
||||
To find a series of solar eclipses, call this function once,
|
||||
then keep calling #NextGlobalSolarEclipse as many times as desired,
|
||||
passing in the `peak` value returned from the previous call.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
startTime : Time
|
||||
The date and time for starting the search for a solar eclipse.
|
||||
|
||||
Returns
|
||||
-------
|
||||
GlobalSolarEclipseInfo
|
||||
"""
|
||||
PruneLatitude = 1.8 # Moon's ecliptic latitude beyond which eclipse is impossible
|
||||
# Iterate through consecutive new moons until we find a solar eclipse visible somewhere on Earth.
|
||||
nmtime = startTime
|
||||
for nmcount in range(12):
|
||||
# Search for the next new moon. Any eclipse will be near it.
|
||||
newmoon = SearchMoonPhase(0.0, nmtime, 40.0)
|
||||
if newmoon is None:
|
||||
raise Error('Cannot find new moon')
|
||||
|
||||
# Pruning: if the new moon's ecliptic latitude is too large, a solar eclipse is not possible.
|
||||
mp = _CalcMoon(newmoon)
|
||||
if math.degrees(abs(mp.geo_eclip_lat)) < PruneLatitude:
|
||||
# Search near the new moon for the time when the center of the Earth
|
||||
# is closest to the line passing through the centers of the Sun and Moon.
|
||||
shadow = _PeakMoonShadow(newmoon)
|
||||
if shadow.r < shadow.p + _EARTH_MEAN_RADIUS_KM:
|
||||
# This is at least a partial solar eclipse visible somewhere on Earth.
|
||||
# Try to find an intersection between the shadow axis and the Earth's oblate geoid.
|
||||
return _GeoidIntersect(shadow)
|
||||
|
||||
# We didn't find an eclipse on this new moon, so search for the next one.
|
||||
nmtime = newmoon.AddDays(10.0)
|
||||
|
||||
# Safety valve to prevent infinite loop.
|
||||
# This should never happen, because at least 2 solar eclipses happen per year.
|
||||
raise Error('Failed to find solar eclipse within 12 full moons.')
|
||||
|
||||
|
||||
def NextGlobalSolarEclipse(prevEclipseTime):
|
||||
"""Searches for the next global solar eclipse in a series.
|
||||
|
||||
After using #SearchGlobalSolarEclipse to find the first solar eclipse
|
||||
in a series, you can call this function to find the next consecutive solar eclipse.
|
||||
Pass in the `peak` value from the #GlobalSolarEclipseInfo returned by the
|
||||
previous call to `SearchGlobalSolarEclipse` or `NextGlobalSolarEclipse`
|
||||
to find the next solar eclipse.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prevEclipseTime : Time
|
||||
A date and time near a new moon. Solar eclipse search will start at the next new moon.
|
||||
|
||||
Returns
|
||||
-------
|
||||
GlobalSolarEclipseInfo
|
||||
"""
|
||||
startTime = prevEclipseTime.AddDays(10.0)
|
||||
return SearchGlobalSolarEclipse(startTime)
|
||||
|
||||
Reference in New Issue
Block a user