# # # This source code is subject to the license referenced at
# # # https://github.com/NRLMMD-GEOIPS.
"""Windsat binary data reader.
This code is designed to read windsat sdr binary data (idr37) file for windsat
37 GHz products in GEOIPS environments. the input file name is something alike
US058SORB-BINspp.wndmi_fws_d20191126_s134102_e153244_r87467_cfnmoc.idr37.
V1.0: initial version. Song Yang, NRL-MRY, 01/08/2020
errflag is the important parameter of the windsat edr dataset. It is a 32-bit
integer which describes what is the current data point status. Here are the
meaning of each bit::
0-7: Wilheit rain flag
8: forward/aft scan (bit set to 1 for forward part of scan, 0 for aft scan )
9: ascending/descending pass flag (1 for ascending, 0 for descending)
10: Warm load flag
11: Warm load gains applied (1 = gains applied, 0 = gains not applied)
12: Glare angle invalid because no 1 vector or LOS doesn't pierce earth
13-18: Glare angle (0 to 30 represents angles of 0 to 60 degree in increments
of 2 deg; 31 represents angles .gt. 60 deg; 32 represents invalid
glare angle)
19: Cold load flag. If set to 1 the VH channel data had to be corrected due
to interference in the cold load signal, such as the moon or a
geostationary satellite.
20: Gain Saturation flag. Set to 1 when strong RFI causes the gain to change.
This is set if any TDR saturation flag is set at this frequency
23: used to hold the rfi flag so that it may be passed on to other structures
such as the resampling and intermediate structures. Finally RFI is placed
in the sdr structure.
other bits are spare
Here is the original sdr record in Fortran::
type IDRRecord_short
real(double) :: JD2000 8 bytes
real, dimension(4):: stokes 16 bytes
real :: latitude 4 bytes
real :: longitude 4 bytes
real :: EIA 4 bytes: earth incidence angle, the angle
on the ground between vertical and
the satellite look vector
real :: PRA 4 bytes: rotation of the polarization plane
from true
real :: CAA 4 bytes: compass azimuth angle on the ground
real :: tI45, tIcp, pra45 12 bytes
integer :: errflag 4 bytes (32-bits integer):a set of bit
flags for data quality and
conditions (above explanation)
integer :: Scan 4 bytes: scan line number in the orbit.
WindSat scans every 1.9 seconds
integer(int16) :: dcnum 2 bytes: pixel number along the scan,
called 'downcount number',
because the highest pixel number
is measured first.
integer(int16) :: SurfaceType 2 bytes: legacy SSMI surface type
integer(int16) :: scanAngle 2 bytes: angle on the ground between the
flight direction and the look
direction
integer(int8) :: water2land ! copied from IDRL record 1 byte:
integer(int8) :: land2water ! copied from IDRL record 1 byte:
end type IDRRecord_short
* Gain saturation is when a sudden, large signal causes the gain to change
quickly and make averaged gain unreliable.
* Forward/Aft is for sensor view position
* The warm load flag indicates that calibration may be unreliable due to solar
intrusion into the warm load.
* Cold load flags do not mean calibration is unreliable. It's a way for us to
check the cold load correction algorithm.
* Sun glare is not something to worry about.
* The RFI flag is never set at 37 GHz.
* tI45, tIcp, pra45, scanAngleI, water2land, and land2water aren't commanly used.
* The first four are for recreating the 6-element pre-Stokes polarization
vector, and the last two measure coastal contamination
The actual idr37 data record (idr_record) in C::
typedef struct {
double jd2000;
float stokes[4];
float plat; lat of earth observation
float plon; lon of earth observation
float eia; radiance= ~53deg
float pra;
float caa;
float slat; latitude of satellite position? not
float slon; longitude of satellite position? not
float salt; altitude of satellite (meter? km?) not
int errflag;
int scan;
short dcnum;
short surf;
float spare;
idr_record;
Its total length of idr_record is 72 bytes
"""
from geoips.interfaces.class_based.readers import BaseReaderPlugin
# Python Standard Libraries
from datetime import datetime
import logging
import os
# Third-Party Libraries
import numpy as np
# import pandas as pd
import xarray
LOG = logging.getLogger(__name__)
[docs]class WindsatIdr37BinaryReaderPlugin(BaseReaderPlugin):
"""Windsat Idr37 Binary reader plugin class."""
interface = "readers"
family = "standard"
name = "windsat_idr37_binary"
dataset_info = {
"WINDSAT_SDR_FWD": {
"ftime_jd2000": "ftime_jd2000",
"ftb37v": "ftb37v",
"ftb37h": "ftb37h",
"flat": "flat",
"flon": "flon",
"fsurfaceType": "fsurfaceType",
"frainFlag": "frainFlag",
"fasc_des_pass": "fasc_des_pass",
},
"WINDSAT_SDR_AFT": {
"atime_jd2000": "atime_jd2000",
"atb37v": "atb37v",
"atb37h": "atb37h",
"alat": "alat",
"alon": "alon",
"asurfaceType": "asurfaceType",
"arainFlag": "arainFlag",
"aasc_des_pass": "aasc_des_pass",
},
}
gvar_info = {
"WINDSAT_SDR_FWD": {"Latitude": "latitude", "Longitude": "longitude"},
"WINDSAT_SDR_AFT": {"Latitude": "latitude", "Longitude": "longitude"},
}
source_names = ["windsat"]
# NOTE: Anytime you see a # NOQA comment, this is for flake8 formatting. Unused
# variables are needed in this for moving through the binary file correctly. There
# is fmt: off and fmt: on comments, which prevent black from moving the # NOQA
# comments.
[docs] def call(
self,
fnames,
metadata_only=False,
chans=None,
area_def=None,
self_register=False,
):
"""Read Windsat binary data products.
Parameters
----------
fnames : list
* List of strings, full paths to files
metadata_only : bool, default=False
* Return before actually reading data if True
chans : list of str, default=None
* NOT YET IMPLEMENTED
* List of desired channels (skip unneeded variables as needed).
* Include all channels if None.
area_def : pyresample.AreaDefinition, default=None
* NOT YET IMPLEMENTED
* Specify region to read
* Read all data if None.
self_register : str or bool, default=False
* NOT YET IMPLEMENTED
* register all data to the specified dataset id (as specified in the
return dictionary keys).
* Read multiple resolutions of data if False.
Returns
-------
dict of xarray.Datasets
* dictionary of xarray.Dataset objects with required Variables and
Attributes.
* Dictionary keys can be any descriptive dataset ids.
See Also
--------
:ref:`xarray_standards`
Additional information regarding required attributes and variables
for GeoIPS-formatted xarray Datasets.
"""
# get data time info from input fname
fname = fnames[0]
time_s_date = os.path.basename(fname).split("_")[2][1:9]
time_s_hhmm = os.path.basename(fname).split("_")[3][1:5]
time_e_hhmm = os.path.basename(fname).split("_")[4][1:5]
# findout whether this data file crossing boundary of day, month, or year
time_s_year = time_s_date[0:4]
time_s_month = time_s_date[4:6]
time_s_day = time_s_date[6:8]
# initialization of ending date for this file
time_e_year = int(time_s_year)
time_e_month = int(time_s_month)
time_e_day = int(time_s_day)
# if time_e_hhmm is less than time_s_hhmm, the data crossed the day
# boundary and entered the next day
if (int(time_s_year) % 400 == 0) or (
(int(time_s_year) % 4 == 0) and (int(time_s_year) % 100 != 0)
):
year_leap = "true"
LOG.info("%d is a Leap Year" % int(time_s_year))
else:
year_leap = "false"
LOG.info("%d is Not the Leap Year" % int(time_s_year))
if int(time_e_hhmm) < int(time_s_hhmm):
time_e_day = int(time_s_day) + 1
if (
(int(time_s_month) == 4)
or (int(time_s_month) == 6)
or (int(time_s_month) == 9)
): # in Apr, Jun and Sep
if time_e_day > 30:
time_e_day == 1 # first day of the next month
time_e_month == int(time_s_month) + 1
else:
if int(time_s_month) == 2: # in Feb
if year_leap: # In a leap year
if time_e_day > 29:
time_e_day == 1 # first day of the next month
time_e_month == int(time_s_month) + 1
else: # No leap year
if time_e_day > 28:
time_e_day == 1 # first day of the next month
time_e_month == int(time_s_month) + 1
else: # in Jan, Mar, Jul, Aug, Oct, Dec
if time_e_day > 31:
time_e_day == 1 # first day of the next month
time_e_month == int(time_s_month) + 1
if (
time_e_month > 12
): # in Dec, deterimine whether it crosses the year boundary
time_e_year == int(time_s_year) + 1
# convret end_time of the data into strings
time_e_date = (
str(time_e_year) + str("%02d" % time_e_month) + str("%02d" % time_e_day)
)
# Need to set up time to be read in by the metadata (year and jday are arrays)
time_start = time_s_date + " " + time_s_hhmm
time_end = time_e_date + " " + time_e_hhmm
xarray_obj = xarray.Dataset()
# Enter metadata
xarray_obj.attrs["start_datetime"] = datetime.strptime(
time_start, "%Y%m%d %H%M"
)
xarray_obj.attrs["end_datetime"] = datetime.strptime(time_end, "%Y%m%d %H%M")
xarray_obj.attrs["source_file_datetimes"] = [xarray_obj.attrs["start_datetime"]]
xarray_obj.attrs["platform_name"] = "coriolis"
xarray_obj.attrs["source_name"] = "windsat"
xarray_obj.attrs["interpolation_radius_of_influence"] = 10000
xarray_obj.attrs["sample_distance_km"] = 2
xarray_obj.attrs["data_provider"] = "NRL-NOAA"
# Passing chans == [] indicates we do not want ANY data, only metadata, so
# return once metadata is set.
if metadata_only:
return {"METADATA": xarray_obj}
# find out size of the input file (bytes)
filesize_info = os.stat(fname).st_size
len_OneRec = 72 # size of record (bytes)?
rec_tot = filesize_info // len_OneRec # how many records in the file
# check for exact data records by mode of the file_size vs data_record
try:
good_datafile = filesize_info % len_OneRec
if good_datafile == 0:
LOG.info("This is a good windsat idr37 data file")
else:
LOG.info("This is not a good windsat idr37 data file: skipping ....")
return
except Exception as resp:
LOG.info(
"\tBLANKET EXCEPTION %s: %s >> %s : %s",
type(resp).__name__,
str(resp.__doc__),
str(resp.args),
"windsat idr37 data does not have even number of data records !! "
"Skipping...",
)
return {"METADATA": xarray_obj}
bad_value = -999
# declare data arrays
try:
windsat_read = np.ma.zeros(rec_tot) # initialization of zeros
np.ma.masked_all_like(windsat_read)
except Exception as resp:
LOG.info(
"\tBLANKET EXCEPTION %s: %s >> %s : %s",
type(resp).__name__,
str(resp.__doc__),
str(resp.args),
"windsat idr37 data does not have even number of data records !! "
"Skipping...",
)
return {"METADATA": xarray_obj}
ftime_jd2000 = np.ma.masked_values(windsat_read, bad_value)
ftb37v = np.ma.masked_values(windsat_read, bad_value)
ftb37h = np.ma.masked_values(windsat_read, bad_value)
flat = np.ma.masked_values(windsat_read, bad_value)
flon = np.ma.masked_values(windsat_read, bad_value)
fsurfaceType = np.ma.masked_values(windsat_read, bad_value)
frainFlag = np.ma.masked_values(windsat_read, bad_value)
fasc_des_pass = np.ma.masked_values(windsat_read, bad_value)
atime_jd2000 = np.ma.masked_values(windsat_read, bad_value)
atb37v = np.ma.masked_values(windsat_read, bad_value)
atb37h = np.ma.masked_values(windsat_read, bad_value)
alat = np.ma.masked_values(windsat_read, bad_value)
alon = np.ma.masked_values(windsat_read, bad_value)
asurfaceType = np.ma.masked_values(windsat_read, bad_value)
arainFlag = np.ma.masked_values(windsat_read, bad_value)
aasc_des_pass = np.ma.masked_values(windsat_read, bad_value)
f1 = open(fname, "rb")
k = 0
k2 = 0
# read in the windsat edr products for all data points
for ii in range(rec_tot): # loop records of this file
if ii % 10000 == 0:
LOG.info("Running record number %s of %s", ii, rec_tot)
try:
# read in variables using their size (bytes)
jd2000 = np.frombuffer(
f1.read(8), dtype=np.dtype("float64")
).byteswap()[
0
] # sec since 1200Z,01/01/2000
# get time info for each data point
# fmt: off
# timeinfo = pd.datetime(2000, 1, 1, 12) + pd.Timedelta(jd2000, unit="s") # NOQA
# time_date = int(
# str(timeinfo.year) + str(timeinfo.month) + str(timeinfo.day)
# )
# time_hhmm = int(str(timeinfo.hour) + str(timeinfo.minute))
tb37v, tb37h, tb37info1, tb37info2 = np.frombuffer(
f1.read(16), dtype=np.dtype("float32")
).byteswap() # K (37v,37h,info1,info2)
lat = np.frombuffer(f1.read(4), dtype=np.dtype("float32")).byteswap()[
0
] # deg
lon = np.frombuffer(f1.read(4), dtype=np.dtype("float32")).byteswap()[
0
] # deg
# if lon > 180.0: # windbarbs needs lon in (-180,180)
# lon=lon-360
eia = np.frombuffer( # NOQA
f1.read(4), dtype=np.dtype("float32")
).byteswap()[
0
] # unit in radiance =~53 deg
pra = np.frombuffer(f1.read(4), dtype=np.dtype("float32")).byteswap()[ # NOQA
0
]
caa = np.frombuffer(f1.read(4), dtype=np.dtype("float32")).byteswap()[ # NOQA
0
]
slat = np.frombuffer( # NOQA
f1.read(4), dtype=np.dtype("float32")
).byteswap()[
0
] # deg
slon = np.frombuffer( # NOQA
f1.read(4), dtype=np.dtype("float32")
).byteswap()[
0
] # deg
salt = np.frombuffer( # NOQA
f1.read(4), dtype=np.dtype("float32")
).byteswap()[
0
] # meter
errflag = np.frombuffer(f1.read(4), dtype=np.dtype("int32")).byteswap()[
0
] # deg
scanNum = np.frombuffer(f1.read(4), dtype=np.dtype("int32")).byteswap()[ # NOQA
0
]
downcountNum = np.frombuffer( # NOQA
f1.read(2), dtype=np.dtype("int16")
).byteswap()[0]
surfaceType = np.frombuffer(
f1.read(2), dtype=np.dtype("int16")
).byteswap()[0]
spare = np.frombuffer( # NOQA
f1.read(4), dtype=np.dtype("float32")
).byteswap()[
0
] # spare var for space holder
# fmt: on
# decode errflag to assign value to approperated variables, i.e.,
# forward/aft mode, ascending/descending etc
fore_aft_scan = (
errflag >> 8 & 1
) # =1, foreward scan; =0, aft scan (foreward scan used for image)
asc_des_pass = errflag >> 9 & 1 # =1, ascending; =0, descending
# rainflag
rrflag_tmp = 0
for j in range(8):
bit = errflag >> j & 1 # =1, set, =0, not set
rrflag_tmp += bit * pow(2, j)
rrflag = rrflag_tmp
if (
fore_aft_scan == 1
): # forward scan points --> will be used for image products
ftb37v[k] = tb37v
ftb37h[k] = tb37h
flat[k] = lat
flon[k] = lon
fsurfaceType[k] = surfaceType
frainFlag[k] = rrflag
ftime_jd2000[k] = jd2000
fasc_des_pass[k] = asc_des_pass
k += 1
else: # aft scan points
atb37v[k2] = tb37v
atb37h[k2] = tb37h
alat[k2] = lat
alon[k2] = lon
asurfaceType[k2] = surfaceType
arainFlag[k2] = rrflag
atime_jd2000[k2] = jd2000
aasc_des_pass[k2] = asc_des_pass
k2 += 1
except Exception as resp:
LOG.info(
"\tBLANKET EXCEPTION %s: %s >> %s : %s",
type(resp).__name__,
str(resp.__doc__),
str(resp.args),
"Failed setting windsat sdr data arrays!! Skipping...",
)
f1.close()
flat = np.ma.masked_values(flat, bad_value)
flon = np.ma.masked_values(flon, bad_value)
ftime_jd2000 = np.ma.masked_values(ftime_jd2000, bad_value)
ftb37v = np.ma.masked_values(ftb37v, bad_value)
ftb37h = np.ma.masked_values(ftb37h, bad_value)
fsurfaceType = np.ma.masked_values(fsurfaceType, bad_value)
frainFlag = np.ma.masked_values(frainFlag, bad_value)
fasc_des_pass = np.ma.masked_values(fasc_des_pass, bad_value)
alat = np.ma.masked_values(alat, bad_value)
alon = np.ma.masked_values(alon, bad_value)
atime_jd2000 = np.ma.masked_values(atime_jd2000, bad_value)
atb37v = np.ma.masked_values(atb37v, bad_value)
atb37h = np.ma.masked_values(atb37h, bad_value)
asurfaceType = np.ma.masked_values(asurfaceType, bad_value)
arainFlag = np.ma.masked_values(arainFlag, bad_value)
aasc_des_pass = np.ma.masked_values(aasc_des_pass, bad_value)
xarray_sdr_fwd = xarray.Dataset()
xarray_sdr_aft = xarray.Dataset()
xarray_sdr_aft = xarray.Dataset()
xarray_sdr_fwd.attrs = xarray_obj.attrs.copy()
xarray_sdr_aft.attrs = xarray_obj.attrs.copy()
timediff = np.datetime64("2000-01-01T12:00:00") - np.datetime64(
"1970-01-01T00:00:00"
)
timestamps = ftime_jd2000.astype("np.datetime64[s]") + timediff
xarray_sdr_aft["time"] = xarray.DataArray(timestamps)
xarray_sdr_aft["latitude"] = xarray.DataArray(alat)
xarray_sdr_aft["longitude"] = xarray.DataArray(alon)
xarray_sdr_aft["atb37v"] = xarray.DataArray(atb37v)
xarray_sdr_aft["asurfaceType"] = xarray.DataArray(asurfaceType)
xarray_sdr_aft["arainFlag"] = xarray.DataArray(arainFlag)
xarray_sdr_aft["aasc_des_pass"] = xarray.DataArray(aasc_des_pass)
xarray_sdr_fwd["time"] = xarray.DataArray(timestamps)
xarray_sdr_fwd["latitude"] = xarray.DataArray(flat)
xarray_sdr_fwd["longitude"] = xarray.DataArray(flon)
xarray_sdr_fwd["ftb37v"] = xarray.DataArray(ftb37v)
xarray_sdr_fwd["fsurfaceType"] = xarray.DataArray(fsurfaceType)
xarray_sdr_fwd["frainFlag"] = xarray.DataArray(frainFlag)
xarray_sdr_fwd["fasc_des_pass"] = xarray.DataArray(fasc_des_pass)
return {"METADATA": xarray_obj, "AFT": xarray_sdr_aft, "FWD": xarray_sdr_fwd}
PLUGIN_CLASS = WindsatIdr37BinaryReaderPlugin