Introduction: Spacecraft Observation Python Code

About: I am an Aerospace Engineer/ Flight Test Engineer that loves to tinker and find fun creative outlets that utilize many different aspects of engineering. I specialized in CAD modeling and 3D printing, but have d…

This project is a python code that when provided with a spacecraft's TLE (to be discussed later) and the observation location, it will output a list of the best opportunities to spot the satellite fly overhead your location.


The code was originally designed as a means to position a telescope in an exact position to take a picture of the satellite. But due to a few rounding errors, I was never able to make that happen. But maybe you will have better luck! (Even if it is a split second opportunity)


This code serves as a baseline to expand upon to your liking/application. I made it very baseline, but provided the Skyfield API website to help assist in wherever this may take you.

Supplies

Python

skyfield.api

A computer with internet access

A dark area (dark enough to see satellites)

Step 1: Background Information

The code functions on a basic idea about how we communicate with satellites. We use something called a "Two-Line Element" or TLE for short. This is a data format which allows the user to understand the orbit and motion of a satellite.


For example, the following TLE is from the International Space Station.

1 25544U 98067A 25242.84749716 .00011470 00000-0 20639-3 0 9997
2 25544 51.6341 297.0246 0003297 294.4566 65.6079 15.50309434526740

There is an easy to read diagnosis of the TLE found at this Reference: Definition of Two-line Element Set Coordinate System


Line 1: General Information

  1. The Name of the satellite is not always included but will typically be a name given to it by scientists or engineers.
  2. The satellite number is the NORAD ID of the object followed be either a U, C, or S. The U meaning it is an unclassified object, Classified, or Secret accordingly.
  3. The international designator is just another way to name the satellite that is used by the world.
  4. The Epoch is the reference date used by the satellite.
  5. The first derivative is also called the ballistic coefficient and it helps to understand the rate of change of the orbit due to drag divided by two. Using the units of revolutions per day.
  6. The second derivative is the acceleration of the drag, or a quantity of how much drag is increasing or decreasing over time. Typically, it is zero. The value provided is the second derivative divided by 6 in the units of revolutions per day cubed.
  7. The drag term, AKA B* Drag Coefficient is just another way of determining atmospheric drag.
  8. The Ephemeris type refers to the specific calculation method used to define the position of celestial bodies. Typically, it is always 0 (in public use TLE data)
  9. The Element Number is how many TLE's have been generated for this object.
  10. The last digit of line 1 is the Check Sum which is just a way to check if there were any errors in the message.

