Note
Go to the end to download the full example code.
Get SUVI data from NODD at a specific cadence
- Purpose:
Download SUVI L1b data at a specific cadence from the NOAA Open Data Dissemination (NODD) AWS S3 buckets using the boto3 library.
The NODD AWS S3 buckets contain almost all SUVI L1b data for GOES-16 through GOES-19 (some early mission data is not available). The data is publicly available at s3://noaa-goes19/ (replace the 19 with 16, 17, or 18 for the earlier missions).
The script below is intended to download data in a specific wavelength channel at a requested cadence. For example, using this script, you could download a SUVI 171 Å file from GOES-19 at a cadence of one hour, instead of having to download all data on the S3 bucket and discarding what you don’t need.
How this script operates: the code will create reference timestamps in the requested period and at the requested cadence, and then look for the file that is closest to each reference timestamp. If a file is found within the defined tolerance, it is accepted, otherwise rejected.
Let’s first import the necessary Python libraries. The “UNSIGNED” and “Config” from botocore are imported so we don’t need a specific username/token for the NODD S3 buckets.
__author__ = "cbethge"
import datetime
import numpy as np
import os
import boto3
from botocore import UNSIGNED
from botocore.config import Config
Now we define the start and end of the period that the code is supposed to consider. The format for those dates is YYYY-MM-DDThh:mm:ss (ISOT without the milliseconds).
start_date = '2026-05-15T12:20:00'
end_date = '2026-05-15T23:30:00'
Note
The code has to parse all files in the requested period! A period of about one month can be handled, but it will be fairly slow. Anything beyond about one month should be broken up into one-month chunks, e.g. with an enclosing for-loop.
Here are some pretty self-explanatory settings: which satellite to get the data from (16, 17, 18, or 19), which wavelength (94, 131, 171, 195, 284, or 304), which file ending (.fits for FITS files or .nc for netCDF), where to download the data to, the cadence in minutes, and the tolerance in minutes.
satellite = 19 # which GOES satellite (16, 17, 18, 19)
wavelength = 171 # which SUVI wavelength channel (94, 131, 171, 195, 284, 304)
file_ending = '.fits' # which file type (.fits or .nc)
outpath = './' # where to download the data
cadence = 60. # requested cadence in minutes
tolerance = 4. # tolerance in minutes
How the tolerance works: in this example script, we requested 171 Å data from GOES-19 at a cadence of one hour, starting at ‘2026-05-15T12:20:00’. The reference timestamps created in this case would be:
Now on to some more settings, they are all Boolean (True or False). The first one is:
enforce_requested_period = False
The second True/False setting is:
short_exp = False
By default, the code will download SUVI L1b long exposures. If short_exp is set to True, short exposures will be downloaded instead. Note that for the 94 and 131 Å channels, the code cannot distinguish between SUVI short exposures and SUVI short flare exposures based on their filenames, so it will download whichever of the two is closest in time to the reference timestamp in this case!
Moving on with the True/False settings:
no_sub_dirs = False
By default, the code will create subdirectories for satellite/year/month/day/wavelength/. If no_sub_dirs is set to True, no subdirectories will be created and the files are downloaded directly to the “outpath” folder defined above.
And finally:
dryrun = True
If dryrun is set to True, the filenames of the accepted files will only be printed out, but the files will not be downloaded. This is the default setting for this website example script.
Now that we are done with the settings, we do some really basic sanity checks. First, we define a function to make sure the submitted dates are in the right format:
def is_valid_date_string(date_string):
# Defining the format from above for datetime
date_format = "%Y-%m-%dT%H:%M:%S"
try:
# Attempt to parse the string
datetime.datetime.strptime(date_string, date_format)
return True
except ValueError:
# If a ValueError is raised, the format or the date itself is invalid
return False
Check that the settings from above are valid inputs:
valid_satellites = [16, 17, 18, 19]
assert satellite in valid_satellites, f'satellite must be in {valid_satellites}'
valid_wavelengths = [94, 131, 171, 195, 284, 304]
assert wavelength in valid_wavelengths, f'wavelength must be in {valid_wavelengths}'
assert is_valid_date_string(start_date), 'Start date must have the form YYYY-MM-DDThh:mm:ss'
syear = int(start_date[0:4])
smonth = int(start_date[5:7])
sday = int(start_date[8:10])
shour = int(start_date[11:13])
sminute = int(start_date[14:16])
ssecond = int(start_date[17:19])
assert syear >= 2019, 'Start year must be >= 2017.'
assert 1 <= smonth <= 12, 'Start month must be between 1 and 12.'
assert 1 <= sday <= 31, 'Start day must be between 1 and 31.'
assert 0 <= shour <= 23, 'Start hour must be between 0 and 23.'
assert 0 <= sminute <= 59, 'Start minute must be between 0 and 59.'
assert 0 <= ssecond <= 59, 'Start second must be between 0 and 59.'
start_date = datetime.datetime.fromisoformat(start_date).replace(tzinfo=datetime.timezone.utc)
assert is_valid_date_string(end_date), 'End date must have the form YYYY-MM-DDThh:mm:ss'
eyear = int(end_date[0:4])
emonth = int(end_date[5:7])
eday = int(end_date[8:10])
ehour = int(end_date[11:13])
eminute = int(end_date[14:16])
esecond = int(end_date[17:19])
assert eyear >= 2017, 'End year must be >= 2017.'
assert 1 <= emonth <= 12, 'End month must be between 1 and 12.'
assert 1 <= eday <= 31, 'End day must be between 1 and 31.'
assert 0 <= ehour <= 23, 'End hour must be between 0 and 23.'
assert 0 <= eminute <= 59, 'End minute must be between 0 and 59.'
assert 0 <= esecond <= 59, 'End second must be between 0 and 59.'
end_date = datetime.datetime.fromisoformat(end_date).replace(tzinfo=datetime.timezone.utc)
assert (end_date-start_date).total_seconds() > 0, 'End date must be after start date.'
The main algorithm:
# Start processing...
satellite_name = 'G'+str(satellite)
satellite_name_long = 'goes'+str(satellite)
# Initialize connection with S3 bucket
suvi_session = boto3.session.Session()
s3 = boto3.resource('s3', config=Config(signature_version=UNSIGNED))
suvi_bucket = s3.Bucket('noaa-'+satellite_name_long)
# These are the SUVI wavelength subfolders on the NODD S3 buckets
wl_folders = { 94: 'SUVI-L1b-Fe093', 131: 'SUVI-L1b-Fe131', 171: 'SUVI-L1b-Fe171',
195: 'SUVI-L1b-Fe195', 284: 'SUVI-L1b-Fe284', 304: 'SUVI-L1b-He303'}
# We have to parse through all days being considered to make a list of all
# available files on the S3
print('Making list of files on S3 bucket...')
tmp_valid_files = []
current_date = start_date
while current_date <= end_date:
date = current_date.strftime("%Y%m%d")
day_of_year = current_date.strftime('%j')
year = int(date[0:4])
month = int(date[4:6])
day = int(date[6:8])
print('Checking', date, 'on S3 bucket...')
# Check if there is data on the server for the requested day.
# If not, skip this day.
number_of_obj = 0
# Need to loop through the hours for the NODD S3 buckets
for h in range(0,24):
hstr = '{:02d}'.format(h)
this_prefix = wl_folders[wavelength]+'/'+'{:04d}'.format(year)+'/'+day_of_year+'/'+hstr+'/'
for obj in suvi_bucket.objects.filter(Prefix=this_prefix).all():
if obj.key.endswith(file_ending):
number_of_obj += 1
tmp_valid_files.append(obj.key)
if number_of_obj == 0:
warn_msg = f'No files found on S3 bucket for {date}. Skipping...'
print(warn_msg)
current_date = current_date + datetime.timedelta(days=1)
continue
current_date = current_date + datetime.timedelta(days=1)
# Get the start and end times from the filenames on the S3 bucket
valid_files = []
if len(tmp_valid_files) == 0:
err_msg = 'No valid files could be selected.'
raise Exception(err_msg)
else:
print('Found', str(len(tmp_valid_files)), 'valid files on the S3.')
tmp_start_times = []
tmp_end_times = []
for tmp_file in tmp_valid_files:
file_base = os.path.basename(tmp_file)
file_base_split = file_base.split('_')
stime = file_base_split[3][1:]
etime = file_base_split[4][1:]
tmp_start_time = stime[0:4]+':'+stime[4:7]+':'+stime[7:9]+':'+stime[9:11]+':'+stime[11:13]
tmp_end_time = etime[0:4]+':'+etime[4:7]+':'+etime[7:9]+':'+etime[9:11]+':'+etime[11:13]
# Exclude files outside of the requested period if requested:
if enforce_requested_period:
if ((datetime.datetime.strptime(tmp_start_time, "%Y:%j:%H:%M:%S").replace(tzinfo=datetime.timezone.utc)-start_date).total_seconds() >= 0) and \
((datetime.datetime.strptime(tmp_end_time, "%Y:%j:%H:%M:%S").replace(tzinfo=datetime.timezone.utc)-end_date).total_seconds() <= 0):
valid_files.append(tmp_file)
tmp_start_times.append(datetime.datetime.strptime(tmp_start_time, "%Y:%j:%H:%M:%S").replace(tzinfo=datetime.timezone.utc))
tmp_end_times.append(datetime.datetime.strptime(tmp_end_time, "%Y:%j:%H:%M:%S").replace(tzinfo=datetime.timezone.utc))
else:
valid_files.append(tmp_file)
tmp_start_times.append(datetime.datetime.strptime(tmp_start_time, "%Y:%j:%H:%M:%S").replace(tzinfo=datetime.timezone.utc))
tmp_end_times.append(datetime.datetime.strptime(tmp_end_time, "%Y:%j:%H:%M:%S").replace(tzinfo=datetime.timezone.utc))
if len(valid_files) == 0:
err_msg = 'No valid files found on the NODD S3 buckets.'
raise Exception(err_msg)
# Make lists of start times, end times, and exposure times
start_time = []
end_time = []
exposure_time = []
for counter, tmp_start_time in enumerate(tmp_start_times):
start_time.append(tmp_start_time)
end_time.append(tmp_end_times[counter])
exposure_time.append((tmp_end_times[counter]-tmp_start_time).total_seconds())
exposure_time = np.array(exposure_time)
# Select long or short exposures, depending on what was requested
if short_exp:
chosen_exposures = np.where(np.around(exposure_time) == 0.)
else:
chosen_exposures = np.where(np.around(exposure_time) == 1.)
chosen_exposure_files = list(np.array(valid_files)[chosen_exposures])
# Make the reference timestamps at the requested cadence
reference_timestamps = []
current_timestamp = start_date
while current_timestamp <= end_date:
reference_timestamps.append(current_timestamp)
current_timestamp = current_timestamp + datetime.timedelta(minutes=cadence)
selected_files = []
for ttime in reference_timestamps:
delta_t = np.zeros(len(chosen_exposures[0]))
for i in range(0,len(chosen_exposures[0])):
delta_t[i] = (ttime-start_time[chosen_exposures[0][i]]).total_seconds()
# Only accept files within the given tolerance:
if (np.abs(delta_t).min()/60.) <= tolerance:
which_file = np.abs(delta_t).argmin()
# Avoid adding the same file multiple times
if chosen_exposure_files[which_file] not in selected_files:
selected_files.append(chosen_exposure_files[which_file])
if len(selected_files) == 0:
err_msg = 'No valid files could be selected.'
raise Exception(err_msg)
else:
if no_sub_dirs:
store_loc = outpath
else:
store_loc = outpath+satellite_name_long+'/'+date[0:4]+'/'+date[4:6]+'/'+'/'+date[6:8]+'/'+str(wavelength)+'/'
if not dryrun:
# Create folders for downloaded data if they do not exist
if not os.path.exists(store_loc):
os.makedirs(store_loc)
for sf in selected_files:
print('Downloading', os.path.basename(sf))
if not dryrun:
suvi_bucket.download_file(sf, store_loc+os.path.basename(sf))
Making list of files on S3 bucket...
Checking 20260515 on S3 bucket...
Found 720 valid files on the S3.
Downloading OR_SUVI-L1b-Fe171_G19_s20261351218237_e20261351218247_c20261351218479.fits
Downloading OR_SUVI-L1b-Fe171_G19_s20261351318239_e20261351318249_c20261351318474.fits
Downloading OR_SUVI-L1b-Fe171_G19_s20261351418240_e20261351418250_c20261351418478.fits
Downloading OR_SUVI-L1b-Fe171_G19_s20261351518241_e20261351518251_c20261351518479.fits
Downloading OR_SUVI-L1b-Fe171_G19_s20261351618243_e20261351618253_c20261351618472.fits
Downloading OR_SUVI-L1b-Fe171_G19_s20261351718244_e20261351718254_c20261351718482.fits
Downloading OR_SUVI-L1b-Fe171_G19_s20261351818245_e20261351818255_c20261351818474.fits
Downloading OR_SUVI-L1b-Fe171_G19_s20261351918246_e20261351918256_c20261351918485.fits
Downloading OR_SUVI-L1b-Fe171_G19_s20261352018248_e20261352018258_c20261352018476.fits
Downloading OR_SUVI-L1b-Fe171_G19_s20261352118249_e20261352118259_c20261352118477.fits
Downloading OR_SUVI-L1b-Fe171_G19_s20261352218250_e20261352218260_c20261352218475.fits
Downloading OR_SUVI-L1b-Fe171_G19_s20261352318252_e20261352318262_c20261352318484.fits
Total running time of the script: (0 minutes 1.611 seconds)