The radius of maximum wind (RMW) is a key strutural parameter of tropical cyclones required to understand the evolution of winds, rainfall, and storm surge. Observations of the RMW are sparse over the ocean, however synthetic aperature radar (SAR) observations have fine enough resolution to observe the RMW in tropical cyclones. The SAR is a c-band radar on polar orbitting satellites, so the RMW is not continously sampled. When an overpass does occur, a fix of the tropical cyclone is added to the f-deck for use by forecasters to estimate the wind structure in real-time. This code below will read in the fix files and save them to numpy arrays.

Code

import copy
import numpy as np
import datetime as dt
import subprocess
import matplotlib.pyplot as plt

import warnings
warnings.filterwarnings("ignore")



def get_fdeck_grep(OBSTYPE,STORMPATH,FIXTYPE=''):
    '''
    
    Function to get a dataset of f-decks as a ND-numpy array
    
    The formatting of the f-decks can be found here:
          https://www.nrlmry.navy.mil/atcf_web/docs/database/new/newfdeck.txt
    
    Input:
     OBSTYPE          : The observation type to get using grep format for multiple types.
                         i.e. '"AIRC\|RDRD"' for aircraft and radar or 'ASCT' for just ASCAT
     STORMPATH        : Path(s) and filename(s) for all files to grep using wildcard for multiple storms
                         i.e. '../dev/fdeck/fal*2019.dat''
     FIXTYPE          : Type of fixes to include, such as 'C' for only center fixes. Flagged fixes will not be
                         output

    Output:
     O_DATE           : Datetime of observation as a string in %Y%m%D%H%M
     O_STID           : Stormid in standard ATCF format
     O_FXFORMAT       : Fix format
     O_FIXTTYPE       : Fix type such as AIRC for aircraft, 
     O_CENTERI        : What was sampled, 'C' for center fix, 'R' for wind radii fix
     O_FLAGIND        : Flagged indicator for suspect data, same as CENTERI but with 'F' for all
     O_FLAT           : Latitude
     O_FLON           : Longitude
     O_HEIGHT         : Height of the observation in meters
     O_POSITCONF      : Position confidence, 1=good, 3=poor
     O_VMAX           : Maximum wind speed in kts
     O_VMAXCONF       : Confidence of VMAX, 1=good, 3=poor
     O_MSLP           : Central Pressure in mb
     O_MSLPCONF       : Confidence in central pressure, 1=good, 3=poor
     O_MSLPDERIV      : Pressure derivation such as 'DVRK' or 'MEAS' for measured
     O_WRADII         : Wind radii ID such as 34, 50, or 64
     O_WRNE           : Radius in NE quadrant in n mi
     O_WRSE           : Radius in SE quadrant in n mi
     O_WRSW           : Radius in SW quadrant in n mi
     O_WRNW           : Radius in NW quadrant in n mi
     O_WRNEMODEL      : Radii mode, E=Edge of Pass, C=cut off by land, B=both
     O_WRSEMODEL      : Radii mode, E=Edge of Pass, C=cut off by land, B=both
     O_WRSWMODEL      : Radii mode, E=Edge of Pass, C=cut off by land, B=both
     O_WRNWMODEL      : Radii mode, E=Edge of Pass, C=cut off by land, B=both
     O_WRCONF         : Wind radii confidence, 1=good, 3=poor
     O_RMW            : Radius of maximum winds in n mi
     O_RADEYE         : Diameter of eye in n mi
     A_RMW            : Radius of maximum winds in n mi derived from Aircraft

     
    Example:
     DATE, STID, FXFORMAT, FIXTTYPE, CENTERI, FLAGIND, FLAT, FLON, HEIGHT, POSITCONF, VMAX, VMAXCONF, MSLP, \
     MSLPCONF, MSLPDERIV, WRADII, WRNE, WRSE, WRSW, WRNW, WRNEMODEL,WRSEMODEL, WRSWMODEL, WRNWMODEL, WRCONF, RMW, \
     RADEYE, ARMW = get_fdeck_grep('"ASCT\|AMSU\|RDRD"' ,'../dev/fdeck/fal092021.dat',FIXTYPE='C')
    
    
    
    '''
    # First read in the b-deck file
    output = subprocess.check_output("grep -h "+OBSTYPE+" "+STORMPATH, shell=True).decode().split('\n')

    # Since not all lines have the same number of variables loop through and only keep the first nmax
    new_data = []
    nmax = 56 # max number of collumns to read in f-deck
    nout = 28 # number of output variables


    for i in range(0,len(output)):
        data = output[i].split(',') # Split the lines by commas

        if (len(data)>2): #make sure we only use lines with nmax values

            # Since older files don't have RMW in the operational best-tracks we need to add data to
            # to make sure the file sizes are correct
            if len(data)<nmax:
                nzeros = nmax-len(data) #number of values to pad the list with
                data = data+[' ' for n in range(0,nzeros)] # Pad the data to get to nmax

            new_data.append(data[:nmax]) #only take the 20 values

    new_data = np.array(new_data).astype(str) #convert back to numpy array

    # If no data is found then return missing values
    if len(new_data)<1:
        return [-999]*nout

    # Get the number of lines
    nrows = len(new_data[:,2])


    #Using the unique number of rows, initialize the arrays we will use    
    O_DATE       = np.empty((nrows),dtype='|U12')
    O_STID       = np.empty((nrows),dtype='|U8')
    O_FXFORMAT   = np.zeros((nrows))
    O_FIXTTYPE   = np.empty((nrows),dtype='|U4')
    O_CENTERI    = np.empty((nrows),dtype='|U10')
    O_FLAGIND    = np.empty((nrows),dtype='|U1')
    O_VMAX       = np.zeros((nrows))
    O_VMAXCONF   = np.zeros((nrows))
    O_MSLP       = np.zeros((nrows))
    O_MSLPCONF   = np.zeros((nrows))
    O_FLON       = np.zeros((nrows))
    O_FLAT       = np.zeros((nrows))
    O_HEIGHT     = np.zeros((nrows))
    O_POSITCONF  = np.zeros((nrows))
    O_MSLPDERIV  = np.empty((nrows),dtype='|U4')
    O_WRCODE     = np.zeros((nrows))
    O_WRADII     = np.zeros((nrows))
    O_WRCONF     = np.zeros((nrows))
    O_WRNE       = np.zeros((nrows))
    O_WRSE       = np.zeros((nrows))
    O_WRSW       = np.zeros((nrows))
    O_WRNW       = np.zeros((nrows))
    O_WRNEMODEL  = np.empty((nrows),dtype='|U1')
    O_WRSEMODEL  = np.empty((nrows),dtype='|U1')
    O_WRSWMODEL  = np.empty((nrows),dtype='|U1')
    O_WRNWMODEL  = np.empty((nrows),dtype='|U1')
    O_RADEYE     = np.zeros((nrows))
    O_RMW        = np.zeros((nrows))
    A_RMW        = np.zeros((nrows))



    # Now lets loop through and save the data to the specified variables
    i_dat = 0


    for i_dat in range(0,nrows): # Loop through the unique datetimes
        
        line = new_data[i_dat].astype(str) #Get the data for the specific date time
        
        O_DATE[i_dat] = str(int(line[2]))

        # Since each date could have multiple rows for each wind radii, we need to loop through them
        O_STID[i_dat]     = str(line[0]).strip()+str(line[1]).strip() + str(int(line[2])).strip()[:4]        
        O_FXFORMAT[i_dat] = int(line[3])
        O_FIXTTYPE[i_dat] = line[4].strip()
        O_CENTERI[i_dat]  = line[5].strip()
        O_FLAGIND[i_dat]  = line[6].strip()

        # Convert latitudes to floats
        if line[7][-1:]=='N':
            O_FLAT[i_dat] = float(line[7][:-1])/100.
        elif line[7][-1:]=='S':
            O_FLAT[i_dat] = -float(line[7][:-1])/100.
        # Convert longitudes to floats
        if line[8][-1:]=='E':
            O_FLON[i_dat]= float(line[8][:-1])/100.
        elif line[8][-1:]=='W':
            O_FLON[i_dat]= -float(line[8][:-1])/100.
            
        O_HEIGHT[i_dat]    = int(line[9]) if (line[9].strip() or None) else np.nan
        O_POSITCONF[i_dat] = int(line[10]) if (line[10].strip() or None) else np.nan
        O_VMAX[i_dat]      = int(line[11]) if (line[11].strip() or None) else np.nan
        O_VMAXCONF[i_dat]  = int(line[12]) if (line[12].strip() or None) else np.nan
        O_MSLP[i_dat]      = int(line[13]) if (line[13].strip() or None) else np.nan
        O_MSLPCONF[i_dat]  = int(line[14]) if (line[14].strip() or None) else np.nan
        O_MSLPDERIV[i_dat] = line[15].strip()       

        # Check if there are wind radii information
        if (line[17].strip()=='NEQ'):

            O_WRADII[i_dat]     = int(line[16])

            if line[18]=='*****':
                O_WRNE[i_dat]       = np.nan
            else:        
                O_WRNE[i_dat]       = int(line[18])

                
            if line[19]=='*****':                
                O_WRSE[i_dat]       = np.nan
            else:
                O_WRSE[i_dat]       = int(line[19])


            if line[20]=='*****':
                O_WRSW[i_dat]       = np.nan
            else:
                O_WRSW[i_dat]       = int(line[20])


            if line[21]=='*****':
                O_WRNW[i_dat]       = np.nan
            else:
                O_WRNW[i_dat]       = int(line[21])


            O_WRNEMODEL[i_dat]  = line[22].strip()
            O_WRSEMODEL[i_dat]  = line[23].strip()
            O_WRSWMODEL[i_dat]  = line[24].strip()
            O_WRNWMODEL[i_dat]  = line[25].strip()
            O_WRCONF[i_dat]     = int(line[26]) if (line[26].strip() or None) else np.nan

        else:
            # If there are no wind radii information then set to missing values
            O_WRADII[i_dat]     = np.nan
            O_WRNE[i_dat]       = np.nan
            O_WRSE[i_dat]       = np.nan
            O_WRSW[i_dat]       = np.nan
            O_WRNW[i_dat]       = np.nan
            O_WRNEMODEL[i_dat]  = ' '
            O_WRSEMODEL[i_dat]  = ' '
            O_WRSWMODEL[i_dat]  = ' '
            O_WRNWMODEL[i_dat]  = ' '
            O_WRCONF[i_dat]     = np.nan
        


        O_RMW[i_dat]  = int(line[27]) if (line[27].strip() or None) else np.nan
        O_RADEYE[i_dat]  = int(line[28]) if (line[28].strip() or None) else np.nan

        # Only for aircraft
        if "AIRC" in line[4].strip():
            A_RMW[i_dat]  = int(line[37]) if (line[37].strip() or None) else np.nan
        else:
            A_RMW[i_dat]  = np.nan
        
    # Check if we need to apply a fix constraint
    # If a contraint is applied then we also make sure that that constraint is not flagged as suspect
    if FIXTYPE!='':
        cond_fx = [i for (i,x,y) in zip(range(0,len(O_CENTERI)),O_CENTERI,O_FLAGIND) if (((FIXTYPE or 'F') not in y))  ]
    else:
        cond_fx = [i for i in range(0,len(O_CENTERI))]
        

    return O_DATE[cond_fx], O_STID[cond_fx], O_FXFORMAT[cond_fx], O_FIXTTYPE[cond_fx], O_CENTERI[cond_fx], O_FLAGIND[cond_fx], O_FLAT[cond_fx], O_FLON[cond_fx], O_HEIGHT[cond_fx], O_POSITCONF[cond_fx], \
        O_VMAX[cond_fx], O_VMAXCONF[cond_fx], O_MSLP[cond_fx], O_MSLPCONF[cond_fx], O_MSLPDERIV[cond_fx], O_WRADII[cond_fx], O_WRNE[cond_fx], O_WRSE[cond_fx], O_WRSW[cond_fx], O_WRNW[cond_fx], O_WRNEMODEL[cond_fx],\
        O_WRSEMODEL[cond_fx], O_WRSWMODEL[cond_fx], O_WRNWMODEL[cond_fx], O_WRCONF[cond_fx], O_RMW[cond_fx], O_RADEYE[cond_fx],A_RMW[cond_fx]