Line 2: Orbit Information

  1. The satellite number repeated for consistency
  2. Inclination: The measure of tilt of an object's orbital pane about a body's equator. [Degrees]
  3. Right Ascension of the Ascending Node (RAAN): The angle between vernal equinox and the point where the orbit crosses the equatorial plane (going north) [Degrees]
  4. Eccentricity: The overall shape of the orbit [Circular (e=0), Elliptical (0<e<1), Parabolic (e=1), Hyperbolic (e>1)
  5. Argument of Perigee: The location in the orbit referenced to the actual orbital plane. [Degrees]
  6. Mean Anomaly: The angle, measured from perigee, of the satellite location in the orbit referenced to a circular orbit with radius equal to the semi-major axis. [Degrees]
  7. Mean Motion: The angular speed for the object to complete one orbit. (1 revolution divided by time to complete one orbit) (revolutions per day)
  8. Revolution number at epoch: The total number of full orbits the object has completed at the specific time (epoch) the orbital elements were calculated.
  9. Check Sum: Error Check for line 2.

The following image reference provides a general overview of line two's terms and meaning. Reference:Spacecraft collision warning orbit calculation method

Now that the background information has been provided as to what we are providing our code, we can proceed to step 2.

But if you are interested in more information about orbits, I recommend doing some research online for NASA's resources and others to get a better idea. It can be as easy or as math-intensive as you prefer.

NASA: https://science.nasa.gov/learn/basics-of-space-flight/chapter5-1/

Step 2: Code Initialization

I am going to assume you have some working knowledge of Python and skip the reason why I include things.


You should start your code with the following:

from skyfield.api import load, wgs84, EarthSatellite

eph = load('de421.bsp')
Location = wgs84.latlon(+28.5227, -80.6820)


Line 2: Load the 'ephemeris' data from JPL, which describes where all the planets and the sun are at all times from 1950 to 2050.

Line 3: Specify coordinates of observation site. The software uses decimal degrees. Positive Latitude is North, Positive Longitude is East. I recommend using as many decimal places as possible to yield better results.

Decimal Degree Conversion:

To get your coordinates, go to your favorite map site (google Maps or Apple Maps, or whatever) and get your exact coordinates. They may present you the coordinates in either deg, min, second, or decimal degrees. We need decimal degrees for the code. Unit conversion is VERY simple.

Example: 28°31'21.8"N 80°40'55.1"W

Decimal Degrees = Degrees + (Minutes/60) + (Seconds/3600)

Decimal Degrees = 28+(31/60)+(21.8/3600) = 28.5227 N = +28.5227

Decimal Degrees = 80+(40/60)+(55.1/3600) = 80.6820 W = -80.6820

Step 3: Satellite TLE

Line 4 of your code:

sat = EarthSatellite("1 27421U 02021A 25242.88310383 .00000861 00000-0 19052-3 0 9995",
"2 27421 98.0290 289.0102 0131675 98.8722 262.7410 14.54370944220644")

How to find a viewable satellite:

There are thousands of satellites that fly above your location on a daily basis. But most of the time, you cannot view them. That begs the question, what makes a satellite viewable?

When a satellite is in orbit, it experiences daylight roughly every 90 minutes (generalization). To view one from your house, that satellite has to be lit up by sunlight. So if you wanted to view a satellite you either have to go outside in the early morning just before sunrise, or about 30 minutes after sunset when it starts to get dark enough to see stars.

A few websites have categorized the amount of sunlight hitting a satellite and relating that to a viewability. We are going to exploit that for this code and make our lives a little simpler.

Using the website "Heavens Above" we can view what satellites are going to pass over on the day of observation.

Step 1: Click "Change your observing Location" on the main page under "Configuration"

https://www.heavens-above.com/

Step 2: Enter your observation location OR use current location based on whatever device you are using

Step 3: Return to the main home page

Step 4: Click "Daily Predictions for brighter satellites"

This will bring up a large table of satellites/objects that will pass overhead in either the morning or evening depending on your selection at the top.

Step 5: Select the date and time (morning/evening) of your observation and related settings

I recommend leaving the default minimum brightness setting at 4.5. Also exclude Starlink passes if you want to see anything other than that. That being said, Starlink are cool when seen in a tight row.

Step 6: Find a satellite/object

You are looking for anything that interests you. Try to find something that has a brightness greater than 4.5 magnitude.

Additionally, take a look at the "highest point" altitude. You want that to be as close to 90 degrees as possible or at least greater than 60 degrees for a good probability of sighting.

You can get more information about the object by clicking on the name.

Step 7: Navigate to "info" about the chosen satellite and copy the Spacetrack catalog number

Step 8: Navigate to https://www.n2yo.com/

Step 9: In the upper right corner of the website, click in the box that says, " Find a satellite" and search the Spacetrack catalog number

This will give you more information about the object/satellite

Step 10: Copy the TLE from the bottom of the page into your code in the format shown above

Now technically, these two websites accomplish what we are coding, but in my opinion is it much cooler to have written a code that tracks a satellite given its TLE data and provides when it will cross our skies, and information about how to position a telescope to have a better (albeit still very small) chance of viewing it for a split second.


Example:

Spot 5

Spacetrack Catalog Number: 27421

1 27421U 02021A 25242.88310383 .00000861 00000-0 19052-3 0 9995
2 27421 98.0290 289.0102 0131675 98.8722 262.7410 14.54370944220644


See the above images for what the websites look like.

Step 4: Rest of the Code

The rest of the code will be shown here. Comments will be added to assist with what each line does.

print(sat) # Confirms TLE was loaded successfully

ts = load.timescale() # Create a Skyfield timescale object for specifying times

search_start = ts.utc(2025, 8, 30) # Specify year, month, day to start our search

search_end = ts.utc(2025, 8, 31) # Search for passes until Year, month, day.

t, events = sat.find_events(Location search_start, search_end, altitude_degrees=30.0)
# Find any 'events', which are risings, culminatings, or settings
# We can specify the threshold at which an 'event' is cued. In this case, the event must
# rise above 30 degrees.

for ti, event in zip(t, events):
name = ('rise above 30°', 'culminate', 'set below 30°')[event]
if event == 1: # Rise above 30 degrees
print(ti.utc_strftime('%Y %b %d %H:%M:%S'), name) # Date and time of event [UTC time]
print((sat - Location).at(ti).altaz())
print("Is the satellite sunlit?: ", sat.at(ti).is_sunlit(eph), "\n") # at time, is it sunlit?
print((sat - Location).at(ti).altaz()) # at time, give us the altitude-azimuth

# These 3 lines will, for each pass, give us an indication of the pass's
# quality. We'll print the location of the highest point the satellite reaches
# in the sky, and we'll print whether the satellite is illuminated.

# This next section summarizes the informatino and provides the max elevation of the pass
t, events = sat.find_events(Location, search_start, search_end, altitude_degrees=30.0)
for ti, event in zip(t, events):
name = ('rise above 30°', 'culminate', 'set below 30°')[event]
if name == "culminate":
tculm = ti
difference = sat - Location
topocentric = difference.at(tculm)
alt, az, distance = topocentric.altaz()
print(ti.utc_strftime('%Y %b %d %H:%M:%S'), name)
print("The Maximum elevation is", alt, az)

To tell if the ground observer is in darkness, you can do it a few ways.

One: Google the sunset time.

Two: Skyfield is actually able to calculate sunrises and sunsets too (just like how it calculated the satellite rising and setting).

That capability could be added to the code, but for simplicity sake, I did not. See https://rhodesmill.org/skyfield/toc.html for more information, code examples, and other things their API is capable of.


Once the code is fully compiled, you should get a output of the following:

catalog #27421 epoch 2025-08-30 21:11:40 UTC
2025 Aug 30 04:50:14 culminate
(<Angle 31deg 07' 15.8">, <Angle 261deg 56' 41.8">, <Distance 7.84565e-06 au>)
Is the satellite sunlit?: True

(<Angle 31deg 07' 15.8">, <Angle 261deg 56' 41.8">, <Distance 7.84565e-06 au>)
2025 Aug 30 16:53:31 culminate
(<Angle 86deg 09' 22.9">, <Angle 102deg 03' 09.2">, <Distance 4.35186e-06 au>)
Is the satellite sunlit?: True

(<Angle 86deg 09' 22.9">, <Angle 102deg 03' 09.2">, <Distance 4.35186e-06 au>)
2025 Aug 30 04:50:14 culminate
The Maximum elevation is 31deg 07' 15.8" 261deg 56' 41.8"
2025 Aug 30 16:53:31 culminate
The Maximum elevation is 86deg 09' 22.9" 102deg 03' 09.2"

Process finished with exit code 0

Each of those are events where the satellite will cross 30 degrees above the horizon from your viewing location. For my viewing, I would choose August 30th at 16:53:31 where it would be almost directly overhead.


Step 5: Full Code

# Collin Duke
# August 31, 2025
# References: Skyfield API https://rhodesmill.org/skyfield/

from skyfield.api import load, wgs84, EarthSatellite

eph = load('de421.bsp')
Location = wgs84.latlon(+35.1306, -118.4530)

sat = EarthSatellite("1 27421U 02021A 25242.88310383 .00000861 00000-0 19052-3 0 9995",
"2 27421 98.0290 289.0102 0131675 98.8722 262.7410 14.54370944220644")
print(sat)

ts = load.timescale()
search_start = ts.utc(2025, 8, 30)
search_end = ts.utc(2025, 8, 31)
t, events = sat.find_events(Location, search_start, search_end, altitude_degrees=30.0)

for ti, event in zip(t, events):
name = ('rise above 30°', 'culminate', 'set below 30°')[event]
if event == 1:
print(ti.utc_strftime('%Y %b %d %H:%M:%S'), name)
print((sat - Location).at(ti).altaz())
print("Is the satellite sunlit?: ", sat.at(ti).is_sunlit(eph), "\n")
print((sat - Location).at(ti).altaz())

t, events = sat.find_events(Location, search_start, search_end, altitude_degrees=30.0)
for ti, event in zip(t, events):
name = ('rise above 30°', 'culminate', 'set below 30°')[event]
if name == "culminate":
tculm = ti
difference = sat - Location
topocentric = difference.at(tculm)
alt, az, distance = topocentric.altaz()
print(ti.utc_strftime('%Y %b %d %H:%M:%S'), name)
print("The Maximum elevation is", alt, az)