Eclipse 2024¶
This tutorial analyzes the total solar eclipse that crossed North America on April 8, 2024. It demonstrates two capabilities:
- Centerline computation: Project the Moon's shadow onto the Earth's surface using JPL ephemerides for the Sun and Moon, accounting for Earth's oblateness
- Eclipse statistics: For any ground location, compute the timing and extent of partial and total eclipse phases by comparing the angular separation of the Sun and Moon against their angular radii
Compute & Plot Centerline of Eclipse Totality¶
The centerline is found by projecting the Moon onto the Earth's surface along the Sun direction. Since the Earth is an oblate spheroid, we scale the z-axis by $1/(1-f)$ where $f$ is the WGS-84 flattening factor, solve for the ray-sphere intersection using the law of cosines, then undo the scaling to recover true ITRF coordinates.
In [1]:
Copied!
import numpy as np
import satkit as sk
import math as m
# Get an array of times over the day
theday = sk.time(2024, 4, 8)
timearr = np.array([theday + sk.duration.from_seconds(i) for i in range(0, 86400, 10)])
# Get the sun position at each time
sun_gcrf = np.array([sk.jplephem.geocentric_pos(sk.solarsystem.Sun, t) for t in timearr])
moon_gcrf = np.array([sk.jplephem.geocentric_pos(sk.solarsystem.Moon, t) for t in timearr])
# Rotate to Earth-fixed ITRF coordinates
qarray = sk.frametransform.qgcrf2itrf(timearr)
sun_itrf = np.array([q*x for q,x in zip(qarray, sun_gcrf)])
moon_itrf = np.array([q*x for q,x in zip(qarray, moon_gcrf)])
# Project the moon to the surface of the Earth along the Sun vector
# This is projecting onto the surface of a sphere. However, the Earth is actually
# an oblate spheroid. We can account for this by scaling the z axis by the flattening
# of the Earth.
scalefac = 1.0 / (1.0 - sk.consts.wgs84_f)
moon_itrf_scaled = moon_itrf
moon_itrf_scaled[:,2] = moon_itrf_scaled[:,2] * scalefac
sun_itrf_scaled = sun_itrf
sun_itrf_scaled[:,2] = sun_itrf_scaled[:,2] * scalefac
sun_itrf_scaled_hat = sun_itrf_scaled / np.linalg.norm(sun_itrf_scaled, axis=1)[:,None]
# Compute the distance to the surface of the Earth. This can be done
# via the law of Cosines and the quadratic equation
lcostheta = np.sum(sun_itrf_scaled_hat * moon_itrf_scaled, axis=1)
sqrtterm = lcostheta**2 - np.sum(moon_itrf_scaled**2, axis=1) + sk.consts.earth_radius**2
# Valid indices (where the projection hits the earth) are where the term under
# the square root is positive
vidx = np.argwhere(sqrtterm > 0).flatten()
# Distance to surface of the Earth along from the moon along the Sun vector
dist = lcostheta[vidx] - np.sqrt(sqrtterm[vidx])
# Now, get the positions on the surface of the Earth
pgnd_itrf_scaled = moon_itrf_scaled[vidx] - dist[:,None] * sun_itrf_scaled_hat[vidx]
# Undo the scaling of the zaxis to account for Earth flattening
pgnd_itrf = pgnd_itrf_scaled
pgnd_itrf[:,2] = pgnd_itrf[:,2] / scalefac
# Get the latitude and longitude of the points on the surface of the Earth
# that follow the center line of totality
coords = [sk.itrfcoord(x) for x in pgnd_itrf]
import numpy as np
import satkit as sk
import math as m
# Get an array of times over the day
theday = sk.time(2024, 4, 8)
timearr = np.array([theday + sk.duration.from_seconds(i) for i in range(0, 86400, 10)])
# Get the sun position at each time
sun_gcrf = np.array([sk.jplephem.geocentric_pos(sk.solarsystem.Sun, t) for t in timearr])
moon_gcrf = np.array([sk.jplephem.geocentric_pos(sk.solarsystem.Moon, t) for t in timearr])
# Rotate to Earth-fixed ITRF coordinates
qarray = sk.frametransform.qgcrf2itrf(timearr)
sun_itrf = np.array([q*x for q,x in zip(qarray, sun_gcrf)])
moon_itrf = np.array([q*x for q,x in zip(qarray, moon_gcrf)])
# Project the moon to the surface of the Earth along the Sun vector
# This is projecting onto the surface of a sphere. However, the Earth is actually
# an oblate spheroid. We can account for this by scaling the z axis by the flattening
# of the Earth.
scalefac = 1.0 / (1.0 - sk.consts.wgs84_f)
moon_itrf_scaled = moon_itrf
moon_itrf_scaled[:,2] = moon_itrf_scaled[:,2] * scalefac
sun_itrf_scaled = sun_itrf
sun_itrf_scaled[:,2] = sun_itrf_scaled[:,2] * scalefac
sun_itrf_scaled_hat = sun_itrf_scaled / np.linalg.norm(sun_itrf_scaled, axis=1)[:,None]
# Compute the distance to the surface of the Earth. This can be done
# via the law of Cosines and the quadratic equation
lcostheta = np.sum(sun_itrf_scaled_hat * moon_itrf_scaled, axis=1)
sqrtterm = lcostheta**2 - np.sum(moon_itrf_scaled**2, axis=1) + sk.consts.earth_radius**2
# Valid indices (where the projection hits the earth) are where the term under
# the square root is positive
vidx = np.argwhere(sqrtterm > 0).flatten()
# Distance to surface of the Earth along from the moon along the Sun vector
dist = lcostheta[vidx] - np.sqrt(sqrtterm[vidx])
# Now, get the positions on the surface of the Earth
pgnd_itrf_scaled = moon_itrf_scaled[vidx] - dist[:,None] * sun_itrf_scaled_hat[vidx]
# Undo the scaling of the zaxis to account for Earth flattening
pgnd_itrf = pgnd_itrf_scaled
pgnd_itrf[:,2] = pgnd_itrf[:,2] / scalefac
# Get the latitude and longitude of the points on the surface of the Earth
# that follow the center line of totality
coords = [sk.itrfcoord(x) for x in pgnd_itrf]
Could not find necessary data files. Install satkit-data package via pypi or Run "satkit.utils.update_datafiles()" to download necessary files This includes JPL ephemeris, gravity, space weather, Earth orientation parameters, leap seconds, and coefficients for inertial-to-Earth-fixed transforms
Downloading JPL Ephemeris file. File size is approx. 100MB
In [2]:
Copied!
import plotly.graph_objects as go
lat, lon = zip(*[(c.latitude_deg, c.longitude_deg) for c in coords])
fig = go.Figure()
fig.add_trace(
go.Scattergeo(
locationmode = 'USA-states',
lon = lon,
lat = lat,
mode = 'lines',
line = dict(width = 2,color = 'red'),
)
)
fig.update_layout(
title_text = 'Centerline of 2024 Solar Eclipse',
showlegend = False,
geo = dict(
projection_type = 'mercator',
resolution=50,
lataxis_range=[20,55],
lonaxis_range=[-140,-40],
showcountries=True,
countrycolor='black',
),
width=650,
height=500,
)
import plotly.graph_objects as go
lat, lon = zip(*[(c.latitude_deg, c.longitude_deg) for c in coords])
fig = go.Figure()
fig.add_trace(
go.Scattergeo(
locationmode = 'USA-states',
lon = lon,
lat = lat,
mode = 'lines',
line = dict(width = 2,color = 'red'),
)
)
fig.update_layout(
title_text = 'Centerline of 2024 Solar Eclipse',
showlegend = False,
geo = dict(
projection_type = 'mercator',
resolution=50,
lataxis_range=[20,55],
lonaxis_range=[-140,-40],
showcountries=True,
countrycolor='black',
),
width=650,
height=500,
)
Eclipse Statistics for Arbitrary Locations¶
For any observer, we compute the angular separation between the Sun and Moon over time and compare it to their angular radii:
- Total eclipse occurs when the separation is less than the difference of the angular radii (Moon fully covers the Sun)
- Partial eclipse occurs when the separation is less than the sum of the angular radii (discs overlap)
- For partial-only locations, the area and diameter fractions of the Sun that are occluded are computed using the geometry of two overlapping circles (lens area via law of cosines)
In [3]:
Copied!
import satkit as sk
import numpy as np
import math as m
# Eclipse happens on April 8, 2024
time0 = sk.time(2024, 4, 8, 12, 0, 0)
timearr = np.array(
time0 + [sk.duration.from_days(x) for x in np.linspace(0, 0.5, 43200)]
)
# Get exact JPL ephemeris for sun & moon
sun_light_travel_time = sk.duration.from_seconds(sk.consts.au / sk.consts.c)
sun_gcrf = sk.jplephem.geocentric_pos(
sk.solarsystem.Sun, timearr - sun_light_travel_time
)
moon_gcrf = sk.jplephem.geocentric_pos(sk.solarsystem.Moon, timearr)
# Rotation to Earth-fixed frame
qitrf2gcrf = sk.frametransform.qitrf2gcrf(timearr)
def eclipse_stats(loc: sk.itrfcoord):
qitrf2ned = loc.qned2itrf.conj
# Location in GCRF
loc_gcrf = np.array([x * loc.vector for x in qitrf2gcrf])
# Compute angle between sun and moon at location
sun_diff = sun_gcrf - loc_gcrf
moon_diff = moon_gcrf - loc_gcrf
sun_norm = np.sqrt(np.sum(sun_diff**2, axis=1))
moon_norm = np.sqrt(np.sum(moon_diff**2, axis=1))
theta = np.arccos(np.sum(sun_diff * moon_diff, axis=1) / sun_norm / moon_norm)
# Compute angular extent of sun & moon
moon_dist = np.mean(moon_norm)
moon_extent_rad = sk.consts.moon_radius / moon_dist
sun_extent_rad = sk.consts.sun_radius / sk.consts.au
# How far off can they be while still having total eclipse?
max_eclipse_offset_rad = moon_extent_rad - sun_extent_rad
idx = np.argwhere(theta == np.min(theta))[0][0]
# Look for times where there is total eclipse
eidx = np.argwhere(theta < max_eclipse_offset_rad)
# Look for times of partial eclipse
pidx = np.argwhere(theta < (sun_extent_rad + moon_extent_rad))
data = {"latitude": loc.latitude_deg, "longitude": loc.longitude_deg}
if len(eidx) > 0:
data["total"] = {
"begin": timearr[eidx[0][0]].as_datetime(), # type: ignore
"end": timearr[eidx[-1][0]].as_datetime(), # type: ignore
"duration_seconds": (timearr[eidx[-1][0]] - timearr[eidx[0][0]]).seconds, # type: ignore
}
data["partial"] = {
"begin": timearr[pidx[0][0]].as_datetime(), # type: ignore
"end": timearr[pidx[-1][0]].as_datetime(), # type: ignore
"duration_seconds": (timearr[pidx[-1][0]] - timearr[pidx[0][0]]).seconds, # type: ignore
"peak": None,
"minangle_deg": None,
"max_area_occlusion": None,
"max_diam_occlusion": None,
}
elif np.min(theta) < (sun_extent_rad + moon_extent_rad):
durp = timearr[pidx[-1]][0] - timearr[pidx[0]][0]
mintheta = np.min(theta)
# Derived via triangles & law of cosines
theta_a = m.acos(
(sun_extent_rad**2 + mintheta**2 - moon_extent_rad**2)
/ (2 * sun_extent_rad * mintheta)
)
theta_b = m.acos(
(moon_extent_rad**2 + mintheta**2 - sun_extent_rad**2)
/ (2 * moon_extent_rad * mintheta)
)
h = sun_extent_rad * m.sin(theta_a)
Lb = h / m.tan(theta_b)
La = h / m.tan(theta_a)
# Area of right side of overlapping "lens"
aright = m.pi * moon_extent_rad**2 * theta_b / m.pi - Lb * h
# Area of left side of overlapping "lens"
aleft = m.pi * sun_extent_rad**2 * theta_a / m.pi - La * h
ashown = m.pi * sun_extent_rad**2 - aright - aleft
max_frac_area_occluded = 1 - ashown / (m.pi * sun_extent_rad**2)
max_frac_diam_occluded = 1 - (sun_extent_rad + mintheta - moon_extent_rad) / (
2 * sun_extent_rad
)
data["partial"] = {
"begin": timearr[pidx[0][0]].datetime(), # type: ignore
"end": timearr[pidx[-1][0]].datetime(), # type: ignore
"peak": timearr[idx].datetime(), # type: ignore
"duration_seconds": (timearr[pidx[-1][0]] - timearr[pidx[0][0]]).seconds, # type: ignore
"minangle_deg": np.min(theta) * 180.0 / m.pi,
"max_area_occlusion": max_frac_area_occluded,
"max_diam_occlusion": max_frac_diam_occluded,
}
data["total"] = None
else:
data["total"] = None
data["partial"] = None
return data
import satkit as sk
import numpy as np
import math as m
# Eclipse happens on April 8, 2024
time0 = sk.time(2024, 4, 8, 12, 0, 0)
timearr = np.array(
time0 + [sk.duration.from_days(x) for x in np.linspace(0, 0.5, 43200)]
)
# Get exact JPL ephemeris for sun & moon
sun_light_travel_time = sk.duration.from_seconds(sk.consts.au / sk.consts.c)
sun_gcrf = sk.jplephem.geocentric_pos(
sk.solarsystem.Sun, timearr - sun_light_travel_time
)
moon_gcrf = sk.jplephem.geocentric_pos(sk.solarsystem.Moon, timearr)
# Rotation to Earth-fixed frame
qitrf2gcrf = sk.frametransform.qitrf2gcrf(timearr)
def eclipse_stats(loc: sk.itrfcoord):
qitrf2ned = loc.qned2itrf.conj
# Location in GCRF
loc_gcrf = np.array([x * loc.vector for x in qitrf2gcrf])
# Compute angle between sun and moon at location
sun_diff = sun_gcrf - loc_gcrf
moon_diff = moon_gcrf - loc_gcrf
sun_norm = np.sqrt(np.sum(sun_diff**2, axis=1))
moon_norm = np.sqrt(np.sum(moon_diff**2, axis=1))
theta = np.arccos(np.sum(sun_diff * moon_diff, axis=1) / sun_norm / moon_norm)
# Compute angular extent of sun & moon
moon_dist = np.mean(moon_norm)
moon_extent_rad = sk.consts.moon_radius / moon_dist
sun_extent_rad = sk.consts.sun_radius / sk.consts.au
# How far off can they be while still having total eclipse?
max_eclipse_offset_rad = moon_extent_rad - sun_extent_rad
idx = np.argwhere(theta == np.min(theta))[0][0]
# Look for times where there is total eclipse
eidx = np.argwhere(theta < max_eclipse_offset_rad)
# Look for times of partial eclipse
pidx = np.argwhere(theta < (sun_extent_rad + moon_extent_rad))
data = {"latitude": loc.latitude_deg, "longitude": loc.longitude_deg}
if len(eidx) > 0:
data["total"] = {
"begin": timearr[eidx[0][0]].as_datetime(), # type: ignore
"end": timearr[eidx[-1][0]].as_datetime(), # type: ignore
"duration_seconds": (timearr[eidx[-1][0]] - timearr[eidx[0][0]]).seconds, # type: ignore
}
data["partial"] = {
"begin": timearr[pidx[0][0]].as_datetime(), # type: ignore
"end": timearr[pidx[-1][0]].as_datetime(), # type: ignore
"duration_seconds": (timearr[pidx[-1][0]] - timearr[pidx[0][0]]).seconds, # type: ignore
"peak": None,
"minangle_deg": None,
"max_area_occlusion": None,
"max_diam_occlusion": None,
}
elif np.min(theta) < (sun_extent_rad + moon_extent_rad):
durp = timearr[pidx[-1]][0] - timearr[pidx[0]][0]
mintheta = np.min(theta)
# Derived via triangles & law of cosines
theta_a = m.acos(
(sun_extent_rad**2 + mintheta**2 - moon_extent_rad**2)
/ (2 * sun_extent_rad * mintheta)
)
theta_b = m.acos(
(moon_extent_rad**2 + mintheta**2 - sun_extent_rad**2)
/ (2 * moon_extent_rad * mintheta)
)
h = sun_extent_rad * m.sin(theta_a)
Lb = h / m.tan(theta_b)
La = h / m.tan(theta_a)
# Area of right side of overlapping "lens"
aright = m.pi * moon_extent_rad**2 * theta_b / m.pi - Lb * h
# Area of left side of overlapping "lens"
aleft = m.pi * sun_extent_rad**2 * theta_a / m.pi - La * h
ashown = m.pi * sun_extent_rad**2 - aright - aleft
max_frac_area_occluded = 1 - ashown / (m.pi * sun_extent_rad**2)
max_frac_diam_occluded = 1 - (sun_extent_rad + mintheta - moon_extent_rad) / (
2 * sun_extent_rad
)
data["partial"] = {
"begin": timearr[pidx[0][0]].datetime(), # type: ignore
"end": timearr[pidx[-1][0]].datetime(), # type: ignore
"peak": timearr[idx].datetime(), # type: ignore
"duration_seconds": (timearr[pidx[-1][0]] - timearr[pidx[0][0]]).seconds, # type: ignore
"minangle_deg": np.min(theta) * 180.0 / m.pi,
"max_area_occlusion": max_frac_area_occluded,
"max_diam_occlusion": max_frac_diam_occluded,
}
data["total"] = None
else:
data["total"] = None
data["partial"] = None
return data
In [4]:
Copied!
# A list of eclipse locations
locations = [
{ "name": "Mexico City", "latitude_deg": 19.4326, "longitude_deg": -99.1332, },
{ "name": "Austin, Tx", "latitude_deg": 30.2672, "longitude_deg": -97.7431, },
{ "name": "Dallas, Tx", "latitude_deg": 32.7767, "longitude_deg": -96.7970, },
{ "name": "St. Louis, Mo", "latitude_deg": 38.6270, "longitude_deg": -90.1994, },
{ "name": "New York City", "latitude_deg": 40.7128, "longitude_deg": -74.0060, },
{ "name": "Boston, MA", "latitude_deg": 42.3601, "longitude_deg": -71.0589, },
{ "name": "Burlington, Vt", "latitude_deg": 44.4759, "longitude_deg": -73.2121, },
{ "name": "Montreal", "latitude_deg": 45.5017, "longitude_deg": -73.5673, },
{ "name": "Quebec City", "latitude_deg": 46.8139, "longitude_deg": -71.2080, },
{ "name": "Halifax", "latitude_deg": 44.6488, "longitude_deg": -63.5752, },
{ "name": "Cleveland, Oh", "latitude_deg": 41.4993, "longitude_deg": -81.6944, },
]
for loc in locations:
loc["coord"] = sk.itrfcoord(latitude_deg=loc["latitude_deg"], longitude_deg=loc["longitude_deg"])
loc["stats"] = eclipse_stats(loc["coord"])
# A list of eclipse locations
locations = [
{ "name": "Mexico City", "latitude_deg": 19.4326, "longitude_deg": -99.1332, },
{ "name": "Austin, Tx", "latitude_deg": 30.2672, "longitude_deg": -97.7431, },
{ "name": "Dallas, Tx", "latitude_deg": 32.7767, "longitude_deg": -96.7970, },
{ "name": "St. Louis, Mo", "latitude_deg": 38.6270, "longitude_deg": -90.1994, },
{ "name": "New York City", "latitude_deg": 40.7128, "longitude_deg": -74.0060, },
{ "name": "Boston, MA", "latitude_deg": 42.3601, "longitude_deg": -71.0589, },
{ "name": "Burlington, Vt", "latitude_deg": 44.4759, "longitude_deg": -73.2121, },
{ "name": "Montreal", "latitude_deg": 45.5017, "longitude_deg": -73.5673, },
{ "name": "Quebec City", "latitude_deg": 46.8139, "longitude_deg": -71.2080, },
{ "name": "Halifax", "latitude_deg": 44.6488, "longitude_deg": -63.5752, },
{ "name": "Cleveland, Oh", "latitude_deg": 41.4993, "longitude_deg": -81.6944, },
]
for loc in locations:
loc["coord"] = sk.itrfcoord(latitude_deg=loc["latitude_deg"], longitude_deg=loc["longitude_deg"])
loc["stats"] = eclipse_stats(loc["coord"])
/tmp/ipykernel_2749/3897109767.py:19: DeprecationWarning: satkit.time.datetime() is deprecated; use satkit.time.as_datetime() instead.
In [5]:
Copied!
# Display stats in a nice table
import pandas as pd
df = pd.DataFrame(
[
{
"Location": loc["name"],
"Total Eclipse Begin": loc["stats"]["total"]["begin"] if loc["stats"]["total"] else None,
"Total Eclipse End": loc["stats"]["total"]["end"] if loc["stats"]["total"] else None,
"Total Eclipse Duration (s)": loc["stats"]["total"]["duration_seconds"] if loc["stats"]["total"] else None,
"Partial Eclipse Begin": loc["stats"]["partial"]["begin"] if loc["stats"]["partial"] else None,
"Partial Eclipse End": loc["stats"]["partial"]["end"] if loc["stats"]["partial"] else None,
"Partial Eclipse Peak": loc["stats"]["partial"]["peak"] if loc["stats"]["partial"] else None,
"Partial Eclipse Duration (s)": loc["stats"]["partial"]["duration_seconds"] if loc["stats"]["partial"] else None,
"Min Separation (deg)": loc["stats"]["partial"]["minangle_deg"] if loc["stats"]["partial"] else None,
"Max Area Occlusion": loc["stats"]["partial"]["max_area_occlusion"] if loc["stats"]["partial"] else None,
"Max Diameter Occlusion": loc["stats"]["partial"]["max_diam_occlusion"] if loc["stats"]["partial"] else None,
}
for loc in locations
]
)
df.style.format({"Total Eclipse Begin": lambda x: x.strftime("%H:%M:%S") if not pd.isnull(x) else 'N/A',
"Total Eclipse End": lambda x: x.strftime("%H:%M:%S") if not pd.isnull(x) else 'N/A',
"Partial Eclipse Begin": lambda x: x.strftime("%H:%M:%S") if not pd.isnull(x) else 'N/A',
"Partial Eclipse End": lambda x: x.strftime("%H:%M:%S") if not pd.isnull(x) else 'N/A',
"Partial Eclipse Peak": lambda x: x.strftime("%H:%M:%S") if not pd.isnull(x) else 'N/A',
"Total Eclipse Duration (s)": lambda x: f"{x:.0f}" if not pd.isnull(x) else 'N/A',
"Partial Eclipse Duration (s)": lambda x: f"{x:.0f}" if not pd.isnull(x) else 'N/A',
"Min Separation (deg)": lambda x: f"{x:.2f}" if not pd.isnull(x) else "N/A",
"Max Area Occlusion": lambda x: f"{x:.2f}" if not pd.isnull(x) else '1.00',
"Max Diameter Occlusion": lambda x: f"{x:.2f}" if not pd.isnull(x) else '1.00',
})
# Display stats in a nice table
import pandas as pd
df = pd.DataFrame(
[
{
"Location": loc["name"],
"Total Eclipse Begin": loc["stats"]["total"]["begin"] if loc["stats"]["total"] else None,
"Total Eclipse End": loc["stats"]["total"]["end"] if loc["stats"]["total"] else None,
"Total Eclipse Duration (s)": loc["stats"]["total"]["duration_seconds"] if loc["stats"]["total"] else None,
"Partial Eclipse Begin": loc["stats"]["partial"]["begin"] if loc["stats"]["partial"] else None,
"Partial Eclipse End": loc["stats"]["partial"]["end"] if loc["stats"]["partial"] else None,
"Partial Eclipse Peak": loc["stats"]["partial"]["peak"] if loc["stats"]["partial"] else None,
"Partial Eclipse Duration (s)": loc["stats"]["partial"]["duration_seconds"] if loc["stats"]["partial"] else None,
"Min Separation (deg)": loc["stats"]["partial"]["minangle_deg"] if loc["stats"]["partial"] else None,
"Max Area Occlusion": loc["stats"]["partial"]["max_area_occlusion"] if loc["stats"]["partial"] else None,
"Max Diameter Occlusion": loc["stats"]["partial"]["max_diam_occlusion"] if loc["stats"]["partial"] else None,
}
for loc in locations
]
)
df.style.format({"Total Eclipse Begin": lambda x: x.strftime("%H:%M:%S") if not pd.isnull(x) else 'N/A',
"Total Eclipse End": lambda x: x.strftime("%H:%M:%S") if not pd.isnull(x) else 'N/A',
"Partial Eclipse Begin": lambda x: x.strftime("%H:%M:%S") if not pd.isnull(x) else 'N/A',
"Partial Eclipse End": lambda x: x.strftime("%H:%M:%S") if not pd.isnull(x) else 'N/A',
"Partial Eclipse Peak": lambda x: x.strftime("%H:%M:%S") if not pd.isnull(x) else 'N/A',
"Total Eclipse Duration (s)": lambda x: f"{x:.0f}" if not pd.isnull(x) else 'N/A',
"Partial Eclipse Duration (s)": lambda x: f"{x:.0f}" if not pd.isnull(x) else 'N/A',
"Min Separation (deg)": lambda x: f"{x:.2f}" if not pd.isnull(x) else "N/A",
"Max Area Occlusion": lambda x: f"{x:.2f}" if not pd.isnull(x) else '1.00',
"Max Diameter Occlusion": lambda x: f"{x:.2f}" if not pd.isnull(x) else '1.00',
})
Out[5]:
| Location | Total Eclipse Begin | Total Eclipse End | Total Eclipse Duration (s) | Partial Eclipse Begin | Partial Eclipse End | Partial Eclipse Peak | Partial Eclipse Duration (s) | Min Separation (deg) | Max Area Occlusion | Max Diameter Occlusion | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Mexico City | N/A | N/A | N/A | 16:55:32 | 19:36:12 | 18:14:15 | 9640 | 0.13 | 0.74 | 0.79 |
| 1 | Austin, Tx | N/A | N/A | N/A | 17:17:23 | 19:57:55 | 18:37:00 | 9632 | 0.01 | 1.00 | 1.00 |
| 2 | Dallas, Tx | 18:40:57 | 18:44:16 | 199 | 17:23:28 | 20:02:30 | N/A | 9542 | N/A | 1.00 | 1.00 |
| 3 | St. Louis, Mo | N/A | N/A | N/A | 17:43:08 | 20:17:15 | 19:00:48 | 9247 | 0.02 | 0.99 | 0.99 |
| 4 | New York City | N/A | N/A | N/A | 18:10:44 | 20:36:25 | 19:25:34 | 8741 | 0.06 | 0.90 | 0.91 |
| 5 | Boston, MA | N/A | N/A | N/A | 18:16:14 | 20:39:10 | 19:29:47 | 8576 | 0.05 | 0.92 | 0.93 |
| 6 | Burlington, Vt | 19:26:12 | 19:29:15 | 183 | 18:14:22 | 20:37:21 | N/A | 8579 | N/A | 1.00 | 1.00 |
| 7 | Montreal | 19:27:03 | 19:27:59 | 56 | 18:14:35 | 20:36:52 | N/A | 8537 | N/A | 1.00 | 1.00 |
| 8 | Quebec City | N/A | N/A | N/A | 18:18:27 | 20:38:24 | 19:30:15 | 8397 | 0.02 | 0.99 | 0.98 |
| 9 | Halifax | N/A | N/A | N/A | 18:27:26 | 20:44:13 | 19:38:02 | 8207 | 0.04 | 0.94 | 0.95 |
| 10 | Cleveland, Oh | 19:13:52 | 19:17:24 | 212 | 17:59:31 | 20:28:57 | N/A | 8966 | N/A | 1.00 | 1.00 |