if __name__=='__main__':

    PATH_TO_FDECK_FILES = 'FIX/f*.dat'

    # Read in the data from the function specifyin the path and the observation type, here we want SAR
    DATE, STID, FXFORMAT, FIXTTYPE, CENTERI, FLAGIND, FLAT, FLON, HEIGHT, POSITCONF, VMAX, VMAXCONF, MSLP, \
     MSLPCONF, MSLPDERIV, WRADII, WRNE, WRSE, WRSW, WRNW, WRNEMODEL,WRSEMODEL, WRSWMODEL, WRNWMODEL, WRCONF, \
     RMW, RADEYE, ARMW = get_fdeck_grep('"SAR"' ,PATH_TO_FDECK_FILES)
    
    #--------------------------------------------------------------
    # Plot RMW v. Latitude
    fig = plt.figure(figsize=(10,6))
    ax = plt.subplot(111)

    cmap1 = copy.copy(plt.cm.rainbow)

    XVAR = RMW[WRADII==34]  # Don't include duplicates from R50 and R64
    YVAR = FLAT[WRADII==34] # Don't include duplicates from R50 and R64

    im = ax.hexbin(XVAR,YVAR,gridsize=20,alpha=.9,linewidths=0,vmin=1,cmap=cmap1)
    im.cmap.set_under('w')

    ax.scatter(XVAR,YVAR,c='k',s=10)

    # Limit the boundaries
    ax.set_xlim(0,225)
    ax.set_ylim(-55,55)

    # Add labels
    ax.set_title('SAR Observations (2016-2023)',fontsize=18)
    ax.set_xlabel('RMW (n mi)', fontsize=14)
    ax.set_ylabel('Latitude (degrees)', fontsize=14)

    # Increase size of xticks
    ax.tick_params(axis='y',labelsize=12)
    ax.tick_params(axis='x',labelsize=12)
    
    # Remove the top and right lines
    ax.spines["top"].set_visible(False)
    ax.spines["right"].set_visible(False)
    
    cb = plt.colorbar(im,pad=.02,shrink=.7)
    

    plt.savefig('SAR_RMWvLat_Allbasins.png',bbox_inches='tight')
    plt.show()




The code above reads in the fix dataset and can be used to plot SAR data. To illustrate, we can plot the distribution of RMW vs. latitude.

A figure showing latitude vs. RMW for all SAR fixes.