def clenshaw_summation(clm, slm, lon, lat, RAD=0, UNITS=0, LMAX=0, LOVE=None,
    ASTYPE=np.float128, SCALE=1e-280):
    """
    Calculates the spatial field for a series of spherical harmonics for a
    sequence of ungridded points

    Arguments
    ---------
    clm: cosine spherical harmonic coefficients
    slm: sine spherical harmonic coefficients
    lon: longitude of points
    lat: latitude of points

    Keyword arguments
    -----------------
    RAD: Gaussian smoothing radius (km)
    UNITS: output data units
        1: cm of water thickness
        2: mm of geoid height
        3: mm of elastic crustal deformation
        4: microGal gravitational perturbation
        5: Pa, equivalent surface pressure in Pascals
        6: cm of viscoelastic rustal uplift (GIA)
    LMAX: Upper bound of Spherical Harmonic Degrees
    LOVE: input load Love numbers up to degree LMAX (hl,kl,ll)

    Returns
    -------
    spatial: calculated spatial field for latitude and longitude
    """

    #-- check if lat and lon are the same size
    if (len(lat) != len(lon)):
        raise ValueError('Incompatable vector dimensions (lon, lat)')

    #-- calculate colatitude and longitude in radians
    th = (90.0 - lat)*np.pi/180.0
    phi = np.squeeze(lon*np.pi/180.0)
    #-- calculate cos and sin of colatitudes
    t = np.cos(th)
    u = np.sin(th)

    #-- dimensions of theta and phi
    npts = len(th)

    #-- Gaussian Smoothing
    if (RAD != 0):
        wl = 2.0*np.pi*gauss_weights(RAD,LMAX)
    else:
        #-- else = 1
        wl = np.ones((LMAX+1))

    #-- Setting units factor for output
    #-- extract arrays of kl, hl, and ll Love Numbers
    factors = units(lmax=LMAX).harmonic(*LOVE)
    #-- dfactor computes the degree dependent coefficients
    if (UNITS == 0):
        #-- 0: keep original scale
        dfactor = factors.norm
    elif (UNITS == 1):
        #-- 1: cmH2O, centimeters water equivalent
        dfactor = factors.cmwe
    elif (UNITS == 2):
        #-- 2: mmGH, mm geoid height
        dfactor = factors.mmGH
    elif (UNITS == 3):
        #-- 3: mmCU, mm elastic crustal deformation
        dfactor = factors.mmCU
    elif (UNITS == 4):
        #-- 4: micGal, microGal gravity perturbations
        dfactor = factors.microGal
    elif (UNITS == 5):
        #-- 5: Pa, equivalent surface pressure in Pascals
        dfactor = factors.Pa
    elif (UNITS == 6):
        #-- 6: cmVCU, cm viscoelastic  crustal uplift (GIA ONLY)
        dfactor = factors.cmVCU
    else:
        raise ValueError(('UNITS is invalid:\n1: cmH2O\n2: mmGH\n3: mmCU '
            '(elastic)\n4:microGal\n5: Pa\n6: cmVCU (viscoelastic)'))

    #-- calculate arrays for clenshaw summations over colatitudes
    s_m_c = np.zeros((npts,LMAX*2+2))
    for m in range(LMAX, -1, -1):
        #-- convolve harmonics with unit factors and smoothing
        s_m_c[:,2*m:2*m+2] = clenshaw_s_m(t, dfactor*wl, m, clm, slm, LMAX)

    #-- calculate cos(phi)
    cos_phi_2 = 2.0*np.cos(phi)
    #-- matrix of cos/sin m*phi summation
    cos_m_phi = np.zeros((npts,LMAX+2),dtype=ASTYPE)
    sin_m_phi = np.zeros((npts,LMAX+2),dtype=ASTYPE)
    #-- initialize matrix with values at lmax+1 and lmax
    cos_m_phi[:,LMAX+1] = np.cos(ASTYPE(LMAX + 1)*phi)
    sin_m_phi[:,LMAX+1] = np.sin(ASTYPE(LMAX + 1)*phi)
    cos_m_phi[:,LMAX] = np.cos(ASTYPE(LMAX)*phi)
    sin_m_phi[:,LMAX] = np.sin(ASTYPE(LMAX)*phi)
    #-- calculate summation for order LMAX
    s_m = s_m_c[:,2*LMAX]*cos_m_phi[:,LMAX] + s_m_c[:,2*LMAX+1]*sin_m_phi[:,LMAX]
    #-- iterate to calculate complete summation
    for m in range(LMAX-1, 0, -1):
        cos_m_phi[:,m] = cos_phi_2*cos_m_phi[:,m+1] - cos_m_phi[:,m+2]
        sin_m_phi[:,m] = cos_phi_2*sin_m_phi[:,m+1] - sin_m_phi[:,m+2]
        #-- calculate summation for order m
        a_m = np.sqrt((2.0*m+3.0)/(2.0*m+2.0))
        s_m = a_m*u*s_m + s_m_c[:,2*m]*cos_m_phi[:,m] + s_m_c[:,2*m+1]*sin_m_phi[:,m]
    #-- calculate spatial field
    spatial = np.sqrt(3.0)*u*s_m + s_m_c[:,0]
    #-- return the calculated spatial field
    return spatial
def combine_harmonics(INPUT_FILE,
                      OUTPUT_FILE,
                      LMAX=None,
                      MMAX=None,
                      LOVE_NUMBERS=0,
                      REFERENCE=None,
                      RAD=None,
                      DESTRIPE=False,
                      UNITS=None,
                      DDEG=None,
                      INTERVAL=None,
                      BOUNDS=None,
                      REDISTRIBUTE=False,
                      LSMASK=None,
                      MEAN_FILE=None,
                      DATAFORM=None,
                      VERBOSE=False,
                      MODE=0o775):

    #-- verify that output directory exists
    DIRECTORY = os.path.abspath(os.path.dirname(OUTPUT_FILE))
    if not os.access(DIRECTORY, os.F_OK):
        os.makedirs(DIRECTORY, MODE, exist_ok=True)

    #-- read input spherical harmonic coefficients from file in DATAFORM
    if (DATAFORM == 'ascii'):
        input_Ylms = harmonics().from_ascii(INPUT_FILE)
    elif (DATAFORM == 'netCDF4'):
        #-- read input netCDF4 file (.nc)
        input_Ylms = harmonics().from_netCDF4(INPUT_FILE)
    elif (DATAFORM == 'HDF5'):
        #-- read input HDF5 file (.H5)
        input_Ylms = harmonics().from_HDF5(INPUT_FILE)
    #-- reform harmonic dimensions to be l,m,t
    #-- truncate to degree and order LMAX, MMAX
    input_Ylms = input_Ylms.truncate(lmax=LMAX, mmax=MMAX).expand_dims()
    #-- remove mean file from input Ylms
    if MEAN_FILE and (DATAFORM == 'ascii'):
        mean_Ylms = harmonics().from_ascii(MEAN_FILE, date=False)
        input_Ylms.subtract(mean_Ylms)
    elif MEAN_FILE and (DATAFORM == 'netCDF4'):
        #-- read input netCDF4 file (.nc)
        mean_Ylms = harmonics().from_netCDF4(MEAN_FILE, date=False)
        input_Ylms.subtract(mean_Ylms)
    elif MEAN_FILE and (DATAFORM == 'HDF5'):
        #-- read input HDF5 file (.H5)
        mean_Ylms = harmonics().from_HDF5(MEAN_FILE, date=False)
        input_Ylms.subtract(mean_Ylms)

    #-- read arrays of kl, hl, and ll Love Numbers
    hl, kl, ll = load_love_numbers(LMAX,
                                   LOVE_NUMBERS=LOVE_NUMBERS,
                                   REFERENCE=REFERENCE)

    #-- distribute total mass uniformly over the ocean
    if REDISTRIBUTE:
        #-- read Land-Sea Mask and convert to spherical harmonics
        ocean_Ylms = ocean_stokes(LSMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll))
        #-- calculate ratio between total mass and a uniformly distributed
        #-- layer of water over the ocean
        ratio = input_Ylms.clm[0, 0, :] / ocean_Ylms['clm'][0, 0]
        #-- for each spherical harmonic
        for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
            for l in range(m, LMAX + 1):  #-- LMAX+1 to include LMAX
                #-- remove the ratio*ocean Ylms from Ylms
                #-- note: x -= y is equivalent to x = x - y
                input_Ylms.clm[l, m, :] -= ratio * ocean_Ylms['clm'][l, m]
                input_Ylms.slm[l, m, :] -= ratio * ocean_Ylms['slm'][l, m]

    #-- if using a decorrelation filter (Isabella's destriping Routine)
    if DESTRIPE:
        input_Ylms = input_Ylms.destripe()

    #-- Gaussian smoothing
    if (RAD != 0):
        wt = 2.0 * np.pi * gauss_weights(RAD, LMAX)
    else:
        wt = np.ones((LMAX + 1))

    #-- Output spatial data
    grid = spatial()
    grid.time = np.copy(input_Ylms.time)
    grid.month = np.copy(input_Ylms.month)

    #-- Output Degree Spacing
    if (len(DDEG) == 1):
        #-- dlon == dlat
        dlon = DDEG
        dlat = DDEG
    else:
        #-- dlon != dlat
        dlon, dlat = DDEG

    #-- Output Degree Interval
    if (INTERVAL == 1):
        #-- (0:360,90:-90)
        nlon = np.int((360.0 / dlon) + 1.0)
        nlat = np.int((180.0 / dlat) + 1.0)
        grid.lon = dlon * np.arange(0, nlon)
        grid.lat = 90.0 - dlat * np.arange(0, nlat)
    elif (INTERVAL == 2):
        #-- (Degree spacing)/2
        grid.lon = np.arange(dlon / 2.0, 360 + dlon / 2.0, dlon)
        grid.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat)
        nlon = len(grid.lon)
        nlat = len(grid.lat)
    elif (INTERVAL == 3):
        #-- non-global grid set with BOUNDS parameter
        minlon, maxlon, minlat, maxlat = BOUNDS.copy()
        grid.lon = np.arange(minlon + dlon / 2.0, maxlon + dlon / 2.0, dlon)
        grid.lat = np.arange(maxlat - dlat / 2.0, minlat - dlat / 2.0, -dlat)
        nlon = len(grid.lon)
        nlat = len(grid.lat)

    #-- Setting units factor for output
    #-- dfactor computes the degree dependent coefficients
    if (UNITS == 1):
        #-- 1: cmwe, centimeters water equivalent
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).cmwe
    elif (UNITS == 2):
        #-- 2: mmGH, mm geoid height
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmGH
    elif (UNITS == 3):
        #-- 3: mmCU, mm elastic crustal deformation
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmCU
    elif (UNITS == 4):
        #-- 4: micGal, microGal gravity perturbations
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).microGal
    elif (UNITS == 5):
        #-- 5: Pa, equivalent surface pressure in Pascals
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).Pa
    else:
        raise ValueError(('UNITS is invalid:\n1: cmwe\n2: mmGH\n3: mmCU '
                          '(elastic)\n4:microGal\n5: Pa'))

    #-- Computing plms for converting to spatial domain
    theta = (90.0 - grid.lat) * np.pi / 180.0
    PLM, dPLM = plm_holmes(LMAX, np.cos(theta))

    #-- output spatial grid
    nt = len(input_Ylms.time)
    grid.data = np.zeros((nlat, nlon, nt))
    #-- converting harmonics to truncated, smoothed coefficients in output units
    for t in range(nt):
        #-- spherical harmonics for time t
        Ylms = input_Ylms.index(t)
        Ylms.convolve(dfactor * wt)
        #-- convert spherical harmonics to output spatial grid
        grid.data[:, :, t] = harmonic_summation(Ylms.clm,
                                                Ylms.slm,
                                                grid.lon,
                                                grid.lat,
                                                LMAX=LMAX,
                                                PLM=PLM).T

    #-- if verbose output: print input and output file names
    if VERBOSE:
        print('{0}:'.format(os.path.basename(sys.argv[0])))
        print('{0} -->\n\t{1}\n'.format(INPUT_FILE, OUTPUT_FILE))
    #-- outputting data to file
    output_data(grid.squeeze(),
                FILENAME=OUTPUT_FILE,
                DATAFORM=DATAFORM,
                UNITS=UNITS)
    #-- change output permissions level to MODE
    os.chmod(OUTPUT_FILE, MODE)
Example #3
0
def gen_pressure_stokes(PG,
                        R,
                        lon,
                        lat,
                        LMAX=60,
                        MMAX=None,
                        PLM=None,
                        LOVE=None):
    """
    Converts pressure fields from the spatial domain to spherical
    harmonic coefficients

    Arguments
    ---------
    PG: pressure/gravity ratio
    R: radius
    lon: longitude array
    lat: latitude array

    Keyword arguments
    -----------------
    LMAX: Upper bound of Spherical Harmonic Degrees
    MMAX: Upper bound of Spherical Harmonic Orders
    PLM: input Legendre polynomials
    LOVE: input load Love numbers up to degree LMAX (hl,kl,ll)

    Returns
    -------
    clm: cosine spherical harmonic coefficients
    slm: sine spherical harmonic coefficients
    l: spherical harmonic degree to LMAX
    m: spherical harmonic order to MMAX
    """

    #-- converting LMAX to integer
    LMAX = np.int(LMAX)
    #-- upper bound of spherical harmonic orders (default = LMAX)
    MMAX = np.copy(LMAX) if not MMAX else MMAX

    #-- grid dimensions
    nlat = np.int(len(lat))
    #-- grid step
    dlon = np.abs(lon[1] - lon[0])
    dlat = np.abs(lat[1] - lat[0])
    #-- longitude degree spacing in radians
    dphi = dlon * np.pi / 180.0
    #-- colatitude degree spacing in radians
    dth = dlat * np.pi / 180.0

    #-- reformatting longitudes to range 0:360 (if previously -180:180)
    lon = np.squeeze(lon.copy())
    if np.any(lon < 0):
        lon_ind, = np.nonzero(lon < 0)
        lon[lon_ind] += 360.0
    #-- Longitude in radians
    phi = lon[np.newaxis, :] * np.pi / 180.0
    #-- Colatitude in radians
    th = (90.0 - np.squeeze(lat.copy())) * np.pi / 180.0

    #-- For gridded data: dmat = original data matrix
    sz = np.shape(PG)
    #-- reforming data to lonXlat if input latXlon
    PG = np.transpose(PG) if (sz[0] == nlat) else PG
    R = np.transpose(R) if (sz[0] == nlat) else R

    #-- Coefficient for calculating Stokes coefficients from pressure field
    #-- extract arrays of kl, hl, and ll Love Numbers
    factors = units(lmax=LMAX).spatial(*LOVE)
    #-- Earth Parameters
    #-- Average Radius of the Earth [m]
    rad_e = factors.rad_e / 100.0
    #-- SH Degree dependent factors with indirect loading components
    dfactor = factors.mmwe

    #-- Calculating cos/sin of phi arrays
    #-- output [m,phi]
    m = np.arange(MMAX + 1)
    ccos = np.cos(np.dot(m[:, np.newaxis], phi))
    ssin = np.sin(np.dot(m[:, np.newaxis], phi))

    #-- Calculates fully-normalized Legendre Polynomials with plm_holmes.py
    #-- Output is plm[l,m,th]
    plm = np.zeros((LMAX + 1, MMAX + 1, nlat))
    #-- added option to precompute plms to improve computational speed
    if PLM is None:
        #-- if plms are not pre-computed: calculate Legendre polynomials
        PLM, dPLM = plm_holmes(LMAX, np.cos(th))

    #-- Multiplying by integration factors [sin(theta)*dtheta*dphi]
    #-- truncate legendre polynomials to spherical harmonic order MMAX
    m = np.arange(MMAX + 1)
    for j in range(0, nlat):
        plm[:, m, j] = PLM[:, m, j] * np.sin(th[j]) * dphi * dth

    #-- Initializing preliminary spherical harmonic matrices
    yclm = np.zeros((LMAX + 1, MMAX + 1))
    yslm = np.zeros((LMAX + 1, MMAX + 1))
    #-- Initializing output spherical harmonic matrices
    clm = np.zeros((LMAX + 1, MMAX + 1))
    slm = np.zeros((LMAX + 1, MMAX + 1))
    for l in range(0, LMAX + 1):  #-- equivalent to 0:LMAX
        mm = np.min([MMAX, l])  #-- truncate to MMAX if specified (if l > MMAX)
        m = np.arange(0, mm + 1)  #-- mm+1 elements between 0 and mm
        #-- Multiplying gridded data with sin/cos of m#phis
        #-- This will sum through all phis in the dot product
        #-- output [m,theta]
        pfactor = PG * (R / rad_e)**(l + 2)
        dcos = np.dot(ccos, pfactor)
        dsin = np.dot(ssin, pfactor)
        #-- Summing product of plms and data over all latitudes
        #-- axis=1 signifies the direction of the summation (colatitude (th))
        #-- ycos and ysin are the SH coefficients before normalizing
        yclm[l, m] = np.sum(plm[l, m, :] * dcos[m, :], axis=1)
        yslm[l, m] = np.sum(plm[l, m, :] * dsin[m, :], axis=1)
        #-- Multiplying by factors to normalize
        clm[l, m] = dfactor[l] * yclm[l, m]
        slm[l, m] = dfactor[l] * yslm[l, m]

    #-- return the harmonics
    return {
        'clm': clm,
        'slm': slm,
        'l': np.arange(LMAX + 1),
        'm': np.arange(MMAX + 1)
    }
def gen_disc_load(data,
                  lon,
                  lat,
                  area,
                  LMAX=60,
                  MMAX=None,
                  PLM=None,
                  LOVE=None):
    """
    Calculates spherical harmonic coefficients for a uniform disc load

    Arguments
    ---------
    data: data magnitude in gigatonnes
    lon: longitude of disc center
    lat: latitude of disc center
    area: area of disc in km^2

    Keyword arguments
    -----------------
    LMAX: Upper bound of Spherical Harmonic Degrees
    MMAX: Upper bound of Spherical Harmonic Orders
    PLM: input Legendre polynomials
    LOVE: input load Love numbers up to degree LMAX (hl,kl,ll)

    Returns
    -------
    clm: cosine spherical harmonic coefficients
    slm: sine spherical harmonic coefficients
    l: spherical harmonic degree to LMAX
    m: spherical harmonic order to MMAX
    """

    #-- upper bound of spherical harmonic orders (default = LMAX)
    if MMAX is None:
        MMAX = np.copy(LMAX)

    #-- Earth Parameters
    factors = units(lmax=LMAX)
    rho_e = factors.rho_e  #-- Average Density of the Earth [g/cm^3]
    rad_e = factors.rad_e  #-- Average Radius of the Earth [cm]

    #-- convert lon and lat to radians
    phi = lon * np.pi / 180.0  #-- Longitude in radians
    th = (90.0 - lat) * np.pi / 180.0  #-- Colatitude in radians

    #-- convert input area into cm^2 and then divide by area of a half sphere
    #-- alpha will be 1 - the ratio of the input area with the half sphere
    alpha = (1.0 - 1e10 * area / (2.0 * np.pi * rad_e**2))

    #-- Input data is in gigatonnes (Gt)
    #-- 1e15 converts from Gt to grams, 1e10 converts from km^2 to cm^2
    unit_conv = 1e15 / (1e10 * area)

    #-- Coefficient for calculating Stokes coefficients for a disc load
    #-- From Jacob et al (2012), Farrell (1972) and Longman (1962)
    coeff = 3.0 / (rad_e * rho_e)

    #-- extract arrays of kl, hl, and ll Love Numbers
    hl, kl, ll = LOVE

    #-- calculate array of l values ranging from 0 to LMAX (harmonic degrees)
    #-- LMAX+1 as there are LMAX+1 elements between 0 and LMAX
    l = np.arange(LMAX + 1)

    #-- calculate SH degree dependent factors to convert from coefficients
    #-- of mass into normalized geoid coefficients
    #-- NOTE: these are not the normal factors for converting to geoid due
    #-- to the square of the denominator
    #-- kl[l] is the Load Love Number of degree l
    dfactor = (1.0 + kl[l]) / ((1.0 + 2.0 * l)**2)

    #-- Calculating plms of the disc
    #-- allocating for constructed array
    pl_alpha = np.zeros((LMAX + 1))
    #-- l=0 is a special case (P(-1) = 1, P(1) = cos(alpha))
    pl_alpha[0] = (1.0 - alpha) / 2.0
    #-- for all other degrees: calculate the legendre polynomials up to LMAX+1
    pl_matrix, dpl_matrix = legendre_polynomials(LMAX + 1, alpha)
    for l in range(1, LMAX + 1):  #-- LMAX+1 to include LMAX
        #-- from Longman (1962) and Jacob et al (2012)
        #-- unnormalizing Legendre polynomials
        #-- sqrt(2*l - 1) == sqrt(2*(l-1) + 1)
        #-- sqrt(2*l + 3) == sqrt(2*(l+1) + 1)
        pl_lower = pl_matrix[l - 1] / np.sqrt(2.0 * l - 1.0)
        pl_upper = pl_matrix[l + 1] / np.sqrt(2.0 * l + 3.0)
        pl_alpha[l] = (pl_lower - pl_upper) / 2.0

    #-- Calculate Legendre Polynomials using Holmes and Featherstone relation
    #-- this would be the plm for the center of the disc load
    #-- used to rotate the disc load to point lat/lon
    if PLM is None:
        plmout, dplm = plm_holmes(LMAX, np.cos(th))
        #-- truncate precomputed plms to order
        plmout = np.squeeze(plmout[:, :MMAX + 1, :])
    else:
        #-- truncate precomputed plms to degree and order
        plmout = PLM[:LMAX + 1, :MMAX + 1]

    #-- calculate array of m values ranging from 0 to MMAX (harmonic orders)
    #-- MMAX+1 as there are MMAX+1 elements between 0 and MMAX
    m = np.arange(MMAX + 1)
    #-- Multiplying by the units conversion factor (unit_conv) to
    #-- convert from the input units into cmH2O equivalent
    #-- Multiplying point mass data (converted to cmH2O) with sin/cos of m*phis
    #-- data normally is 1 for a uniform 1cm water equivalent layer
    #-- but can be a mass point if reconstructing a spherical harmonic field
    #-- NOTE: NOT a matrix multiplication as data (and phi) is a single point
    dcos = unit_conv * data * np.cos(m * phi)
    dsin = unit_conv * data * np.sin(m * phi)

    #-- Multiplying by plm_alpha (F_l from Jacob 2012)
    plm = np.zeros((LMAX + 1, MMAX + 1))
    #-- Initializing preliminary spherical harmonic matrices
    yclm = np.zeros((LMAX + 1, MMAX + 1))
    yslm = np.zeros((LMAX + 1, MMAX + 1))
    #-- Initializing output spherical harmonic matrices
    Ylms = {}
    Ylms['l'] = np.arange(LMAX + 1)
    Ylms['m'] = np.arange(MMAX + 1)
    Ylms['clm'] = np.zeros((LMAX + 1, MMAX + 1))
    Ylms['slm'] = np.zeros((LMAX + 1, MMAX + 1))
    for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
        l = np.arange(m, LMAX + 1)  #-- LMAX+1 to include LMAX
        #-- rotate disc load to be centered at lat/lon
        plm[l, m] = plmout[l, m] * pl_alpha[l]
        #-- multiplying clm by cos(m*phi) and slm by sin(m*phi)
        #-- to get a field of spherical harmonics
        yclm[l, m] = plm[l, m] * dcos[m]
        yslm[l, m] = plm[l, m] * dsin[m]
        #-- multiplying by coefficients to convert to geoid coefficients
        Ylms['clm'][l, m] = coeff * dfactor[l] * yclm[l, m]
        Ylms['slm'][l, m] = coeff * dfactor[l] * yslm[l, m]

    #-- return the output spherical harmonics
    return Ylms
def mascon_reconstruct(DSET,
                       LMAX,
                       RAD,
                       START=None,
                       END=None,
                       MMAX=None,
                       DESTRIPE=False,
                       LOVE_NUMBERS=0,
                       REFERENCE=None,
                       GIA=None,
                       GIA_FILE=None,
                       ATM=False,
                       DATAFORM=None,
                       MASCON_FILE=None,
                       REDISTRIBUTE_MASCONS=False,
                       RECONSTRUCT_FILE=None,
                       LANDMASK=None,
                       OUTPUT_DIRECTORY=None,
                       MODE=0o775):

    #-- for datasets not GSM: will add a label for the dataset
    dset_str = '' if (DSET == 'GSM') else '_{0}'.format(DSET)
    #-- atmospheric ECMWF "jump" flag (if ATM)
    atm_str = '_wATM' if ATM else ''
    #-- Gaussian smoothing string for radius RAD
    gw_str = '_r{0:0.0f}km'.format(RAD) if (RAD != 0) else ''
    #-- input GIA spherical harmonic datafiles
    GIA_Ylms_rate = read_GIA_model(GIA_FILE, GIA=GIA, LMAX=LMAX, MMAX=MMAX)
    gia_str = '_{0}'.format(GIA_Ylms_rate['title']) if GIA else ''
    #-- output string for both LMAX==MMAX and LMAX != MMAX cases
    MMAX = np.copy(LMAX) if not MMAX else MMAX
    order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else ''
    #-- filter grace coefficients flag
    ds_str = '_FL' if DESTRIPE else ''
    #-- output filename suffix
    suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')
    #-- file parser for reading index files
    #-- removes commented lines (can comment out files in the index)
    #-- removes empty lines (if there are extra empty lines)
    parser = re.compile(r'^(?!\#|\%|$)', re.VERBOSE)

    #-- create initial reconstruct index for calc_mascon.py
    fid = open(RECONSTRUCT_FILE, 'w')
    #-- output file format
    file_format = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}_{9:03d}-{10:03d}.{11}'

    #-- read load love numbers
    hl, kl, ll = load_love_numbers(LMAX,
                                   LOVE_NUMBERS=LOVE_NUMBERS,
                                   REFERENCE=REFERENCE)
    #-- Earth Parameters
    factors = units(lmax=LMAX).harmonic(hl, kl, ll)
    #-- Average Density of the Earth [g/cm^3]
    rho_e = factors.rho_e
    #-- Average Radius of the Earth [cm]
    rad_e = factors.rad_e
    #-- Read Ocean function and convert to Ylms for redistribution
    if REDISTRIBUTE_MASCONS:
        #-- read Land-Sea Mask and convert to spherical harmonics
        ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll))
        ocean_str = '_OCN'
    else:
        #-- not distributing uniformly over ocean
        ocean_str = ''

    #-- input mascon spherical harmonic datafiles
    with open(MASCON_FILE, 'r') as f:
        mascon_files = [l for l in f.read().splitlines() if parser.match(l)]
    for k, fi in enumerate(mascon_files):
        #-- read mascon spherical harmonics
        if (DATAFORM == 'ascii'):
            #-- ascii (.txt)
            Ylms = harmonics().from_ascii(os.path.expanduser(fi), date=False)
        elif (DATAFORM == 'netCDF4'):
            #-- netcdf (.nc)
            Ylms = harmonics().from_netCDF4(os.path.expanduser(fi), date=False)
        elif (DATAFORM == 'HDF5'):
            #-- HDF5 (.H5)
            Ylms = harmonics().from_HDF5(os.path.expanduser(fi), date=False)
        #-- Calculating the total mass of each mascon (1 cmwe uniform)
        total_area = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0
        #-- distribute mascon mass uniformly over the ocean
        if REDISTRIBUTE_MASCONS:
            #-- calculate ratio between total mascon mass and
            #-- a uniformly distributed cm of water over the ocean
            ratio = Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0]
            #-- for each spherical harmonic
            for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
                for l in range(m, LMAX + 1):  #-- LMAX+1 to include LMAX
                    #-- remove ratio*ocean Ylms from mascon Ylms
                    #-- note: x -= y is equivalent to x = x - y
                    Ylms.clm[l, m] -= ratio * ocean_Ylms.clm[l, m]
                    Ylms.slm[l, m] -= ratio * ocean_Ylms.slm[l, m]
        #-- truncate mascon spherical harmonics to d/o LMAX/MMAX
        Ylms = Ylms.truncate(lmax=LMAX, mmax=MMAX)
        #-- mascon base is the file without directory or suffix
        mascon_base = os.path.basename(fi)
        mascon_base = os.path.splitext(mascon_base)[0]
        #-- if lower case, will capitalize
        mascon_base = mascon_base.upper()
        #-- if mascon name contains degree and order info, remove
        mascon_name = mascon_base.replace('_L{0:d}'.format(LMAX), '')

        #-- input filename format (for both LMAX==MMAX and LMAX != MMAX cases):
        #-- mascon name, GRACE dataset, GIA model, LMAX, (MMAX,)
        #-- Gaussian smoothing, filter flag, remove reconstructed fields flag
        #-- output GRACE error file
        args = (mascon_name, dset_str, gia_str.upper(), atm_str, ocean_str,
                LMAX, order_str, gw_str, ds_str)
        file_input = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}.txt'.format(*args)
        mascon_data_input = np.loadtxt(
            os.path.join(OUTPUT_DIRECTORY, file_input))

        #-- convert mascon time-series from Gt to cmwe
        mascon_sigma = 1e15 * mascon_data_input[:, 2] / total_area
        #-- mascon time-series Ylms
        mascon_Ylms = Ylms.scale(mascon_sigma)
        mascon_Ylms.time = mascon_data_input[:, 1].copy()
        mascon_Ylms.month = mascon_data_input[:, 0].astype(np.int64)

        #-- output to file: no ascii option
        args = (mascon_name, dset_str, gia_str.upper(), atm_str, ocean_str,
                LMAX, order_str, gw_str, ds_str, START, END, suffix[DATAFORM])
        FILE = file_format.format(*args)
        #-- output harmonics to file
        if (DATAFORM == 'netCDF4'):
            #-- netcdf (.nc)
            mascon_Ylms.to_netCDF4(os.path.join(OUTPUT_DIRECTORY, FILE))
        elif (DATAFORM == 'HDF5'):
            #-- HDF5 (.H5)
            mascon_Ylms.to_HDF5(os.path.join(OUTPUT_DIRECTORY, FILE))
        #-- print file name to index
        print(tilde_compress(os.path.join(OUTPUT_DIRECTORY, FILE)), file=fid)
        #-- change the permissions mode
        os.chmod(os.path.join(OUTPUT_DIRECTORY, FILE), MODE)
    #-- close the reconstruct index
    fid.close()
    #-- change the permissions mode of the index file
    os.chmod(RECONSTRUCT_FILE, MODE)
def least_squares_mascons(parameters,
                          LOVE_NUMBERS=0,
                          REFERENCE=None,
                          MODE=0o775):
    #-- convert parameters to variables
    #-- index file of data files (containing path)
    #-- path.expanduser = tilde expansion of path
    INDEX_FILE = os.path.expanduser(parameters['INDEX_FILE'])
    DATAFORM = parameters['DATAFORM']
    #-- spherical harmonic degree range
    LMIN = np.int(parameters['LMIN'])
    LMAX = np.int(parameters['LMAX'])
    #-- maximum spherical harmonic order
    if (parameters['MMAX'].title() == 'None'):
        MMAX = np.copy(LMAX)
    else:
        MMAX = np.int(parameters['MMAX'])
    #-- gaussian smoothing radius
    RAD = np.float(parameters['RAD'])
    #-- filter coefficients for stripe effects
    DESTRIPE = parameters['DESTRIPE'] in ('Y', 'y')
    #-- index of mascons spherical harmonics
    MASCON_INDEX = os.path.expanduser(parameters['MASCON_INDEX'])
    #-- mascon distribution over the ocean
    MASCON_OCEAN = parameters['MASCON_OCEAN'] in ('Y', 'y')
    #-- destribute total mass of spherical harmonics over the ocean
    REDISTRIBUTE = parameters['REDISTRIBUTE'] in ('Y', 'y')
    #-- calculating with data errors
    DATA_ERROR = parameters['DATA_ERROR'] in ('Y', 'y')
    #-- input data have dates
    DATE = parameters['DATE'] in ('Y', 'y')
    #-- output directory
    DIRECTORY = os.path.expanduser(parameters['DIRECTORY'])
    #-- 1: fit mass, 2: fit geoid
    FIT_METHOD = np.int(parameters['FIT_METHOD'])

    #-- Recursively create output directory if not currently existing
    if (not os.access(DIRECTORY, os.F_OK)):
        os.makedirs(DIRECTORY, mode=MODE, exist_ok=True)

    #-- list object of output files for file logs (full path)
    output_files = []

    #-- read arrays of kl, hl, and ll Love Numbers
    hl, kl, ll = load_love_numbers(LMAX,
                                   LOVE_NUMBERS=LOVE_NUMBERS,
                                   REFERENCE='CF')

    #-- Earth Parameters
    factors = units(lmax=LMAX).harmonic(hl, kl, ll)
    #-- Average Density of the Earth [g/cm^3]
    rho_e = factors.rho_e
    #-- Average Radius of the Earth [cm]
    rad_e = factors.rad_e

    #-- Calculating the Gaussian smoothing for radius RAD
    if (RAD != 0):
        wt = 2.0 * np.pi * gauss_weights(RAD, LMAX)
        gw_str = '_r{0:0.0f}km'.format(RAD)
    else:
        #-- else = 1
        wt = np.ones((LMAX + 1))
        gw_str = ''

    #-- output string for both LMAX==MMAX and LMAX != MMAX cases
    order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else ''
    #-- output string for destriped harmonics
    ds_str = '_FL' if DESTRIPE else ''

    #-- Read Ocean function and convert to Ylms for redistribution
    if (MASCON_OCEAN | REDISTRIBUTE):
        #-- read Land-Sea Mask and convert to spherical harmonics
        LANDMASK = os.path.expanduser(parameters['LANDMASK'])
        ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll))
        ocean_str = '_OCN'
    else:
        #-- not distributing uniformly over ocean
        ocean_str = ''

    #-- input spherical harmonic datafile index
    #-- correspond file names with GRACE month
    #-- this allows additional months to be in the index
    data_Ylms = harmonics().from_index(INDEX_FILE, DATAFORM, date=DATE)
    #-- number of files within the index
    n_files = data_Ylms.shape[-1]
    #-- truncate to degree and order
    data_Ylms = data_Ylms.truncate(lmax=LMAX, mmax=MMAX)
    #-- Total mass in the simulated input data in gigatonnes
    rmass = 4.0 * np.pi * (rad_e**
                           3.0) * rho_e * data_Ylms.clm[0, 0, :] / 3.0 / 1e15
    #-- distribute Ylms uniformly over the ocean
    if REDISTRIBUTE:
        #-- calculate ratio between total removed mass and
        #-- a uniformly distributed cm of water over the ocean
        ratio = data_Ylms.clm[0, 0, :] / ocean_Ylms['clm'][0, 0]
        #-- for each spherical harmonic
        for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
            for l in range(m, LMAX + 1):  #-- LMAX+1 to include LMAX
                #-- remove the ratio*ocean Ylms from Ylms
                #-- note: x -= y is equivalent to x = x - y
                data_Ylms.clm[l, m, :] -= ratio * ocean_Ylms['clm'][l, m]
                data_Ylms.slm[l, m, :] -= ratio * ocean_Ylms['slm'][l, m]
    #-- filter data coefficients
    if DESTRIPE:
        data_Ylms = data_Ylms.destripe()

    #-- input mascon spherical harmonic datafiles
    with open(MASCON_INDEX, 'r') as f:
        mascon_files = f.read().splitlines()
    #-- number of mascons
    n_mas = len(mascon_files)
    #-- spatial area of the mascon
    area_tot = np.zeros((n_mas))
    #-- name of each mascon
    mascon_name = []
    #-- for each valid file in the index (iterate over mascons)
    mascon_list = []
    for k in range(n_mas):
        #-- read mascon spherical harmonics
        if (DATAFORM == 'ascii'):
            #-- ascii (.txt)
            Ylms = harmonics().from_ascii(mascon_files[k], date=False)
        elif (DATAFORM == 'netCDF4'):
            #-- netcdf (.nc)
            Ylms = harmonics().from_netCDF4(mascon_files[k], date=False)
        elif (DATAFORM == 'HDF5'):
            #-- HDF5 (.H5)
            Ylms = harmonics().from_HDF5(mascon_files[k], date=False)
        #-- Calculating the total mass of each mascon (1 cmH2O uniform)
        area_tot[k] = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0
        #-- distribute MASCON mass uniformly over the ocean
        if MASCON_OCEAN:
            #-- calculate ratio between total mascon mass and
            #-- a uniformly distributed cm of water over the ocean
            ratio = Ylms.clm[0, 0] / ocean_Ylms['clm'][0, 0]
            #-- for each spherical harmonic
            for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
                for l in range(m, LMAX + 1):  #-- LMAX+1 to include LMAX
                    #-- remove ratio*ocean Ylms from mascon Ylms
                    #-- note: x -= y is equivalent to x = x - y
                    Ylms.clm[l, m] -= ratio * ocean_Ylms['clm'][l, m]
                    Ylms.slm[l, m] -= ratio * ocean_Ylms['slm'][l, m]
        #-- truncate mascon spherical harmonics to d/o LMAX/MMAX and add to list
        mascon_list.append(Ylms.truncate(lmax=LMAX, mmax=MMAX))
        #-- mascon base is the file without directory or suffix
        mascon_base = os.path.basename(mascon_files[k])
        mascon_base = os.path.splitext(mascon_base)[0]
        #-- if lower case, will capitalize
        mascon_base = mascon_base.upper()
        #-- if mascon name contains degree and order info, remove
        mascon_name.append(mascon_base.replace('_L{0:d}'.format(LMAX), ''))
    #-- create single harmonics object from list
    mascon_Ylms = harmonics().from_list(mascon_list, date=False)

    #-- Calculating the number of cos and sin harmonics between LMIN and LMAX
    #-- taking into account MMAX (if MMAX == LMAX then LMAX-MMAX=0)
    n_harm = np.int(LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX)**2 -
                    (LMAX - MMAX))

    #-- Initialing harmonics for least squares fitting
    #-- mascon kernel
    M_lm = np.zeros((n_harm, n_mas))
    #-- mascon kernel converted to output unit
    MA_lm = np.zeros((n_harm, n_mas))
    #-- corrected clm and slm
    Y_lm = np.zeros((n_harm, n_files))
    #-- sensitivity kernel
    A_lm = np.zeros((n_harm, n_mas))
    #-- Initializing output Mascon time-series
    mascon = np.zeros((n_mas, n_files))
    #-- Initializing conversion factors
    #-- factor for converting to coefficients of mass
    fact = np.zeros((n_harm))
    #-- smoothing factor
    wt_lm = np.zeros((n_harm))

    #-- ii is a counter variable for building the mascon column array
    ii = 0
    #-- Creating column array of clm/slm coefficients
    #-- Order is [C00...C6060,S11...S6060]
    coeff = rho_e * rad_e / 3.0
    #-- Switching between Cosine and Sine Stokes
    for cs, csharm in enumerate(['clm', 'slm']):
        #-- copy cosine and sin harmonics
        mascon_harm = getattr(mascon_Ylms, csharm)
        data_harm = getattr(data_Ylms, csharm)
        #-- for each spherical harmonic degree
        #-- +1 to include LMAX
        for l in range(LMIN, LMAX + 1):
            #-- for each spherical harmonic order
            #-- Sine Stokes for (m=0) = 0
            mm = np.min([MMAX, l])
            #-- +1 to include l or MMAX (whichever is smaller)
            for m in range(cs, mm + 1):
                #-- Mascon Spherical Harmonics
                M_lm[ii, :] = np.copy(mascon_harm[l, m, :])
                #-- Data Spherical Harmonics
                Y_lm[ii, :] = np.copy(data_harm[l, m, :])
                #-- degree dependent factor to convert to mass
                fact[ii] = (2.0 * l + 1.0) / (1.0 + kl[l])
                #-- degree dependent smoothing
                wt_lm[ii] = np.copy(wt[l])
                #-- add 1 to counter
                ii += 1
    #-- free up memory from data harmonics
    data_Ylms.clm = None
    data_Ylms.slm = None

    #-- Converting mascon coefficients to fit method
    if (FIT_METHOD == 1):
        #-- Fitting Sensitivity Kernel as mass coefficients
        #-- converting M_lm to mass coefficients of the kernel
        for i in range(n_harm):
            MA_lm[i, :] = wt_lm[i] * M_lm[i, :] * fact[i]
        fit_factor = wt_lm * fact
    else:
        #-- Fitting Sensitivity Kernel as geoid coefficients
        MA_lm[:, :] = np.copy(M_lm)
        fit_factor = wt_lm * np.ones((n_harm))

    #-- Fitting the sensitivity kernel from the input kernel
    for i in range(n_harm):
        #-- setting kern_i equal to 1 for d/o
        kern_i = np.zeros((n_harm))
        #-- converting to mass coefficients if specified
        kern_i[i] = 1.0 * fit_factor[i]
        #-- spherical harmonics solution for the
        #-- mascon sensitivity kernels
        #-- Least Squares Solutions: Inv(X'.X).(X'.Y)
        kern_lm = np.linalg.lstsq(MA_lm, kern_i, rcond=-1)[0]
        for k in range(n_mas):
            A_lm[i, k] = kern_lm[k] * area_tot[k]

    #-- output formatting string if containing date variables
    if DATE:
        formatting_string = '{0:03d} {1:12.4f} {2:16.10f} {3:16.5f} {4:16.10f}'
    else:
        formatting_string = '{0:16.10f} {1:16.5f} {2:16.10f}'

    #-- for each mascon
    for k in range(n_mas):
        #-- output filename format:
        #-- mascon name, LMAX, Gaussian smoothing radii, filter flag
        file_out = '{0}{1}_L{2:d}{3}{4}{5}{6}.txt'.format(
            parameters['FILENAME'], mascon_name[k], LMAX, order_str, gw_str,
            ds_str, ocean_str)
        #-- add output files to list object
        output_files.append(os.path.join(DIRECTORY, file_out))

        #-- Output mascon datafiles
        #-- Will output each mascon mass series
        #-- if with dates:
        #-- month, date, mascon mass, mascon area
        #-- else:
        #-- mascon mass, mascon area
        #-- open output mascon time-series file
        fid = open(os.path.join(DIRECTORY, file_out), 'w')
        #-- for each date
        for f in range(n_files):
            #-- Summing over all spherical harmonics for mascon k, and time t
            #-- multiplies by the degree dependent factor to convert
            #-- the harmonics into mass coefficients
            #-- Converting mascon mass time-series from g to gigatonnes
            if DATA_ERROR:
                #-- if input data are errors (i.e. estimated dealiasing errors)
                mascon[k, f] = np.sqrt(np.sum(
                    (A_lm[:, k] * Y_lm[:, f])**2)) / 1e15
            else:
                mascon[k, f] = np.sum(A_lm[:, k] * Y_lm[:, f]) / 1e15
            #-- output to file
            if DATE:
                #-- if files contain date information
                args = (data_Ylms.month[f], data_Ylms.time[f], mascon[k, f],
                        area_tot[k] / 1e10, rmass[f])
                print(formatting_string.format(*args), file=fid)
            else:
                #-- just print the time-series and mascon areas
                args = (mascon[k, f], area_tot[k] / 1e10, rmass[f])
                print(formatting_string.format(*args), file=fid)
        #-- close the output file
        fid.close()
        #-- change the permissions mode of the output file
        os.chmod(os.path.join(DIRECTORY, file_out), MODE)
        #-- add output files to list object
        output_files.append(os.path.join(DIRECTORY, file_out))

    #-- return the list of output files
    return output_files
Example #7
0
def gen_spherical_cap(data,
                      lon,
                      lat,
                      LMAX=60,
                      MMAX=None,
                      AREA=0,
                      RAD_CAP=0,
                      RAD_KM=0,
                      UNITS=1,
                      PLM=None,
                      LOVE=None):
    """
    Calculates spherical harmonic coefficients for a spherical cap

    Arguments
    ---------
    data: data magnitude
    lon: longitude of spherical cap center
    lat: latitude of spherical cap center

    Keyword arguments
    -----------------
    LMAX: Upper bound of Spherical Harmonic Degrees
    MMAX: Upper bound of Spherical Harmonic Orders
    AREA: spherical cap area in cm^2
    UNITS: input data units
        1: cm of water thickness (default)
        2: gigatonnes of mass
        3: kg/m^2
    PLM: input Legendre polynomials
    LOVE: input load Love numbers up to degree LMAX (hl,kl,ll)

    Returns
    -------
    clm: cosine spherical harmonic coefficients
    slm: sine spherical harmonic coefficients
    l: spherical harmonic degree to LMAX
    m: spherical harmonic order to MMAX
    """

    #-- upper bound of spherical harmonic orders (default = LMAX)
    if MMAX is None:
        MMAX = np.copy(LMAX)

    #-- Earth Parameters
    factors = units(lmax=LMAX)
    rho_e = factors.rho_e  #-- Average Density of the Earth [g/cm^3]
    rad_e = factors.rad_e  #-- Average Radius of the Earth [cm]

    #-- convert lon and lat to radians
    phi = lon * np.pi / 180.0  #-- Longitude in radians
    th = (90.0 - lat) * np.pi / 180.0  #-- Colatitude in radians

    #-- Converting input area into an equivalent spherical cap radius
    #-- Following Jacob et al. (2012) Equation 4 and 5
    #-- alpha is the vertical semi-angle subtending a cone at the
    #-- center of the earth
    if (RAD_CAP != 0):
        #-- if given spherical cap radius in degrees
        #-- converting to radians
        alpha = RAD_CAP * np.pi / 180.0
    elif (AREA != 0):
        #-- if given spherical cap area in cm^2
        #-- radius in centimeters
        radius_cm = np.sqrt(AREA / np.pi)
        #-- Calculating angular radius of spherical cap
        alpha = (radius_cm / rad_e)
    elif (RAD_KM != 0):
        #-- if given spherical cap radius in kilometers
        #-- Calculating angular radius of spherical cap
        alpha = (1e5 * RAD_KM) / rad_e
    else:
        raise ValueError('Input RAD_CAP, AREA or RAD_KM of spherical cap')

    #-- Calculate factor to convert from input units into cmH2O equivalent
    #-- Default input is for inputs already in cmH2O (unit_conv = 1)
    if (UNITS == 1):
        #-- Input data is in cm water equivalent (cmH2O)
        unit_conv = 1.0
    elif (UNITS == 2):
        #-- Input data is in gigatonnes (Gt)
        #-- calculate spherical cap area from angular radius
        area = np.pi * (alpha * rad_e)**2
        #-- the 1.e15 converts from gigatons/cm^2 to cm of water
        #-- 1 g/cm^3 = 1000 kg/m^3 = density water
        #-- 1 Gt = 1 Pg = 1.e15 g
        unit_conv = 1.e15 / area
    elif (UNITS == 3):
        #-- Input data is in kg/m^2
        #-- 1 kg = 1000 g
        #-- 1 m^2 = 100*100 cm^2 = 1e4 cm^2
        unit_conv = 0.1
    else:
        raise ValueError('UNITS (1: cmH2O, 2: Gt, 3: kg/m^2)')

    #-- Coefficient for calculating Stokes coefficients for a spherical cap
    #-- From Jacob et al (2012), Farrell (1972) and Longman (1962)
    coeff = 3.0 / (rad_e * rho_e)

    #-- extract arrays of kl, hl, and ll Love Numbers
    hl, kl, ll = LOVE

    #-- calculate array of l values ranging from 0 to LMAX (harmonic degrees)
    #-- LMAX+1 as there are LMAX+1 elements between 0 and LMAX
    l = np.arange(LMAX + 1)
    #-- calculate SH degree dependent factors to convert from coefficients
    #-- of mass into normalized geoid coefficients
    #-- NOTE: these are not the normal factors for converting to geoid due
    #-- to the square of the denominator
    #-- kl[l] is the Load Love Number of degree l
    dfactor = (1.0 + kl[l]) / ((1.0 + 2.0 * l)**2)

    #-- Calculating plms of the spherical caps
    #-- From Longman et al. (1962)
    #-- pl_alpha = F(alpha) from Jacob 2011
    #-- pl_alpha is purely zonal and depends only on the size of the cap
    #-- allocating for constructed array
    pl_alpha = np.zeros((LMAX + 1))
    #-- l=0 is a special case (P(-1) = 1, P(1) = cos(alpha))
    pl_alpha[0] = (1.0 - np.cos(alpha)) / 2.0
    #-- for all other degrees: calculate the legendre polynomials up to LMAX+1
    pl_matrix, dpl_matrix = legendre_polynomials(LMAX + 1, np.cos(alpha))
    for l in range(1, LMAX + 1):  #-- LMAX+1 to include LMAX
        #-- from Longman (1962) and Jacob et al (2012)
        #-- unnormalizing Legendre polynomials
        #-- sqrt(2*l - 1) == sqrt(2*(l-1) + 1)
        #-- sqrt(2*l + 3) == sqrt(2*(l+1) + 1)
        pl_lower = pl_matrix[l - 1] / np.sqrt(2.0 * l - 1.0)
        pl_upper = pl_matrix[l + 1] / np.sqrt(2.0 * l + 3.0)
        pl_alpha[l] = (pl_lower - pl_upper) / 2.0

    #-- Calculating Legendre Polynomials
    #-- added option to precompute plms to improve computational speed
    #-- this would be the plm for the center of the spherical cap
    #-- used to rotate the spherical cap to point lat/lon
    if PLM is None:
        plmout, dplm = plm_holmes(LMAX, np.cos(th))
        #-- truncate precomputed plms to order
        plmout = np.squeeze(plmout[:, :MMAX + 1, :])
    else:
        #-- truncate precomputed plms to degree and order
        plmout = PLM[:LMAX + 1, :MMAX + 1]

    #-- calculate array of m values ranging from 0 to MMAX (harmonic orders)
    #-- MMAX+1 as there are MMAX+1 elements between 0 and MMAX
    m = np.arange(MMAX + 1)
    #-- Multiplying by the units conversion factor (unit_conv) to
    #-- convert from the input units into cmH2O equivalent
    #-- Multiplying point mass data (converted to cmH2O) with sin/cos of m*phis
    #-- data normally is 1 for a uniform 1cm water equivalent layer
    #-- but can be a mass point if reconstructing a spherical harmonic field
    #-- NOTE: NOT a matrix multiplication as data (and phi) is a single point
    dcos = unit_conv * data * np.cos(m * phi)
    dsin = unit_conv * data * np.sin(m * phi)

    #-- Multiplying by plm_alpha (F_l from Jacob 2012)
    plm = np.zeros((LMAX + 1, MMAX + 1))
    #-- Initializing preliminary spherical harmonic matrices
    yclm = np.zeros((LMAX + 1, MMAX + 1))
    yslm = np.zeros((LMAX + 1, MMAX + 1))
    #-- Initializing output spherical harmonic matrices
    Ylms = {}
    Ylms['l'] = np.arange(LMAX + 1)
    Ylms['m'] = np.arange(MMAX + 1)
    Ylms['clm'] = np.zeros((LMAX + 1, MMAX + 1))
    Ylms['slm'] = np.zeros((LMAX + 1, MMAX + 1))
    for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
        l = np.arange(m, LMAX + 1)  #-- LMAX+1 to include LMAX
        #-- rotate spherical cap to be centered at lat/lon
        plm[l, m] = plmout[l, m] * pl_alpha[l]
        #-- multiplying clm by cos(m*phi) and slm by sin(m*phi)
        #-- to get a field of spherical harmonics
        yclm[l, m] = plm[l, m] * dcos[m]
        yslm[l, m] = plm[l, m] * dsin[m]
        #-- multiplying by coefficients to convert to geoid coefficients
        Ylms['clm'][l, m] = coeff * dfactor[l] * yclm[l, m]
        Ylms['slm'][l, m] = coeff * dfactor[l] * yslm[l, m]

    #-- return the output spherical harmonics
    return Ylms
def grace_spatial_maps(base_dir,
                       PROC,
                       DREL,
                       DSET,
                       LMAX,
                       RAD,
                       START=None,
                       END=None,
                       MISSING=None,
                       LMIN=None,
                       MMAX=None,
                       LOVE_NUMBERS=0,
                       REFERENCE=None,
                       DESTRIPE=False,
                       UNITS=None,
                       DDEG=None,
                       INTERVAL=None,
                       BOUNDS=None,
                       GIA=None,
                       GIA_FILE=None,
                       ATM=False,
                       POLE_TIDE=False,
                       DEG1=None,
                       DEG1_FILE=None,
                       MODEL_DEG1=False,
                       SLR_C20=None,
                       SLR_21=None,
                       SLR_22=None,
                       SLR_C30=None,
                       SLR_C50=None,
                       DATAFORM=None,
                       MEAN_FILE=None,
                       MEANFORM=None,
                       REMOVE_FILES=None,
                       REMOVE_FORMAT=None,
                       REDISTRIBUTE_REMOVED=False,
                       LANDMASK=None,
                       OUTPUT_DIRECTORY=None,
                       FILE_PREFIX=None,
                       VERBOSE=False,
                       MODE=0o775):

    #-- recursively create output directory if not currently existing
    if not os.access(OUTPUT_DIRECTORY, os.F_OK):
        os.makedirs(OUTPUT_DIRECTORY, mode=MODE, exist_ok=True)

    #-- list object of output files for file logs (full path)
    output_files = []

    #-- file information
    suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')

    #-- read arrays of kl, hl, and ll Love Numbers
    hl, kl, ll = load_love_numbers(LMAX,
                                   LOVE_NUMBERS=LOVE_NUMBERS,
                                   REFERENCE=REFERENCE)

    #-- Calculating the Gaussian smoothing for radius RAD
    if (RAD != 0):
        wt = 2.0 * np.pi * gauss_weights(RAD, LMAX)
        gw_str = '_r{0:0.0f}km'.format(RAD)
    else:
        #-- else = 1
        wt = np.ones((LMAX + 1))
        gw_str = ''

    #-- flag for spherical harmonic order
    MMAX = np.copy(LMAX) if not MMAX else MMAX
    order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else ''

    #-- reading GRACE months for input date range
    #-- replacing low-degree harmonics with SLR values if specified
    #-- include degree 1 (geocenter) harmonics if specified
    #-- correcting for Pole-Tide and Atmospheric Jumps if specified
    Ylms = grace_input_months(base_dir,
                              PROC,
                              DREL,
                              DSET,
                              LMAX,
                              START,
                              END,
                              MISSING,
                              SLR_C20,
                              DEG1,
                              MMAX=MMAX,
                              SLR_21=SLR_21,
                              SLR_22=SLR_22,
                              SLR_C30=SLR_C30,
                              SLR_C50=SLR_C50,
                              DEG1_FILE=DEG1_FILE,
                              MODEL_DEG1=MODEL_DEG1,
                              ATM=ATM,
                              POLE_TIDE=POLE_TIDE)
    #-- convert to harmonics object and remove mean if specified
    GRACE_Ylms = harmonics().from_dict(Ylms)
    GRACE_Ylms.directory = Ylms['directory']
    #-- use a mean file for the static field to remove
    if MEAN_FILE:
        #-- read data form for input mean file (ascii, netCDF4, HDF5, gfc)
        mean_Ylms = harmonics().from_file(MEAN_FILE,
                                          format=MEANFORM,
                                          date=False)
        #-- remove the input mean
        GRACE_Ylms.subtract(mean_Ylms)
    else:
        GRACE_Ylms.mean(apply=True)
    #-- date information of GRACE/GRACE-FO coefficients
    nfiles = len(GRACE_Ylms.time)

    #-- filter GRACE/GRACE-FO coefficients
    if DESTRIPE:
        #-- destriping GRACE/GRACE-FO coefficients
        ds_str = '_FL'
        GRACE_Ylms = GRACE_Ylms.destripe()
    else:
        #-- using standard GRACE/GRACE-FO harmonics
        ds_str = ''

    #-- input GIA spherical harmonic datafiles
    GIA_Ylms_rate = read_GIA_model(GIA_FILE, GIA=GIA, LMAX=LMAX, MMAX=MMAX)
    gia_str = '_{0}'.format(GIA_Ylms_rate['title']) if GIA else ''
    #-- calculate the monthly mass change from GIA
    GIA_Ylms = GRACE_Ylms.zeros_like()
    GIA_Ylms.time[:] = np.copy(GRACE_Ylms.time)
    GIA_Ylms.month[:] = np.copy(GRACE_Ylms.month)
    #-- monthly GIA calculated by gia_rate*time elapsed
    #-- finding change in GIA each month
    for t in range(nfiles):
        GIA_Ylms.clm[:, :,
                     t] = GIA_Ylms_rate['clm'] * (GIA_Ylms.time[t] - 2003.3)
        GIA_Ylms.slm[:, :,
                     t] = GIA_Ylms_rate['slm'] * (GIA_Ylms.time[t] - 2003.3)

    #-- default file prefix
    if not FILE_PREFIX:
        fargs = (PROC, DREL, DSET, Ylms['title'], gia_str)
        FILE_PREFIX = '{0}_{1}_{2}{3}{4}_'.format(*fargs)

    #-- Read Ocean function and convert to Ylms for redistribution
    if REDISTRIBUTE_REMOVED:
        #-- read Land-Sea Mask and convert to spherical harmonics
        ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll))
        ocean_str = '_OCN'
    else:
        ocean_str = ''

    #-- input spherical harmonic datafiles to be removed from the GRACE data
    #-- Remove sets of Ylms from the GRACE data before returning
    remove_Ylms = GRACE_Ylms.zeros_like()
    remove_Ylms.time[:] = np.copy(GRACE_Ylms.time)
    remove_Ylms.month[:] = np.copy(GRACE_Ylms.month)
    if REMOVE_FILES:
        #-- extend list if a single format was entered for all files
        if len(REMOVE_FORMAT) < len(REMOVE_FILES):
            REMOVE_FORMAT = REMOVE_FORMAT * len(REMOVE_FILES)
        #-- for each file to be removed
        for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT):
            if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'):
                #-- ascii (.txt)
                #-- netCDF4 (.nc)
                #-- HDF5 (.H5)
                Ylms = harmonics().from_file(REMOVE_FILE, format=REMOVEFORM)
            elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'):
                #-- read from index file
                _, removeform = REMOVEFORM.split('-')
                #-- index containing files in data format
                Ylms = harmonics().from_index(REMOVE_FILE, format=removeform)
            #-- reduce to GRACE/GRACE-FO months and truncate to degree and order
            Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX, mmax=MMAX)
            #-- distribute removed Ylms uniformly over the ocean
            if REDISTRIBUTE_REMOVED:
                #-- calculate ratio between total removed mass and
                #-- a uniformly distributed cm of water over the ocean
                ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0]
                #-- for each spherical harmonic
                for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
                    for l in range(m, LMAX + 1):  #-- LMAX+1 to include LMAX
                        #-- remove the ratio*ocean Ylms from Ylms
                        #-- note: x -= y is equivalent to x = x - y
                        Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m]
                        Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m]
            #-- filter removed coefficients
            if DESTRIPE:
                Ylms = Ylms.destripe()
            #-- add data for month t and INDEX_FILE to the total
            #-- remove_clm and remove_slm matrices
            #-- redistributing the mass over the ocean if specified
            remove_Ylms.add(Ylms)

    #-- Output spatial data object
    grid = spatial()
    #-- Output Degree Spacing
    dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1])
    #-- Output Degree Interval
    if (INTERVAL == 1):
        #-- (-180:180,90:-90)
        nlon = np.int64((360.0 / dlon) + 1.0)
        nlat = np.int64((180.0 / dlat) + 1.0)
        grid.lon = -180 + dlon * np.arange(0, nlon)
        grid.lat = 90.0 - dlat * np.arange(0, nlat)
    elif (INTERVAL == 2):
        #-- (Degree spacing)/2
        grid.lon = np.arange(-180 + dlon / 2.0, 180 + dlon / 2.0, dlon)
        grid.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat)
        nlon = len(grid.lon)
        nlat = len(grid.lat)
    elif (INTERVAL == 3):
        #-- non-global grid set with BOUNDS parameter
        minlon, maxlon, minlat, maxlat = BOUNDS.copy()
        grid.lon = np.arange(minlon + dlon / 2.0, maxlon + dlon / 2.0, dlon)
        grid.lat = np.arange(maxlat - dlat / 2.0, minlat - dlat / 2.0, -dlat)
        nlon = len(grid.lon)
        nlat = len(grid.lat)

    #-- Computing plms for converting to spatial domain
    theta = (90.0 - grid.lat) * np.pi / 180.0
    PLM, dPLM = plm_holmes(LMAX, np.cos(theta))

    #-- Earth Parameters
    #-- output spatial units
    unit_list = ['cmwe', 'mmGH', 'mmCU', u'\u03BCGal', 'mbar']
    unit_name = [
        'Equivalent Water Thickness', 'Geoid Height', 'Elastic Crustal Uplift',
        'Gravitational Undulation', 'Equivalent Surface Pressure'
    ]
    #-- Setting units factor for output
    #-- dfactor computes the degree dependent coefficients
    if (UNITS == 1):
        #-- 1: cmwe, centimeters water equivalent
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).cmwe
    elif (UNITS == 2):
        #-- 2: mmGH, mm geoid height
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmGH
    elif (UNITS == 3):
        #-- 3: mmCU, mm elastic crustal deformation
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmCU
    elif (UNITS == 4):
        #-- 4: micGal, microGal gravity perturbations
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).microGal
    elif (UNITS == 5):
        #-- 5: mbar, millibars equivalent surface pressure
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mbar
    else:
        raise ValueError('Invalid units code {0:d}'.format(UNITS))

    #-- output file format
    file_format = '{0}{1}_L{2:d}{3}{4}{5}_{6:03d}.{7}'
    #-- converting harmonics to truncated, smoothed coefficients in units
    #-- combining harmonics to calculate output spatial fields
    for i, grace_month in enumerate(GRACE_Ylms.month):
        #-- GRACE/GRACE-FO harmonics for time t
        Ylms = GRACE_Ylms.index(i)
        #-- Remove GIA rate for time
        Ylms.subtract(GIA_Ylms.index(i))
        #-- Remove monthly files to be removed
        Ylms.subtract(remove_Ylms.index(i))
        #-- smooth harmonics and convert to output units
        Ylms.convolve(dfactor * wt)
        #-- convert spherical harmonics to output spatial grid
        grid.data = harmonic_summation(Ylms.clm,
                                       Ylms.slm,
                                       grid.lon,
                                       grid.lat,
                                       LMIN=LMIN,
                                       LMAX=LMAX,
                                       MMAX=MMAX,
                                       PLM=PLM).T
        #-- copy time variables for month
        grid.time = np.copy(Ylms.time)
        grid.month = np.copy(Ylms.month)

        #-- output monthly files to ascii, netCDF4 or HDF5
        args = (FILE_PREFIX, unit_list[UNITS - 1], LMAX, order_str, gw_str,
                ds_str, grace_month, suffix[DATAFORM])
        FILE = os.path.join(OUTPUT_DIRECTORY, file_format.format(*args))
        if (DATAFORM == 'ascii'):
            #-- ascii (.txt)
            grid.to_ascii(FILE, date=True, verbose=VERBOSE)
        elif (DATAFORM == 'netCDF4'):
            #-- netCDF4
            grid.to_netCDF4(FILE,
                            date=True,
                            verbose=VERBOSE,
                            units=unit_list[UNITS - 1],
                            longname=unit_name[UNITS - 1],
                            title='GRACE/GRACE-FO Spatial Data')
        elif (DATAFORM == 'HDF5'):
            #-- HDF5
            grid.to_HDF5(FILE,
                         date=True,
                         verbose=VERBOSE,
                         units=unit_list[UNITS - 1],
                         longname=unit_name[UNITS - 1],
                         title='GRACE/GRACE-FO Spatial Data')
        #-- set the permissions mode of the output files
        os.chmod(FILE, MODE)
        #-- add file to list
        output_files.append(FILE)

    #-- return the list of output files
    return output_files
Example #9
0
def gen_stokes(data,
               lon,
               lat,
               LMIN=0,
               LMAX=60,
               MMAX=None,
               UNITS=1,
               PLM=None,
               LOVE=None):
    """
    Converts data from the spatial domain to spherical harmonic coefficients

    Arguments
    ---------
    data: data matrix
    lon: longitude array
    lat: latitude array

    Keyword arguments
    -----------------
    LMIN: Lower bound of Spherical Harmonic Degrees
    LMAX: Upper bound of Spherical Harmonic Degrees
    MMAX: Upper bound of Spherical Harmonic Orders
    UNITS: input data units
        1: cm of water thickness
        2: Gtons of mass
        3: kg/m^2
    PLM: input Legendre polynomials
    LOVE: input load Love numbers up to degree LMAX (hl,kl,ll)

    Returns
    -------
    clm: cosine spherical harmonic coefficients
    slm: sine spherical harmonic coefficients
    l: spherical harmonic degree to LMAX
    m: spherical harmonic order to MMAX
    """

    #-- converting LMIN and LMAX to integer
    LMIN = np.int(LMIN)
    LMAX = np.int(LMAX)
    #-- upper bound of spherical harmonic orders (default = LMAX)
    MMAX = np.copy(LMAX) if (MMAX is None) else MMAX

    #-- grid dimensions
    nlat = np.int(len(lat))
    #-- grid step
    dlon = np.abs(lon[1] - lon[0])
    dlat = np.abs(lat[1] - lat[0])
    #-- longitude degree spacing in radians
    dphi = dlon * np.pi / 180.0
    #-- colatitude degree spacing in radians
    dth = dlat * np.pi / 180.0

    #-- reformatting longitudes to range 0:360 (if previously -180:180)
    lon = np.squeeze(lon.copy())
    if np.any(lon < 0):
        lon_ind, = np.nonzero(lon < 0)
        lon[lon_ind] += 360.0
    #-- Longitude in radians
    phi = lon[np.newaxis, :] * np.pi / 180.0
    #-- Colatitude in radians
    th = (90.0 - np.squeeze(lat.copy())) * np.pi / 180.0

    #-- reforming data to lonXlat if input latXlon
    sz = np.shape(data)
    data = data.T if (sz[0] == nlat) else np.copy(data)

    #-- SH Degree dependent factors to convert into geodesy normalized SH's
    #-- use splat operator to extract arrays of kl, hl, and ll Love Numbers
    factors = units(lmax=LMAX).spatial(*LOVE)

    #-- extract degree dependent factor for specific units
    #-- calculate integration factors for theta and phi
    #-- Multiplying sin(th) with differentials of theta and phi
    #-- to calculate the integration factor at each latitude
    int_fact = np.zeros((nlat))
    if (UNITS == 1):
        #-- Default Parameter: Input in cm w.e. (g/cm^2)
        dfactor = factors.cmwe
        int_fact[:] = np.sin(th) * dphi * dth
    elif (UNITS == 2):
        #-- Input in gigatonnes (Gt)
        dfactor = factors.cmwe
        #-- rad_e: Average Radius of the Earth [cm]
        int_fact[:] = 1e15 / (factors.rad_e**2)
    elif (UNITS == 3):
        #-- Input in kg/m^2 (mm w.e.)
        dfactor = factors.mmwe
        int_fact[:] = np.sin(th) * dphi * dth
    else:
        #-- default is cm w.e. (g/cm^2)
        dfactor = factors.cmwe
        int_fact[:] = np.sin(th) * dphi * dth

    #-- Calculating cos/sin of phi arrays
    #-- output [m,phi]
    m = np.arange(MMAX + 1)
    ccos = np.cos(np.dot(m[:, np.newaxis], phi))
    ssin = np.sin(np.dot(m[:, np.newaxis], phi))

    #-- Calculating fully-normalized Legendre Polynomials
    #-- Output is plm[l,m,th]
    plm = np.zeros((LMAX + 1, MMAX + 1, nlat))
    #-- added option to precompute plms to improve computational speed
    if PLM is None:
        #-- if plms are not pre-computed: calculate Legendre polynomials
        PLM, dPLM = plm_holmes(LMAX, np.cos(th))

    #-- Multiplying by integration factors [sin(theta)*dtheta*dphi]
    #-- truncate legendre polynomials to spherical harmonic order MMAX
    for j in range(0, nlat):
        plm[:, m, j] = PLM[:, m, j] * int_fact[j]

    #-- Initializing preliminary spherical harmonic matrices
    yclm = np.zeros((LMAX + 1, MMAX + 1))
    yslm = np.zeros((LMAX + 1, MMAX + 1))
    #-- Initializing output spherical harmonic matrices
    clm = np.zeros((LMAX + 1, MMAX + 1))
    slm = np.zeros((LMAX + 1, MMAX + 1))
    #-- Multiplying gridded data with sin/cos of m#phis
    #-- This will sum through all phis in the dot product
    #-- output [m,theta]
    dcos = np.dot(ccos, data)
    dsin = np.dot(ssin, data)
    for l in range(LMIN, LMAX + 1):  #-- equivalent to LMIN:LMAX
        mm = np.min([MMAX, l])  #-- truncate to MMAX if specified (if l > MMAX)
        m = np.arange(0, mm + 1)  #-- mm+1 elements between 0 and mm
        #-- Summing product of plms and data over all latitudes
        #-- axis=1 signifies the direction of the summation
        yclm[l, m] = np.sum(plm[l, m, :] * dcos[m, :], axis=1)
        yslm[l, m] = np.sum(plm[l, m, :] * dsin[m, :], axis=1)
        #-- Multiplying by factors to convert to geodesy normalized coefficients
        clm[l, m] = dfactor[l] * yclm[l, m]
        slm[l, m] = dfactor[l] * yslm[l, m]

    return {
        'clm': clm,
        'slm': slm,
        'l': np.arange(LMAX + 1),
        'm': np.arange(MMAX + 1)
    }
def calc_sensitivity_kernel(LMAX,
                            RAD,
                            LMIN=None,
                            MMAX=None,
                            LOVE_NUMBERS=0,
                            REFERENCE=None,
                            DATAFORM=None,
                            MASCON_FILE=None,
                            REDISTRIBUTE_MASCONS=False,
                            FIT_METHOD=0,
                            LANDMASK=None,
                            DDEG=None,
                            INTERVAL=None,
                            OUTPUT_DIRECTORY=None,
                            MODE=0o775):

    #-- file information
    suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')
    #-- file parser for reading index files
    #-- removes commented lines (can comment out files in the index)
    #-- removes empty lines (if there are extra empty lines)
    parser = re.compile(r'^(?!\#|\%|$)', re.VERBOSE)

    #-- Create output Directory if not currently existing
    if (not os.access(OUTPUT_DIRECTORY, os.F_OK)):
        os.mkdir(OUTPUT_DIRECTORY)

    #-- list object of output files for file logs (full path)
    output_files = []

    #-- read arrays of kl, hl, and ll Love Numbers
    hl, kl, ll = load_love_numbers(LMAX,
                                   LOVE_NUMBERS=LOVE_NUMBERS,
                                   REFERENCE=REFERENCE)

    #-- Earth Parameters
    factors = units(lmax=LMAX).harmonic(hl, kl, ll)
    #-- Average Density of the Earth [g/cm^3]
    rho_e = factors.rho_e
    #-- Average Radius of the Earth [cm]
    rad_e = factors.rad_e

    #-- input/output string for both LMAX==MMAX and LMAX != MMAX cases
    MMAX = np.copy(LMAX) if not MMAX else MMAX
    order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else ''

    #-- Calculating the Gaussian smoothing for radius RAD
    if (RAD != 0):
        wt = 2.0 * np.pi * gauss_weights(RAD, LMAX)
        gw_str = '_r{0:0.0f}km'.format(RAD)
    else:
        #-- else = 1
        wt = np.ones((LMAX + 1))
        gw_str = ''

    #-- Read Ocean function and convert to Ylms for redistribution
    if REDISTRIBUTE_MASCONS:
        #-- read Land-Sea Mask and convert to spherical harmonics
        ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll))
        ocean_str = '_OCN'
    else:
        #-- not distributing uniformly over ocean
        ocean_str = ''

    #-- input mascon spherical harmonic datafiles
    with open(MASCON_FILE, 'r') as f:
        mascon_files = [l for l in f.read().splitlines() if parser.match(l)]
    #-- number of mascons
    n_mas = len(mascon_files)
    #-- spatial area of the mascon
    total_area = np.zeros((n_mas))
    #-- name of each mascon
    mascon_name = []
    #-- for each valid file in the index (iterate over mascons)
    mascon_list = []
    for k, fi in enumerate(mascon_files):
        #-- read mascon spherical harmonics
        Ylms = harmonics().from_file(os.path.expanduser(fi),
                                     format=DATAFORM,
                                     date=False)
        #-- Calculating the total mass of each mascon (1 cmwe uniform)
        total_area[k] = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0
        #-- distribute mascon mass uniformly over the ocean
        if REDISTRIBUTE_MASCONS:
            #-- calculate ratio between total mascon mass and
            #-- a uniformly distributed cm of water over the ocean
            ratio = Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0]
            #-- for each spherical harmonic
            for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
                for l in range(m, LMAX + 1):  #-- LMAX+1 to include LMAX
                    #-- remove ratio*ocean Ylms from mascon Ylms
                    #-- note: x -= y is equivalent to x = x - y
                    Ylms.clm[l, m] -= ratio * ocean_Ylms.clm[l, m]
                    Ylms.slm[l, m] -= ratio * ocean_Ylms.slm[l, m]
        #-- truncate mascon spherical harmonics to d/o LMAX/MMAX and add to list
        mascon_list.append(Ylms.truncate(lmax=LMAX, mmax=MMAX))
        #-- mascon base is the file without directory or suffix
        mascon_base = os.path.basename(mascon_files[k])
        mascon_base = os.path.splitext(mascon_base)[0]
        #-- if lower case, will capitalize
        mascon_base = mascon_base.upper()
        #-- if mascon name contains degree and order info, remove
        mascon_name.append(mascon_base.replace('_L{0:d}'.format(LMAX), ''))
    #-- create single harmonics object from list
    mascon_Ylms = harmonics().from_list(mascon_list, date=False)

    #-- Output spatial data object
    grid = spatial()
    #-- Output Degree Spacing
    dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1])
    #-- Output Degree Interval
    if (INTERVAL == 1):
        #-- (-180:180,90:-90)
        n_lon = np.int64((360.0 / dlon) + 1.0)
        n_lat = np.int64((180.0 / dlat) + 1.0)
        grid.lon = -180 + dlon * np.arange(0, n_lon)
        grid.lat = 90.0 - dlat * np.arange(0, n_lat)
    elif (INTERVAL == 2):
        #-- (Degree spacing)/2
        grid.lon = np.arange(-180 + dlon / 2.0, 180 + dlon / 2.0, dlon)
        grid.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat)
        n_lon = len(grid.lon)
        n_lat = len(grid.lat)

    #-- Computing plms for converting to spatial domain
    theta = (90.0 - grid.lat) * np.pi / 180.0
    PLM, dPLM = plm_holmes(LMAX, np.cos(theta))

    #-- Calculating the number of cos and sin harmonics between LMIN and LMAX
    #-- taking into account MMAX (if MMAX == LMAX then LMAX-MMAX=0)
    n_harm = np.int64(LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX)**2 -
                      (LMAX - MMAX))

    #-- Initialing harmonics for least squares fitting
    #-- mascon kernel
    M_lm = np.zeros((n_harm, n_mas))
    #-- mascon kernel converted to output unit
    MA_lm = np.zeros((n_harm, n_mas))
    #-- sensitivity kernel
    A_lm = np.zeros((n_harm, n_mas))
    #-- Initializing conversion factors
    #-- factor for converting to smoothed coefficients of mass
    fact = np.zeros((n_harm))
    #-- factor for converting back into geoid coefficients
    fact_inv = np.zeros((n_harm))
    #-- smoothing factor
    wt_lm = np.zeros((n_harm))

    #-- ii is a counter variable for building the mascon column array
    ii = 0
    #-- Creating column array of clm/slm coefficients
    #-- Order is [C00...C6060,S11...S6060]
    #-- Calculating factor to convert geoid spherical harmonic coefficients
    #-- to coefficients of mass (Wahr, 1998)
    coeff = rho_e * rad_e / 3.0
    coeff_inv = 0.75 / (np.pi * rho_e * rad_e**3)
    #-- Switching between Cosine and Sine Stokes
    for cs, csharm in enumerate(['clm', 'slm']):
        #-- copy cosine and sin harmonics
        mascon_harm = getattr(mascon_Ylms, csharm)
        #-- for each spherical harmonic degree
        #-- +1 to include LMAX
        for l in range(LMIN, LMAX + 1):
            #-- for each spherical harmonic order
            #-- Sine Stokes for (m=0) = 0
            mm = np.min([MMAX, l])
            #-- +1 to include l or MMAX (whichever is smaller)
            for m in range(cs, mm + 1):
                #-- Mascon Spherical Harmonics
                M_lm[ii, :] = np.copy(mascon_harm[l, m, :])
                #-- degree dependent factor to convert to mass
                fact[ii] = (2.0 * l + 1.0) / (1.0 + kl[l])
                #-- degree dependent factor to convert from mass
                fact_inv[ii] = coeff_inv * (1.0 + kl[l]) / (2.0 * l + 1.0)
                #-- degree dependent smoothing
                wt_lm[ii] = np.copy(wt[l])
                #-- add 1 to counter
                ii += 1

    #-- Converting mascon coefficients to fit method
    if (FIT_METHOD == 1):
        #-- Fitting Sensitivity Kernel as mass coefficients
        #-- converting M_lm to mass coefficients of the kernel
        for i in range(n_harm):
            MA_lm[i, :] = M_lm[i, :] * wt_lm[i] * fact[i]
        fit_factor = wt_lm * fact
        inv_fit_factor = np.copy(fact_inv)
    else:
        #-- Fitting Sensitivity Kernel as geoid coefficients
        for i in range(n_harm):
            MA_lm[:, :] = M_lm[i, :] * wt_lm[i]
        fit_factor = wt_lm * np.ones((n_harm))
        inv_fit_factor = np.ones((n_harm))

    #-- Fitting the sensitivity kernel from the input kernel
    for i in range(n_harm):
        #-- setting kern_i equal to 1 for d/o
        kern_i = np.zeros((n_harm))
        #-- converting to mass coefficients if specified
        kern_i[i] = 1.0 * fit_factor[i]
        #-- spherical harmonics solution for the
        #-- mascon sensitivity kernels
        #-- Least Squares Solutions: Inv(X'.X).(X'.Y)
        kern_lm = np.linalg.lstsq(MA_lm, kern_i, rcond=-1)[0]
        for k in range(n_mas):
            A_lm[i, k] = kern_lm[k] * total_area[k]

    #-- for each mascon
    for k in range(n_mas):
        #-- reshaping harmonics of sensitivity kernel to LMAX+1,MMAX+1
        #-- calculating the spatial sensitivity kernel of each mascon
        #-- kernel calculated as outlined in Tiwari (2009) and Jacobs (2012)
        #-- Initializing output sensitivity kernel (both spatial and Ylms)
        kern_Ylms = harmonics(lmax=LMAX, mmax=MMAX)
        kern_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1))
        kern_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1))
        kern_Ylms.time = total_area[k]
        #-- counter variable for deconstructing the mascon column arrays
        ii = 0
        #-- Switching between Cosine and Sine Stokes
        for cs, csharm in enumerate(['clm', 'slm']):
            #-- for each spherical harmonic degree
            #-- +1 to include LMAX
            for l in range(LMIN, LMAX + 1):
                #-- for each spherical harmonic order
                #-- Sine Stokes for (m=0) = 0
                mm = np.min([MMAX, l])
                #-- +1 to include l or MMAX (whichever is smaller)
                for m in range(cs, mm + 1):
                    #-- inv_fit_factor: normalize from mass harmonics
                    temp = getattr(kern_Ylms, csharm)
                    temp[l, m] = inv_fit_factor[ii] * A_lm[ii, k]
                    #-- add 1 to counter
                    ii += 1

        #-- convert spherical harmonics to output spatial grid
        grid.data = harmonic_summation(kern_Ylms.clm,
                                       kern_Ylms.slm,
                                       grid.lon,
                                       grid.lat,
                                       LMAX=LMAX,
                                       MMAX=MMAX,
                                       PLM=PLM).T
        grid.time = total_area[k]

        #-- output names for sensitivity kernel Ylm and spatial files
        #-- for both LMAX==MMAX and LMAX != MMAX cases
        args = (mascon_name[k], ocean_str, LMAX, order_str, gw_str,
                suffix[DATAFORM])
        FILE1 = '{0}_SKERNEL_CLM{1}_L{2:d}{3}{4}.{5}'.format(*args)
        FILE2 = '{0}_SKERNEL{1}_L{2:d}{3}{4}.{5}'.format(*args)
        #-- output sensitivity kernel to file
        if (DATAFORM == 'ascii'):
            #-- ascii (.txt)
            kern_Ylms.to_ascii(os.path.join(OUTPUT_DIRECTORY, FILE1),
                               date=False)
            grid.to_ascii(os.path.join(OUTPUT_DIRECTORY, FILE2),
                          date=False,
                          units='unitless',
                          longname='Sensitivity_Kernel')
        elif (DATAFORM == 'netCDF4'):
            #-- netCDF4 (.nc)
            kern_Ylms.to_netCDF4(os.path.join(OUTPUT_DIRECTORY, FILE1),
                                 date=False)
            grid.to_netCDF4(os.path.join(OUTPUT_DIRECTORY, FILE2),
                            date=False,
                            units='unitless',
                            longname='Sensitivity_Kernel')
        elif (DATAFORM == 'HDF5'):
            #-- netcdf (.H5)
            kern_Ylms.to_HDF5(os.path.join(OUTPUT_DIRECTORY, FILE1),
                              date=False)
            grid.to_HDF5(os.path.join(OUTPUT_DIRECTORY, FILE2),
                         date=False,
                         units='unitless',
                         longname='Sensitivity_Kernel')
        #-- change the permissions mode
        os.chmod(os.path.join(OUTPUT_DIRECTORY, FILE1), MODE)
        os.chmod(os.path.join(OUTPUT_DIRECTORY, FILE2), MODE)
        #-- add output files to list object
        output_files.append(os.path.join(OUTPUT_DIRECTORY, FILE1))
        output_files.append(os.path.join(OUTPUT_DIRECTORY, FILE2))

    #-- return the list of output files
    return output_files
Example #11
0
def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD,
    START=None,
    END=None,
    MISSING=None,
    LMIN=None,
    MMAX=None,
    LOVE_NUMBERS=0,
    REFERENCE=None,
    DESTRIPE=False,
    DDEG=None,
    INTERVAL=None,
    GIA=None,
    GIA_FILE=None,
    ATM=False,
    POLE_TIDE=False,
    DEG1=None,
    DEG1_FILE=None,
    MODEL_DEG1=False,
    SLR_C20=None,
    SLR_21=None,
    SLR_22=None,
    SLR_C30=None,
    SLR_C50=None,
    DATAFORM=None,
    MEAN_FILE=None,
    MEANFORM=None,
    REMOVE_FILES=None,
    REMOVE_FORMAT=None,
    REDISTRIBUTE_REMOVED=False,
    SCALE_FILE=None,
    ERROR_FILE=None,
    POWER_FILE=None,
    LANDMASK=None,
    OUTPUT_DIRECTORY=None,
    FILE_PREFIX=None,
    VERBOSE=False,
    MODE=0o775):

    #-- recursively create output Directory if not currently existing
    if not os.access(OUTPUT_DIRECTORY, os.F_OK):
        os.makedirs(OUTPUT_DIRECTORY, mode=MODE, exist_ok=True)

    #-- list object of output files for file logs (full path)
    output_files = []

    #-- file information
    suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')
    #-- output file format
    file_format = '{0}{1}{2}_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}'

    #-- read arrays of kl, hl, and ll Love Numbers
    hl,kl,ll = load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS,
        REFERENCE=REFERENCE)

    #-- atmospheric ECMWF "jump" flag (if ATM)
    atm_str = '_wATM' if ATM else ''
    #-- output string for both LMAX==MMAX and LMAX != MMAX cases
    MMAX = np.copy(LMAX) if not MMAX else MMAX
    order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else ''
    #-- output spatial units
    unit_str = 'cmwe'
    unit_name = 'Equivalent Water Thickness'
    #-- invalid value
    fill_value = -9999.0

    #-- Calculating the Gaussian smoothing for radius RAD
    if (RAD != 0):
        wt = 2.0*np.pi*gauss_weights(RAD,LMAX)
        gw_str = '_r{0:0.0f}km'.format(RAD)
    else:
        #-- else = 1
        wt = np.ones((LMAX+1))
        gw_str = ''

    #-- Read Ocean function and convert to Ylms for redistribution
    if REDISTRIBUTE_REMOVED:
        #-- read Land-Sea Mask and convert to spherical harmonics
        ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl,kl,ll))

    #-- Grid spacing
    dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1])
    #-- Grid dimensions
    if (INTERVAL == 1):#-- (0:360, 90:-90)
        nlon = np.int64((360.0/dlon)+1.0)
        nlat = np.int64((180.0/dlat)+1.0)
    elif (INTERVAL == 2):#-- degree spacing/2
        nlon = np.int64((360.0/dlon))
        nlat = np.int64((180.0/dlat))

    #-- read data for input scale files (ascii, netCDF4, HDF5)
    if (DATAFORM == 'ascii'):
        kfactor = spatial(spacing=[dlon,dlat],nlat=nlat,nlon=nlon).from_ascii(
            SCALE_FILE,date=False)
        k_error = spatial(spacing=[dlon,dlat],nlat=nlat,nlon=nlon).from_ascii(
            ERROR_FILE,date=False)
        k_power = spatial(spacing=[dlon,dlat],nlat=nlat,nlon=nlon).from_ascii(
            POWER_FILE,date=False)
    elif (DATAFORM == 'netCDF4'):
        kfactor = spatial().from_netCDF4(SCALE_FILE,date=False)
        k_error = spatial().from_netCDF4(ERROR_FILE,date=False)
        k_power = spatial().from_netCDF4(POWER_FILE,date=False)
    elif (DATAFORM == 'HDF5'):
        kfactor = spatial().from_HDF5(SCALE_FILE,date=False)
        k_error = spatial().from_HDF5(ERROR_FILE,date=False)
        k_power = spatial().from_HDF5(POWER_FILE,date=False)
    #-- input data shape
    nlat,nlon = kfactor.shape

    #-- input GRACE/GRACE-FO spherical harmonic datafiles for date range
    #-- replacing low-degree harmonics with SLR values if specified
    #-- include degree 1 (geocenter) harmonics if specified
    #-- correcting for Pole-Tide and Atmospheric Jumps if specified
    Ylms = grace_input_months(base_dir, PROC, DREL, DSET, LMAX,
        START, END, MISSING, SLR_C20, DEG1, MMAX=MMAX,
        SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C50=SLR_C50,
        DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM,
        POLE_TIDE=POLE_TIDE)
    #-- create harmonics object from GRACE/GRACE-FO data
    GRACE_Ylms = harmonics().from_dict(Ylms)
    GRACE_Ylms.directory = Ylms['directory']
    #-- use a mean file for the static field to remove
    if MEAN_FILE:
        #-- read data form for input mean file (ascii, netCDF4, HDF5, gfc)
        mean_Ylms = harmonics().from_file(MEAN_FILE,format=MEANFORM,date=False)
        #-- remove the input mean
        GRACE_Ylms.subtract(mean_Ylms)
    else:
        GRACE_Ylms.mean(apply=True)
    #-- date information of GRACE/GRACE-FO coefficients
    nfiles = len(GRACE_Ylms.time)

    #-- filter GRACE/GRACE-FO coefficients
    if DESTRIPE:
        #-- destriping GRACE/GRACE-FO coefficients
        ds_str = '_FL'
        GRACE_Ylms = GRACE_Ylms.destripe()
    else:
        #-- using standard GRACE/GRACE-FO harmonics
        ds_str = ''

    #-- input GIA spherical harmonic datafiles
    GIA_Ylms_rate = read_GIA_model(GIA_FILE,GIA=GIA,LMAX=LMAX,MMAX=MMAX)
    gia_str = '_{0}'.format(GIA_Ylms_rate['title']) if GIA else ''
    #-- calculate the monthly mass change from GIA
    GIA_Ylms = GRACE_Ylms.zeros_like()
    GIA_Ylms.time[:] = np.copy(GRACE_Ylms.time)
    GIA_Ylms.month[:] = np.copy(GRACE_Ylms.month)
    #-- monthly GIA calculated by gia_rate*time elapsed
    #-- finding change in GIA each month
    for t in range(nfiles):
        GIA_Ylms.clm[:,:,t] = GIA_Ylms_rate['clm']*(GIA_Ylms.time[t]-2003.3)
        GIA_Ylms.slm[:,:,t] = GIA_Ylms_rate['slm']*(GIA_Ylms.time[t]-2003.3)

    #-- default file prefix
    if not FILE_PREFIX:
        fargs = (PROC,DREL,DSET,Ylms['title'],gia_str)
        FILE_PREFIX = '{0}_{1}_{2}{3}{4}_'.format(*fargs)

    #-- input spherical harmonic datafiles to be removed from the GRACE data
    #-- Remove sets of Ylms from the GRACE data before returning
    remove_Ylms = GRACE_Ylms.zeros_like()
    remove_Ylms.time[:] = np.copy(GRACE_Ylms.time)
    remove_Ylms.month[:] = np.copy(GRACE_Ylms.month)
    if REMOVE_FILES:
        #-- extend list if a single format was entered for all files
        if len(REMOVE_FORMAT) < len(REMOVE_FILES):
            REMOVE_FORMAT = REMOVE_FORMAT*len(REMOVE_FILES)
        #-- for each file to be removed
        for REMOVE_FILE,REMOVEFORM in zip(REMOVE_FILES,REMOVE_FORMAT):
            if REMOVEFORM in ('ascii','netCDF4','HDF5'):
                #-- ascii (.txt)
                #-- netCDF4 (.nc)
                #-- HDF5 (.H5)
                Ylms = harmonics().from_file(REMOVE_FILE, format=REMOVEFORM)
            elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'):
                #-- read from index file
                _,removeform = REMOVEFORM.split('-')
                #-- index containing files in data format
                Ylms = harmonics().from_index(REMOVE_FILE, format=removeform)
            #-- reduce to GRACE/GRACE-FO months and truncate to degree and order
            Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX,mmax=MMAX)
            #-- distribute removed Ylms uniformly over the ocean
            if REDISTRIBUTE_REMOVED:
                #-- calculate ratio between total removed mass and
                #-- a uniformly distributed cm of water over the ocean
                ratio = Ylms.clm[0,0,:]/ocean_Ylms.clm[0,0]
                #-- for each spherical harmonic
                for m in range(0,MMAX+1):#-- MMAX+1 to include MMAX
                    for l in range(m,LMAX+1):#-- LMAX+1 to include LMAX
                        #-- remove the ratio*ocean Ylms from Ylms
                        #-- note: x -= y is equivalent to x = x - y
                        Ylms.clm[l,m,:] -= ratio*ocean_Ylms.clm[l,m]
                        Ylms.slm[l,m,:] -= ratio*ocean_Ylms.slm[l,m]
            #-- filter removed coefficients
            if DESTRIPE:
                Ylms = Ylms.destripe()
            #-- add data for month t and INDEX_FILE to the total
            #-- remove_clm and remove_slm matrices
            #-- redistributing the mass over the ocean if specified
            remove_Ylms.add(Ylms)

    #-- calculating GRACE/GRACE-FO error (Wahr et al. 2006)
    #-- output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases)
    args = (PROC,DREL,DSET,LMAX,order_str,ds_str,atm_str,GRACE_Ylms.month[0],
        GRACE_Ylms.month[-1], suffix[DATAFORM])
    delta_format = '{0}_{1}_{2}_DELTA_CLM_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}'
    DELTA_FILE = os.path.join(GRACE_Ylms.directory,delta_format.format(*args))
    #-- check full path of the GRACE directory for delta file
    #-- if file was previously calculated: will read file
    #-- else: will calculate the GRACE/GRACE-FO error
    if not os.access(DELTA_FILE, os.F_OK):
        #-- add output delta file to list object
        output_files.append(DELTA_FILE)

        #-- Delta coefficients of GRACE time series (Error components)
        delta_Ylms = harmonics(lmax=LMAX,mmax=MMAX)
        delta_Ylms.clm = np.zeros((LMAX+1,MMAX+1))
        delta_Ylms.slm = np.zeros((LMAX+1,MMAX+1))
        #-- Smoothing Half-Width (CNES is a 10-day solution)
        #-- All other solutions are monthly solutions (HFWTH for annual = 6)
        if ((PROC == 'CNES') and (DREL in ('RL01','RL02'))):
            HFWTH = 19
        else:
            HFWTH = 6
        #-- Equal to the noise of the smoothed time-series
        #-- for each spherical harmonic order
        for m in range(0,MMAX+1):#-- MMAX+1 to include MMAX
            #-- for each spherical harmonic degree
            for l in range(m,LMAX+1):#-- LMAX+1 to include LMAX
                #-- Delta coefficients of GRACE time series
                for cs,csharm in enumerate(['clm','slm']):
                    #-- calculate GRACE Error (Noise of smoothed time-series)
                    #-- With Annual and Semi-Annual Terms
                    val1 = getattr(GRACE_Ylms, csharm)
                    smth = tssmooth(GRACE_Ylms.time, val1[l,m,:], HFWTH=HFWTH)
                    #-- number of smoothed points
                    nsmth = len(smth['data'])
                    tsmth = np.mean(smth['time'])
                    #-- GRACE delta Ylms
                    #-- variance of data-(smoothed+annual+semi)
                    val2 = getattr(delta_Ylms, csharm)
                    val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth)

        #-- save GRACE/GRACE-FO delta harmonics to file
        delta_Ylms.time = np.copy(tsmth)
        delta_Ylms.month = np.int64(nsmth)
        delta_Ylms.to_file(DELTA_FILE,format=DATAFORM)
    else:
        #-- read GRACE/GRACE-FO delta harmonics from file
        delta_Ylms = harmonics().from_file(DELTA_FILE,format=DATAFORM)
        #-- copy time and number of smoothed fields
        tsmth = np.squeeze(delta_Ylms.time)
        nsmth = np.int64(delta_Ylms.month)

    #-- Output spatial data object
    grid = spatial()
    grid.lon = np.copy(kfactor.lon)
    grid.lat = np.copy(kfactor.lat)
    grid.time = np.zeros((nfiles))
    grid.month = np.zeros((nfiles),dtype=np.int64)
    grid.data = np.zeros((nlat,nlon,nfiles))
    grid.mask = np.zeros((nlat,nlon,nfiles),dtype=bool)

    #-- Computing plms for converting to spatial domain
    phi = grid.lon[np.newaxis,:]*np.pi/180.0
    theta = (90.0-grid.lat)*np.pi/180.0
    PLM,dPLM = plm_holmes(LMAX,np.cos(theta))
    #-- square of legendre polynomials truncated to order MMAX
    mm = np.arange(0,MMAX+1)
    PLM2 = PLM[:,mm,:]**2

    #-- dfactor is the degree dependent coefficients
    #-- for converting to centimeters water equivalent (cmwe)
    dfactor = units(lmax=LMAX).harmonic(hl,kl,ll).cmwe

    #-- converting harmonics to truncated, smoothed coefficients in units
    #-- combining harmonics to calculate output spatial fields
    for i,gm in enumerate(GRACE_Ylms.month):
        #-- GRACE/GRACE-FO harmonics for time t
        Ylms = GRACE_Ylms.index(i)
        #-- Remove GIA rate for time
        Ylms.subtract(GIA_Ylms.index(i))
        #-- Remove monthly files to be removed
        Ylms.subtract(remove_Ylms.index(i))
        #-- smooth harmonics and convert to output units
        Ylms.convolve(dfactor*wt)
        #-- convert spherical harmonics to output spatial grid
        grid.data[:,:,i] = harmonic_summation(Ylms.clm, Ylms.slm,
            grid.lon, grid.lat, LMAX=LMAX, MMAX=MMAX, PLM=PLM).T
        #-- copy time variables for month
        grid.time[i] = np.copy(Ylms.time)
        grid.month[i] = np.copy(Ylms.month)
    #-- update spacing and dimensions
    grid.update_spacing()
    grid.update_extents()
    grid.update_dimensions()

    #-- scale output data with kfactor
    grid = grid.scale(kfactor.data)
    grid.replace_invalid(fill_value, mask=kfactor.mask)

    #-- output monthly files to ascii, netCDF4 or HDF5
    args = (FILE_PREFIX,'',unit_str,LMAX,order_str,gw_str,ds_str,
        grid.month[0],grid.month[-1],suffix[DATAFORM])
    FILE=os.path.join(OUTPUT_DIRECTORY,file_format.format(*args))
    if (DATAFORM == 'ascii'):
        #-- ascii (.txt)
        grid.to_ascii(FILE, date=True, verbose=VERBOSE)
    elif (DATAFORM == 'netCDF4'):
        #-- netCDF4
        grid.to_netCDF4(FILE, date=True, verbose=VERBOSE,
            units=unit_str, longname=unit_name,
            title='GRACE/GRACE-FO Spatial Data')
    elif (DATAFORM == 'HDF5'):
        #-- HDF5
        grid.to_HDF5(FILE, date=True, verbose=VERBOSE,
            units=unit_str, longname=unit_name,
            title='GRACE/GRACE-FO Spatial Data')
    #-- set the permissions mode of the output files
    os.chmod(FILE, MODE)
    #-- add file to list
    output_files.append(FILE)

    #-- calculate power of scaled GRACE/GRACE-FO data
    scaled_power = grid.sum(power=2.0).power(0.5)
    #-- calculate residual leakage errors
    #-- scaled by ratio of GRACE and synthetic power
    ratio = scaled_power.scale(k_power.power(-1).data)
    error = k_error.scale(ratio.data)

    #-- output monthly error files to ascii, netCDF4 or HDF5
    args = (FILE_PREFIX,'ERROR_',unit_str,LMAX,order_str,gw_str,ds_str,
        grid.month[0],grid.month[-1],suffix[DATAFORM])
    FILE = os.path.join(OUTPUT_DIRECTORY,file_format.format(*args))
    if (DATAFORM == 'ascii'):
        #-- ascii (.txt)
        error.to_ascii(FILE, date=False, verbose=VERBOSE)
    elif (DATAFORM == 'netCDF4'):
        #-- netCDF4
        error.to_netCDF4(FILE, date=False, verbose=VERBOSE,
            units=unit_str, longname=unit_name,
            title='GRACE/GRACE-FO Scaling Error')
    elif (DATAFORM == 'HDF5'):
        #-- HDF5
        error.to_HDF5(FILE, date=False, verbose=VERBOSE,
            units=unit_str, longname=unit_name,
            title='GRACE/GRACE-FO Scaling Error')
    #-- set the permissions mode of the output files
    os.chmod(FILE, MODE)
    #-- add file to list
    output_files.append(FILE)

    #-- Output spatial data object
    delta = spatial()
    delta.lon = np.copy(kfactor.lon)
    delta.lat = np.copy(kfactor.lat)
    delta.time = np.copy(tsmth)
    delta.month = np.copy(nsmth)
    delta.data = np.zeros((nlat,nlon))
    delta.mask = np.zeros((nlat,nlon),dtype=bool)
    #-- calculate scaled spatial error
    #-- Calculating cos(m*phi)^2 and sin(m*phi)^2
    m = delta_Ylms.m[:,np.newaxis]
    ccos = np.cos(np.dot(m,phi))**2
    ssin = np.sin(np.dot(m,phi))**2

    #-- truncate delta harmonics to spherical harmonic range
    Ylms = delta_Ylms.truncate(LMAX,lmin=LMIN,mmax=MMAX)
    #-- convolve delta harmonics with degree dependent factors
    #-- smooth harmonics and convert to output units
    Ylms = Ylms.convolve(dfactor*wt).power(2.0).scale(1.0/nsmth)
    #-- Calculate fourier coefficients
    d_cos = np.zeros((MMAX+1,nlat))#-- [m,th]
    d_sin = np.zeros((MMAX+1,nlat))#-- [m,th]
    #-- Calculating delta spatial values
    for k in range(0,nlat):
        #-- summation over all spherical harmonic degrees
        d_cos[:,k] = np.sum(PLM2[:,:,k]*Ylms.clm, axis=0)
        d_sin[:,k] = np.sum(PLM2[:,:,k]*Ylms.slm, axis=0)
    #-- Multiplying by c/s(phi#m) to get spatial error map
    delta.data[:] = np.sqrt(np.dot(ccos.T,d_cos) + np.dot(ssin.T,d_sin)).T
    #-- update spacing and dimensions
    delta.update_spacing()
    delta.update_extents()
    delta.update_dimensions()

    #-- scale output harmonic errors with kfactor
    delta = delta.scale(kfactor.data)
    delta.replace_invalid(fill_value, mask=kfactor.mask)

    #-- output monthly files to ascii, netCDF4 or HDF5
    args = (FILE_PREFIX,'DELTA_',unit_str,LMAX,order_str,gw_str,ds_str,
        grid.month[0],grid.month[-1],suffix[DATAFORM])
    FILE=os.path.join(OUTPUT_DIRECTORY,file_format.format(*args))
    if (DATAFORM == 'ascii'):
        #-- ascii (.txt)
        delta.to_ascii(FILE, date=True, verbose=VERBOSE)
    elif (DATAFORM == 'netCDF4'):
        #-- netCDF4
        delta.to_netCDF4(FILE, date=True, verbose=VERBOSE,
            units=unit_str, longname=unit_name,
            title='GRACE/GRACE-FO Spatial Error')
    elif (DATAFORM == 'HDF5'):
        #-- HDF5
        delta.to_HDF5(FILE, date=True, verbose=VERBOSE,
            units=unit_str, longname=unit_name,
            title='GRACE/GRACE-FO Spatial Error')
    #-- set the permissions mode of the output files
    os.chmod(FILE, MODE)
    #-- add file to list
    output_files.append(FILE)

    #-- return the list of output files
    return output_files
def mascon_reconstruct(parameters, LOVE_NUMBERS=0, REFERENCE=None, MODE=0o775):
    #-- convert parameters into variables
    #-- Data processing center
    PROC = parameters['PROC']
    #-- Data Release
    DREL = parameters['DREL']
    #-- GRACE dataset
    DSET = parameters['DSET']
    #-- Date Range
    START_MON = np.int(parameters['START'])
    END_MON = np.int(parameters['END'])
    #-- spherical harmonic parameters
    LMAX = np.int(parameters['LMAX'])
    #-- maximum spherical harmonic order
    if (parameters['MMAX'].title() == 'None'):
        MMAX = np.copy(LMAX)
    else:
        MMAX = np.int(parameters['MMAX'])
    #-- gaussian smoothing radius
    RAD = np.float(parameters['RAD'])
    #-- filtered coefficients for stripe effects
    DESTRIPE = parameters['DESTRIPE'] in ('Y', 'y')
    #-- ECMWF jump corrections
    ATM = parameters['ATM'] in ('Y', 'y')
    #-- Glacial Isostatic Adjustment file to read
    GIA = parameters['GIA'] if (parameters['GIA'].title() != 'None') else None
    GIA_FILE = os.path.expanduser(parameters['GIA_FILE'])
    #-- input/output data format (ascii, netCDF4, HDF5)
    DATAFORM = parameters['DATAFORM']
    #-- index of mascons spherical harmonics
    #-- path.expanduser = tilde expansion of path
    MASCON_INDEX = os.path.expanduser(parameters['MASCON_INDEX'])
    #-- mascon distribution over the ocean
    MASCON_OCEAN = parameters['MASCON_OCEAN'] in ('Y', 'y')

    #-- for datasets not GSM: will add a label for the dataset
    dset_str = '' if (DSET == 'GSM') else '_{0}'.format(DSET)
    #-- atmospheric ECMWF "jump" flag (if ATM)
    atm_str = '_wATM' if ATM else ''
    #-- Gaussian smoothing string for radius RAD
    gw_str = '_r{0:0.0f}km'.format(RAD) if (RAD != 0) else ''
    #-- input GIA spherical harmonic datafiles
    GIA_Ylms_rate = read_GIA_model(GIA_FILE, GIA=GIA, LMAX=LMAX, MMAX=MMAX)
    gia_str = '_{0}'.format(GIA_Ylms_rate['title']) if GIA else ''
    #-- output string for both LMAX==MMAX and LMAX != MMAX cases
    order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else ''
    #-- filter grace coefficients flag
    ds_str = '_FL' if DESTRIPE else ''
    #-- output filename suffix
    suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')

    #-- GRACE output filename prefix
    #-- mascon directory for GRACE product, processing and date range
    DIRECTORY = os.path.expanduser(parameters['DIRECTORY'])

    #-- create initial reconstruct index for calc_mascon.py
    fid = open(os.path.expanduser(parameters['RECONSTRUCT_INDEX']), 'w')
    HOME = os.path.expanduser('~')
    #-- output file format
    file_format = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}_{9:03d}-{10:03d}.{11}'

    #-- read load love numbers
    hl, kl, ll = load_love_numbers(LMAX,
                                   LOVE_NUMBERS=LOVE_NUMBERS,
                                   REFERENCE=REFERENCE)
    #-- Earth Parameters
    factors = units(lmax=LMAX).harmonic(hl, kl, ll)
    #-- Average Density of the Earth [g/cm^3]
    rho_e = factors.rho_e
    #-- Average Radius of the Earth [cm]
    rad_e = factors.rad_e
    #-- Read Ocean function and convert to Ylms for redistribution
    if MASCON_OCEAN:
        #-- read Land-Sea Mask and convert to spherical harmonics
        LANDMASK = os.path.expanduser(parameters['LANDMASK'])
        ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll))
        ocean_str = '_OCN'
    else:
        #-- not distributing uniformly over ocean
        ocean_str = ''

    #-- input mascon spherical harmonic datafiles
    with open(MASCON_INDEX, 'r') as f:
        mascon_files = f.read().splitlines()
    for k, fi in enumerate(mascon_files):
        #-- read mascon spherical harmonics
        if (DATAFORM == 'ascii'):
            #-- ascii (.txt)
            Ylms = harmonics().from_ascii(os.path.expanduser(fi), date=False)
        elif (DATAFORM == 'netCDF4'):
            #-- netcdf (.nc)
            Ylms = harmonics().from_netCDF4(os.path.expanduser(fi), date=False)
        elif (DATAFORM == 'HDF5'):
            #-- HDF5 (.H5)
            Ylms = harmonics().from_HDF5(os.path.expanduser(fi), date=False)
        #-- Calculating the total mass of each mascon (1 cmwe uniform)
        total_area = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0
        #-- distribute MASCON mass uniformly over the ocean
        if MASCON_OCEAN:
            #-- calculate ratio between total mascon mass and
            #-- a uniformly distributed cm of water over the ocean
            ratio = Ylms.clm[0, 0] / ocean_Ylms['clm'][0, 0]
            #-- for each spherical harmonic
            for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
                for l in range(m, LMAX + 1):  #-- LMAX+1 to include LMAX
                    #-- remove ratio*ocean Ylms from mascon Ylms
                    #-- note: x -= y is equivalent to x = x - y
                    Ylms.clm[l, m] -= ratio * ocean_Ylms['clm'][l, m]
                    Ylms.slm[l, m] -= ratio * ocean_Ylms['slm'][l, m]
        #-- truncate mascon spherical harmonics to d/o LMAX/MMAX
        Ylms = Ylms.truncate(lmax=LMAX, mmax=MMAX)
        #-- mascon base is the file without directory or suffix
        mascon_base = os.path.basename(fi)
        mascon_base = os.path.splitext(mascon_base)[0]
        #-- if lower case, will capitalize
        mascon_base = mascon_base.upper()
        #-- if mascon name contains degree and order info, remove
        mascon_name = mascon_base.replace('_L{0:d}'.format(LMAX), '')

        #-- input filename format (for both LMAX==MMAX and LMAX != MMAX cases):
        #-- mascon name, GRACE dataset, GIA model, LMAX, (MMAX,)
        #-- Gaussian smoothing, filter flag, remove reconstructed fields flag
        #-- output GRACE error file
        args = (mascon_name, dset_str, gia_str.upper(), atm_str, ocean_str,
                LMAX, order_str, gw_str, ds_str)
        file_input = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}.txt'.format(*args)
        mascon_data_input = np.loadtxt(os.path.join(DIRECTORY, file_input))
        nmon = np.shape(mascon_data_input)[0]

        #-- convert mascon time-series from Gt to cmwe
        mascon_sigma = 1e15 * mascon_data_input[:, 2] / total_area
        #-- mascon time-series Ylms
        mascon_Ylms = Ylms.scale(mascon_sigma)
        mascon_Ylms.time = mascon_data_input[:, 1].copy()
        mascon_Ylms.month = mascon_data_input[:, 0].astype(np.int)

        #-- output to file: no ascii option
        args = (mascon_name, dset_str, gia_str.upper(), atm_str, ocean_str,
                LMAX, order_str, gw_str, ds_str, START_MON, END_MON,
                suffix[DATAFORM])
        FILE = file_format.format(*args)
        #-- output harmonics to file
        if (DATAFORM == 'netCDF4'):
            #-- netcdf (.nc)
            mascon_Ylms.to_netCDF4(os.path.join(DIRECTORY, FILE))
        elif (DATAFORM == 'HDF5'):
            #-- HDF5 (.H5)
            mascon_Ylms.to_HDF5(os.path.join(DIRECTORY, FILE))
        #-- print file name to index
        print(os.path.join(DIRECTORY, FILE).replace(HOME, '~'), file=fid)
        #-- change the permissions mode
        os.chmod(os.path.join(DIRECTORY, FILE), MODE)
    #-- close the reconstruct index
    fid.close()
    #-- change the permissions mode of the index file
    os.chmod(os.path.expanduser(parameters['RECONSTRUCT_INDEX']), MODE)
def grace_spatial_error(base_dir,
                        parameters,
                        LOVE_NUMBERS=0,
                        REFERENCE=None,
                        VERBOSE=False,
                        MODE=0o775):
    #-- Data processing center
    PROC = parameters['PROC']
    #-- Data Release
    DREL = parameters['DREL']
    #-- GRACE dataset
    DSET = parameters['DSET']
    #-- Date Range and missing months
    start_mon = np.int(parameters['START'])
    end_mon = np.int(parameters['END'])
    missing = np.array(parameters['MISSING'].split(','), dtype=np.int)
    #-- minimum degree
    LMIN = np.int(parameters['LMIN'])
    #-- maximum degree and order
    LMAX = np.int(parameters['LMAX'])
    if (parameters['MMAX'].title() == 'None'):
        MMAX = np.copy(LMAX)
    else:
        MMAX = np.int(parameters['MMAX'])
    #-- SLR C2,0 and C3,0
    SLR_C20 = parameters['SLR_C20']
    SLR_C30 = parameters['SLR_C30']
    #-- Degree 1 correction
    DEG1 = parameters['DEG1']
    MODEL_DEG1 = parameters['MODEL_DEG1'] in ('Y', 'y')
    #-- ECMWF jump corrections
    ATM = parameters['ATM'] in ('Y', 'y')
    #-- Pole Tide correction from Wahr et al. (2015)
    POLE_TIDE = parameters['POLE_TIDE'] in ('Y', 'y')
    #-- smoothing radius
    RAD = np.int(parameters['RAD'])
    #-- destriped coefficients
    DESTRIPE = parameters['DESTRIPE'] in ('Y', 'y')
    #-- output spatial units
    UNITS = np.int(parameters['UNITS'])
    #-- output degree spacing
    #-- can enter dlon and dlat as [dlon,dlat] or a single value
    DDEG = np.squeeze(np.array(parameters['DDEG'].split(','), dtype='f'))
    #-- output degree interval (0:360, 90:-90) or (degree spacing/2)
    INTERVAL = np.int(parameters['INTERVAL'])
    #-- output data format (ascii, netCDF4, HDF5)
    DATAFORM = parameters['DATAFORM']
    #-- output directory and base filename
    DIRECTORY = os.path.expanduser(parameters['DIRECTORY'])
    FILENAME = parameters['FILENAME']

    #-- recursively create output directory if not currently existing
    if (not os.access(DIRECTORY, os.F_OK)):
        os.makedirs(DIRECTORY, mode=MODE, exist_ok=True)

    #-- list object of output files for file logs (full path)
    output_files = []

    #-- file information
    suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')

    #-- read arrays of kl, hl, and ll Love Numbers
    hl, kl, ll = load_love_numbers(LMAX,
                                   LOVE_NUMBERS=LOVE_NUMBERS,
                                   REFERENCE=REFERENCE)

    #-- Calculating the Gaussian smoothing for radius RAD
    if (RAD != 0):
        wt = 2.0 * np.pi * gauss_weights(RAD, LMAX)
        gw_str = '_r{0:0.0f}km'.format(RAD)
    else:
        #-- else = 1
        wt = np.ones((LMAX + 1))
        gw_str = ''

    #-- flag for spherical harmonic order
    order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else ''
    #-- atmospheric ECMWF "jump" flag (if ATM)
    atm_str = '_wATM' if ATM else ''

    #-- reading GRACE months for input range with grace_input_months.py
    #-- replacing SLR and Degree 1 if specified
    #-- correcting for Pole-Tide and Atmospheric Jumps if specified
    Ylms = grace_input_months(base_dir,
                              PROC,
                              DREL,
                              DSET,
                              LMAX,
                              start_mon,
                              end_mon,
                              missing,
                              SLR_C20,
                              DEG1,
                              MMAX=MMAX,
                              SLR_C30=SLR_C30,
                              MODEL_DEG1=MODEL_DEG1,
                              ATM=ATM,
                              POLE_TIDE=POLE_TIDE)
    #-- convert to harmonics object and remove mean if specified
    GRACE_Ylms = harmonics().from_dict(Ylms)
    grace_dir = Ylms['directory']
    #-- mean parameters
    MEAN = parameters['MEAN'] in ('Y', 'y')
    #-- use a mean file for the static field to remove
    if (parameters['MEAN_FILE'].title() == 'None'):
        mean_Ylms = GRACE_Ylms.mean(apply=MEAN)
    else:
        #-- read data form for input mean file (ascii, netCDF4, HDF5, gfc)
        if (parameters['MEANFORM'] == 'ascii'):
            mean_Ylms = harmonics().from_ascii(parameters['MEAN_FILE'],
                                               date=False)
        elif (parameters['MEANFORM'] == 'netCDF4'):
            mean_Ylms = harmonics().from_netCDF4(parameters['MEAN_FILE'],
                                                 date=False)
        elif (parameters['MEANFORM'] == 'HDF5'):
            mean_Ylms = harmonics().from_HDF5(parameters['MEAN_FILE'],
                                              date=False)
        elif (parameters['MEANFORM'] == 'gfc'):
            mean_Ylms = harmonics().from_gfc(parameters['MEAN_FILE'])
        #-- remove the input mean
        if MEAN:
            GRACE_Ylms.subtract(mean_Ylms)
    #-- date information of GRACE/GRACE-FO coefficients
    nfiles = len(GRACE_Ylms.time)

    #-- filter GRACE/GRACE-FO coefficients
    if DESTRIPE:
        #-- destriping GRACE/GRACE-FO coefficients
        ds_str = '_FL'
        GRACE_Ylms = GRACE_Ylms.destripe()
    else:
        #-- using standard GRACE/GRACE-FO harmonics
        ds_str = ''

    #-- calculating GRACE error (Wahr et al 2006)
    #-- output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases)
    args = (PROC, DREL, DSET, LMAX, order_str, ds_str, atm_str,
            GRACE_Ylms.month[0], GRACE_Ylms.month[-1], suffix[DATAFORM])
    delta_format = '{0}_{1}_{2}_DELTA_CLM_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}'
    DELTA_FILE = delta_format.format(*args)
    #-- full path of the GRACE directory
    #-- if file was previously calculated, will read file
    #-- else will calculate the GRACE error
    if (not os.access(os.path.join(grace_dir, DELTA_FILE), os.F_OK)):
        #-- add output delta file to list object
        output_files.append(os.path.join(grace_dir, DELTA_FILE))

        #-- Delta coefficients of GRACE time series (Error components)
        delta_Ylms = harmonics(lmax=LMAX, mmax=MMAX)
        delta_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1))
        delta_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1))
        #-- Smoothing Half-Width (CNES is a 10-day solution)
        #-- 365/10/2 = 18.25 (next highest is 19)
        #-- All other solutions are monthly solutions (HFWTH for annual = 6)
        if ((PROC == 'CNES') and (DREL in ('RL01', 'RL02'))):
            HFWTH = 19
        else:
            HFWTH = 6
        #-- Equal to the noise of the smoothed time-series
        #-- for each spherical harmonic order
        for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
            #-- for each spherical harmonic degree
            for l in range(m, LMAX + 1):  #-- LMAX+1 to include LMAX
                #-- Delta coefficients of GRACE time series
                for cs, csharm in enumerate(['clm', 'slm']):
                    #-- Constrained GRACE Error (Noise of smoothed time-series)
                    #-- With Annual and Semi-Annual Terms
                    val1 = getattr(GRACE_Ylms, csharm)
                    smth = tssmooth(GRACE_Ylms.time,
                                    val1[l, m, :],
                                    HFWTH=HFWTH)
                    #-- number of smoothed points
                    nsmth = len(smth['data'])
                    #-- GRACE delta Ylms
                    #-- variance of data-(smoothed+annual+semi)
                    val2 = getattr(delta_Ylms, csharm)
                    val2[l, m] = np.sqrt(np.sum(smth['noise']**2) / nsmth)

        #-- save GRACE DELTA to file
        delta_Ylms.time = np.copy(nsmth)
        delta_Ylms.month = np.copy(nsmth)
        if (DATAFORM == 'ascii'):
            #-- ascii (.txt)
            delta_Ylms.to_ascii(os.path.join(grace_dir, DELTA_FILE))
        elif (DATAFORM == 'netCDF4'):
            #-- netcdf (.nc)
            delta_Ylms.to_netCDF4(os.path.join(grace_dir, DELTA_FILE))
        elif (DATAFORM == 'HDF5'):
            #-- HDF5 (.H5)
            delta_Ylms.to_HDF5(os.path.join(grace_dir, DELTA_FILE))
        #-- set the permissions mode of the output harmonics file
        os.chmod(os.path.join(grace_dir, DELTA_FILE), MODE)
        #-- append delta harmonics file to output files list
        output_files.append(os.path.join(grace_dir, DELTA_FILE))
    else:
        #-- read GRACE DELTA spherical harmonics datafile
        if (DATAFORM == 'ascii'):
            #-- ascii (.txt)
            delta_Ylms = harmonics().from_ascii(
                os.path.join(grace_dir, DELTA_FILE))
        elif (DATAFORM == 'netCDF4'):
            #-- netcdf (.nc)
            delta_Ylms = harmonics().from_netCDF4(
                os.path.join(grace_dir, DELTA_FILE))
        elif (DATAFORM == 'HDF5'):
            #-- HDF5 (.H5)
            delta_Ylms = harmonics().from_HDF5(
                os.path.join(grace_dir, DELTA_FILE))
        #-- truncate grace delta clm and slm to d/o LMAX/MMAX
        delta_Ylms = delta_Ylms.truncate(lmax=LMAX, mmax=MMAX)
        nsmth = np.int(delta_Ylms.time)

    #-- Output spatial data object
    delta = spatial()
    #-- Output Degree Spacing
    dlon, dlat = (DDEG, DDEG) if (np.ndim(DDEG) == 0) else (DDEG[0], DDEG[1])
    #-- Output Degree Interval
    if (INTERVAL == 1):
        #-- (-180:180,90:-90)
        nlon = np.int((360.0 / dlon) + 1.0)
        nlat = np.int((180.0 / dlat) + 1.0)
        delta.lon = -180 + dlon * np.arange(0, nlon)
        delta.lat = 90.0 - dlat * np.arange(0, nlat)
    elif (INTERVAL == 2):
        #-- (Degree spacing)/2
        delta.lon = np.arange(-180 + dlon / 2.0, 180 + dlon / 2.0, dlon)
        delta.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat)
        nlon = len(delta.lon)
        nlat = len(delta.lat)

    #-- Earth Parameters
    factors = units(lmax=LMAX).harmonic(hl, kl, ll)
    #-- output spatial units
    unit_list = ['cmwe', 'mmGH', 'mmCU', u'\u03BCGal', 'mbar']
    unit_name = [
        'Equivalent Water Thickness', 'Geoid Height', 'Elastic Crustal Uplift',
        'Gravitational Undulation', 'Equivalent Surface Pressure'
    ]
    #-- dfactor is the degree dependent coefficients
    #-- for specific spherical harmonic output units
    if (UNITS == 1):
        #-- 1: cmwe, centimeters water equivalent
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).cmwe
    elif (UNITS == 2):
        #-- 2: mmGH, millimeters geoid height
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmGH
    elif (UNITS == 3):
        #-- 3: mmCU, millimeters elastic crustal deformation
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmCU
    elif (UNITS == 4):
        #-- 4: micGal, microGal gravity perturbations
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).microGal
    elif (UNITS == 5):
        #-- 5: mbar, millibar equivalent surface pressure
        dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mbar

    #-- Computing plms for converting to spatial domain
    phi = delta.lon[np.newaxis, :] * np.pi / 180.0
    theta = (90.0 - delta.lat) * np.pi / 180.0
    PLM, dPLM = plm_holmes(LMAX, np.cos(theta))
    #-- square of legendre polynomials truncated to order MMAX
    mm = np.arange(0, MMAX + 1)
    PLM2 = PLM[:, mm, :]**2

    #-- Calculating cos(m*phi)^2 and sin(m*phi)^2
    m = delta_Ylms.m[:, np.newaxis]
    ccos = np.cos(np.dot(m, phi))**2
    ssin = np.sin(np.dot(m, phi))**2

    #-- truncate delta harmonics to spherical harmonic range
    Ylms = delta_Ylms.truncate(LMAX, lmin=LMIN, mmax=MMAX)
    #-- convolve delta harmonics with degree dependent factors
    #-- smooth harmonics and convert to output units
    Ylms = Ylms.convolve(dfactor * wt).power(2.0).scale(1.0 / nsmth)
    #-- Calculate fourier coefficients
    d_cos = np.zeros((MMAX + 1, nlat))  #-- [m,th]
    d_sin = np.zeros((MMAX + 1, nlat))  #-- [m,th]
    #-- Calculating delta spatial values
    for k in range(0, nlat):
        #-- summation over all spherical harmonic degrees
        d_cos[:, k] = np.sum(PLM2[:, :, k] * Ylms.clm, axis=0)
        d_sin[:, k] = np.sum(PLM2[:, :, k] * Ylms.slm, axis=0)

    #-- Multiplying by c/s(phi#m) to get spatial maps (lon,lat)
    delta.data = np.sqrt(np.dot(ccos.T, d_cos) + np.dot(ssin.T, d_sin)).T

    #-- output file format
    file_format = '{0}{1}_L{2:d}{3}{4}{5}_ERR_{6:03d}-{7:03d}.{8}'
    #-- output error file to ascii, netCDF4 or HDF5
    args = (FILENAME, unit_list[UNITS - 1], LMAX, order_str, gw_str, ds_str,
            GRACE_Ylms.month[0], GRACE_Ylms.month[-1], suffix[DATAFORM])
    FILE = os.path.join(DIRECTORY, file_format.format(*args))
    if (DATAFORM == 'ascii'):
        #-- ascii (.txt)
        delta.to_ascii(FILE, date=False, verbose=VERBOSE)
    elif (DATAFORM == 'netCDF4'):
        #-- netCDF4
        delta.to_netCDF4(FILE,
                         date=False,
                         verbose=VERBOSE,
                         units=unit_list[UNITS - 1],
                         longname=unit_name[UNITS - 1],
                         title='GRACE/GRACE-FO Spatial Error')
    elif (DATAFORM == 'HDF5'):
        #-- HDF5
        delta.to_HDF5(FILE,
                      date=False,
                      verbose=VERBOSE,
                      units=unit_list[UNITS - 1],
                      longname=unit_name[UNITS - 1],
                      title='GRACE/GRACE-FO Spatial Error')
    #-- set the permissions mode of the output files
    os.chmod(FILE, MODE)
    #-- add file to list
    output_files.append(FILE)

    #-- return the list of output files
    return output_files
Example #14
0
def calc_mascon(base_dir,
                parameters,
                LOVE_NUMBERS=0,
                REFERENCE=None,
                MODE=0o775):
    #-- convert parameters to variables
    #-- Data processing center
    PROC = parameters['PROC']
    #-- Data Release
    DREL = parameters['DREL']
    #-- GRACE dataset
    DSET = parameters['DSET']
    #-- Date Range and missing months
    start_mon = np.int(parameters['START'])
    end_mon = np.int(parameters['END'])
    missing = np.array(parameters['MISSING'].split(','), dtype=np.int)
    #-- spherical harmonic degree range
    LMIN = np.int(parameters['LMIN'])
    LMAX = np.int(parameters['LMAX'])
    #-- maximum spherical harmonic order
    if (parameters['MMAX'].title() == 'None'):
        MMAX = np.copy(LMAX)
    else:
        MMAX = np.int(parameters['MMAX'])
    #-- degree 1 coefficients
    #-- None: No degree 1
    #-- Tellus: GRACE/GRACE-FO TN-13 from PO.DAAC
    #--     https://grace.jpl.nasa.gov/data/get-data/geocenter/
    #-- SLR: satellite laser ranging from CSR
    #--     ftp://ftp.csr.utexas.edu/pub/slr/geocenter/
    #-- SLF: Sutterley and Velicogna, Remote Sensing (2019)
    #--     https://www.mdpi.com/2072-4292/11/18/2108
    DEG1 = parameters['DEG1']
    #-- replace C20, C30 with coefficients from SLR
    SLR_C20 = parameters['SLR_C20']
    SLR_C30 = parameters['SLR_C30']
    #-- ECMWF jump corrections
    ATM = parameters['ATM'] in ('Y', 'y')
    #-- Pole-Tide from Wahr et al. (2015)
    POLE_TIDE = parameters['POLE_TIDE'] in ('Y', 'y')
    #-- Glacial Isostatic Adjustment file to read
    GIA = parameters['GIA'] if (parameters['GIA'].title() != 'None') else None
    GIA_FILE = os.path.expanduser(parameters['GIA_FILE'])
    #-- remove a set of spherical harmonics from the GRACE data
    REMOVE_INDEX = parameters['REMOVE_INDEX']
    REDISTRIBUTE_REMOVED = parameters['REDISTRIBUTE_REMOVED'] in ('Y', 'y')
    #-- remove reconstructed fields
    RECONSTRUCT = parameters['RECONSTRUCT'] in ('Y', 'y')
    #-- gaussian smoothing radius
    RAD = np.float(parameters['RAD'])
    #-- filter coefficients for stripe effects
    DESTRIPE = parameters['DESTRIPE'] in ('Y', 'y')
    #-- input/output data format (ascii, netCDF4, HDF5)
    DATAFORM = parameters['DATAFORM']
    #-- index of mascons spherical harmonics
    #-- path.expanduser = tilde expansion of path
    MASCON_INDEX = os.path.expanduser(parameters['MASCON_INDEX'])
    #-- output directory for mascon time series files
    DIRECTORY = os.path.expanduser(parameters['DIRECTORY'])
    #-- 1: fit mass, 2: fit geoid
    FIT_METHOD = np.int(parameters['FIT_METHOD'])
    #-- mascon redistribution
    MASCON_OCEAN = parameters['MASCON_OCEAN'] in ('Y', 'y')

    #-- recursively create output Directory if not currently existing
    if (not os.access(DIRECTORY, os.F_OK)):
        os.makedirs(DIRECTORY, mode=MODE, exist_ok=True)

    #-- list object of output files for file logs (full path)
    output_files = []

    #-- file information
    suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')

    #-- read arrays of kl, hl, and ll Love Numbers
    hl, kl, ll = load_love_numbers(LMAX,
                                   LOVE_NUMBERS=LOVE_NUMBERS,
                                   REFERENCE=REFERENCE)

    #-- Earth Parameters
    factors = units(lmax=LMAX).harmonic(hl, kl, ll)
    #-- Average Density of the Earth [g/cm^3]
    rho_e = factors.rho_e
    #-- Average Radius of the Earth [cm]
    rad_e = factors.rad_e

    #-- for datasets not GSM: will add a label for the dataset
    dset_str = '' if (DSET == 'GSM') else '_{0}'.format(DSET)
    #-- atmospheric ECMWF "jump" flag (if ATM)
    atm_str = '_wATM' if ATM else ''
    #-- output string for both LMAX==MMAX and LMAX != MMAX cases
    order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else ''

    #-- Calculating the Gaussian smoothing for radius RAD
    if (RAD != 0):
        wt = 2.0 * np.pi * gauss_weights(RAD, LMAX)
        gw_str = '_r{0:0.0f}km'.format(RAD)
    else:
        #-- else = 1
        wt = np.ones((LMAX + 1))
        gw_str = ''

    #-- Read Ocean function and convert to Ylms for redistribution
    if (MASCON_OCEAN | REDISTRIBUTE_REMOVED):
        #-- read Land-Sea Mask and convert to spherical harmonics
        LSMASK = os.path.expanduser(parameters['LANDMASK'])
        ocean_Ylms = ocean_stokes(LSMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll))
        ocean_str = '_OCN'
    else:
        #-- not distributing uniformly over ocean
        ocean_str = ''

    #-- input GRACE/GRACE-FO spherical harmonic datafiles
    #-- reading GRACE months for input range with grace_input_months.py
    #-- replacing SLR and Degree 1 if specified
    #-- correcting for Pole-Tide and Atmospheric Jumps if specified
    Ylms = grace_input_months(base_dir,
                              PROC,
                              DREL,
                              DSET,
                              LMAX,
                              start_mon,
                              end_mon,
                              missing,
                              SLR_C20,
                              DEG1,
                              MMAX=MMAX,
                              SLR_C30=SLR_C30,
                              MODEL_DEG1=True,
                              ATM=ATM,
                              POLE_TIDE=POLE_TIDE)
    #-- full path to directory for specific GRACE/GRACE-FO product
    grace_dir = Ylms['directory']
    #-- create harmonics object from GRACE/GRACE-FO data
    GRACE_Ylms = harmonics().from_dict(Ylms)
    #-- use a mean file for the static field to remove
    if (parameters['MEAN_FILE'].title() == 'None'):
        GRACE_Ylms.mean(apply=True)
    else:
        #-- read data form for input mean file (ascii, netCDF4, HDF5, gfc)
        if (parameters['MEANFORM'] == 'ascii'):
            mean_Ylms = harmonics().from_ascii(parameters['MEAN_FILE'],
                                               date=False)
        elif (parameters['MEANFORM'] == 'netCDF4'):
            mean_Ylms = harmonics().from_netCDF4(parameters['MEAN_FILE'],
                                                 date=False)
        elif (parameters['MEANFORM'] == 'HDF5'):
            mean_Ylms = harmonics().from_HDF5(parameters['MEAN_FILE'],
                                              date=False)
        elif (parameters['MEANFORM'] == 'gfc'):
            mean_Ylms = harmonics().from_gfc(parameters['MEAN_FILE'])
        #-- remove the input mean
        GRACE_Ylms.subtract(mean_Ylms)
    #-- date information of GRACE/GRACE-FO coefficients
    n_files = len(GRACE_Ylms.time)

    #-- filter GRACE/GRACE-FO coefficients
    if DESTRIPE:
        #-- destriping GRACE/GRACE-FO coefficients
        ds_str = '_FL'
        GRACE_Ylms = GRACE_Ylms.destripe()
    else:
        #-- using standard GRACE/GRACE-FO harmonics
        ds_str = ''

    #-- input GIA spherical harmonic datafiles
    GIA_Ylms_rate = read_GIA_model(GIA_FILE, GIA=GIA, LMAX=LMAX, MMAX=MMAX)
    gia_str = '_{0}'.format(GIA_Ylms_rate['title']) if GIA else ''
    #-- calculate the monthly mass change from GIA
    GIA_Ylms = GRACE_Ylms.zeros_like()
    GIA_Ylms.time[:] = np.copy(GRACE_Ylms.time)
    GIA_Ylms.month[:] = np.copy(GRACE_Ylms.month)
    #-- monthly GIA calculated by gia_rate*time elapsed
    #-- finding change in GIA each month
    for t in range(n_files):
        GIA_Ylms.clm[:, :,
                     t] = GIA_Ylms_rate['clm'] * (GIA_Ylms.time[t] - 2003.3)
        GIA_Ylms.slm[:, :,
                     t] = GIA_Ylms_rate['slm'] * (GIA_Ylms.time[t] - 2003.3)

    #-- input spherical harmonic datafiles to be removed from the GRACE data
    #-- Remove sets of Ylms from the GRACE data before returning
    remove_Ylms = GRACE_Ylms.zeros_like()
    remove_Ylms.time[:] = np.copy(GRACE_Ylms.time)
    remove_Ylms.month[:] = np.copy(GRACE_Ylms.month)
    if (parameters['REMOVE_INDEX'].title() != 'None'):
        #-- for each file index separated by commas
        for REMOVE_INDEX in parameters['REMOVE_INDEX'].split(','):
            Ylms = harmonics().from_index(REMOVE_INDEX, DATAFORM)
            #-- reduce to GRACE/GRACE-FO months and truncate to degree and order
            Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX, mmax=MMAX)
            #-- distribute removed Ylms uniformly over the ocean
            if REDISTRIBUTE_REMOVED:
                #-- calculate ratio between total removed mass and
                #-- a uniformly distributed cm of water over the ocean
                ratio = Ylms.clm[0, 0, :] / ocean_Ylms['clm'][0, 0]
                #-- for each spherical harmonic
                for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
                    for l in range(m, LMAX + 1):  #-- LMAX+1 to include LMAX
                        #-- remove the ratio*ocean Ylms from Ylms
                        #-- note: x -= y is equivalent to x = x - y
                        Ylms.clm[l, m, :] -= ratio * ocean_Ylms['clm'][l, m]
                        Ylms.slm[l, m, :] -= ratio * ocean_Ylms['slm'][l, m]
            #-- filter removed coefficients
            if DESTRIPE:
                Ylms = Ylms.destripe()
            #-- add data for month t and INDEX_FILE to the total
            #-- remove_clm and remove_slm matrices
            #-- redistributing the mass over the ocean if specified
            remove_Ylms.add(Ylms)

    #-- input reconstructed spherical harmonic datafiles
    construct_Ylms = GRACE_Ylms.zeros_like()
    construct_Ylms.time[:] = np.copy(GRACE_Ylms.time)
    construct_Ylms.month[:] = np.copy(GRACE_Ylms.month)
    if RECONSTRUCT:
        #-- input index for reconstructed spherical harmonic datafiles
        RECONSTRUCT_INDEX = os.path.expanduser(parameters['RECONSTRUCT_INDEX'])
        with open(RECONSTRUCT_INDEX, 'r') as f:
            reconstruct_files = f.read().splitlines()
        #-- remove commented lines (can comment out files in the index)
        reconstruct_files = [
            f for f in reconstruct_files if not re.match('#', f)
        ]
        #-- for each valid file in the index (iterate over mascons)
        for construct_file in reconstruct_files:
            #-- read reconstructed spherical harmonics
            if (DATAFORM == 'ascii'):
                #-- ascii (.txt)
                Ylms = harmonics().from_ascii(construct_file)
            elif (DATAFORM == 'netCDF4'):
                #-- netcdf (.nc)
                Ylms = harmonics().from_netCDF4(construct_file)
            elif (DATAFORM == 'HDF5'):
                #-- HDF5 (.H5)
                Ylms = harmonics().from_HDF5(construct_file)
            #-- truncate clm and slm matrices to LMAX/MMAX
            #-- add harmonics object to total
            construct_Ylms.add(Ylms.truncate(lmax=LMAX, mmax=MMAX))
        #-- filter reconstructed coefficients
        if DESTRIPE:
            construct_Ylms = construct_Ylms.destripe()
        #-- set flag for removing reconstructed coefficients
        construct_str = '_LEAKAGE'
    else:
        #-- set flag for not removing the reconstructed coefficients
        construct_str = ''

    #-- input mascon spherical harmonic datafiles
    with open(MASCON_INDEX, 'r') as f:
        mascon_files = f.read().splitlines()
    #-- number of mascons
    n_mas = len(mascon_files)
    #-- spatial area of the mascon
    total_area = np.zeros((n_mas))
    #-- name of each mascon
    mascon_name = []
    #-- for each valid file in the index (iterate over mascons)
    mascon_list = []
    for k, fi in enumerate(mascon_files):
        #-- read mascon spherical harmonics
        if (DATAFORM == 'ascii'):
            #-- ascii (.txt)
            Ylms = harmonics().from_ascii(os.path.expanduser(fi), date=False)
        elif (DATAFORM == 'netCDF4'):
            #-- netcdf (.nc)
            Ylms = harmonics().from_netCDF4(os.path.expanduser(fi), date=False)
        elif (DATAFORM == 'HDF5'):
            #-- HDF5 (.H5)
            Ylms = harmonics().from_HDF5(os.path.expanduser(fi), date=False)
        #-- Calculating the total mass of each mascon (1 cmwe uniform)
        total_area[k] = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0
        #-- distribute MASCON mass uniformly over the ocean
        if MASCON_OCEAN:
            #-- calculate ratio between total mascon mass and
            #-- a uniformly distributed cm of water over the ocean
            ratio = Ylms.clm[0, 0] / ocean_Ylms['clm'][0, 0]
            #-- for each spherical harmonic
            for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
                for l in range(m, LMAX + 1):  #-- LMAX+1 to include LMAX
                    #-- remove ratio*ocean Ylms from mascon Ylms
                    #-- note: x -= y is equivalent to x = x - y
                    Ylms.clm[l, m] -= ratio * ocean_Ylms['clm'][l, m]
                    Ylms.slm[l, m] -= ratio * ocean_Ylms['slm'][l, m]
        #-- truncate mascon spherical harmonics to d/o LMAX/MMAX and add to list
        mascon_list.append(Ylms.truncate(lmax=LMAX, mmax=MMAX))
        #-- mascon base is the file without directory or suffix
        mascon_base = os.path.basename(mascon_files[k])
        mascon_base = os.path.splitext(mascon_base)[0]
        #-- if lower case, will capitalize
        mascon_base = mascon_base.upper()
        #-- if mascon name contains degree and order info, remove
        mascon_name.append(mascon_base.replace('_L{0:d}'.format(LMAX), ''))
    #-- create single harmonics object from list
    mascon_Ylms = harmonics().from_list(mascon_list, date=False)

    #-- calculating GRACE/GRACE-FO error (Wahr et al. 2006)
    #-- output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases)
    args = (PROC, DREL, DSET, LMAX, order_str, ds_str, atm_str,
            GRACE_Ylms.month[0], GRACE_Ylms.month[-1], suffix[DATAFORM])
    delta_format = '{0}_{1}_{2}_DELTA_CLM_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}'
    DELTA_FILE = delta_format.format(*args)
    #-- check full path of the GRACE directory for delta file
    #-- if file was previously calculated: will read file
    #-- else: will calculate the GRACE/GRACE-FO error
    if (not os.access(os.path.join(grace_dir, DELTA_FILE), os.F_OK)):
        #-- add output delta file to list object
        output_files.append(os.path.join(grace_dir, DELTA_FILE))

        #-- Delta coefficients of GRACE time series (Error components)
        delta_Ylms = harmonics(lmax=LMAX, mmax=MMAX)
        delta_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1))
        delta_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1))
        #-- Smoothing Half-Width (CNES is a 10-day solution)
        #-- All other solutions are monthly solutions (HFWTH for annual = 6)
        if ((PROC == 'CNES') and (DREL in ('RL01', 'RL02'))):
            HFWTH = 19
        else:
            HFWTH = 6
        #-- Equal to the noise of the smoothed time-series
        #-- for each spherical harmonic order
        for m in range(0, MMAX + 1):  #-- MMAX+1 to include MMAX
            #-- for each spherical harmonic degree
            for l in range(m, LMAX + 1):  #-- LMAX+1 to include LMAX
                #-- Delta coefficients of GRACE time series
                for cs, csharm in enumerate(['clm', 'slm']):
                    #-- calculate GRACE Error (Noise of smoothed time-series)
                    #-- With Annual and Semi-Annual Terms
                    val1 = getattr(GRACE_Ylms, csharm)
                    smth = tssmooth(GRACE_Ylms.time,
                                    val1[l, m, :],
                                    HFWTH=HFWTH)
                    #-- number of smoothed points
                    nsmth = len(smth['data'])
                    tsmth = np.mean(smth['time'])
                    #-- GRACE delta Ylms
                    #-- variance of data-(smoothed+annual+semi)
                    val2 = getattr(delta_Ylms, csharm)
                    val2[l, m] = np.sqrt(np.sum(smth['noise']**2) / nsmth)

        #-- save GRACE/GRACE-FO delta harmonics to file
        delta_Ylms.time = np.copy(tsmth)
        delta_Ylms.month = np.int(nsmth)
        if (DATAFORM == 'ascii'):
            #-- ascii (.txt)
            delta_Ylms.to_ascii(os.path.join(grace_dir, DELTA_FILE))
        elif (DATAFORM == 'netCDF4'):
            #-- netcdf (.nc)
            delta_Ylms.to_netCDF4(os.path.join(grace_dir, DELTA_FILE))
        elif (DATAFORM == 'HDF5'):
            #-- HDF5 (.H5)
            delta_Ylms.to_HDF5(os.path.join(grace_dir, DELTA_FILE))
    else:
        #-- read GRACE/GRACE-FO delta harmonics from file
        if (DATAFORM == 'ascii'):
            #-- ascii (.txt)
            delta_Ylms = harmonics().from_ascii(
                os.path.join(grace_dir, DELTA_FILE))
        elif (DATAFORM == 'netCDF4'):
            #-- netcdf (.nc)
            delta_Ylms = harmonics().from_netCDF4(
                os.path.join(grace_dir, DELTA_FILE))
        elif (DATAFORM == 'HDF5'):
            #-- HDF5 (.H5)
            delta_Ylms = harmonics().from_HDF5(
                os.path.join(grace_dir, DELTA_FILE))
        #-- truncate GRACE/GRACE-FO delta clm and slm to d/o LMAX/MMAX
        delta_Ylms = delta_Ylms.truncate(lmax=LMAX, mmax=MMAX)
        tsmth = np.squeeze(delta_Ylms.time)
        nsmth = np.int(delta_Ylms.month)

    #-- Calculating the number of cos and sin harmonics between LMIN and LMAX
    #-- taking into account MMAX (if MMAX == LMAX then LMAX-MMAX=0)
    n_harm = np.int(LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX)**2 -
                    (LMAX - MMAX))

    #-- Initialing harmonics for least squares fitting
    #-- mascon kernel
    M_lm = np.zeros((n_harm, n_mas))
    #-- mascon kernel converted to output unit
    MA_lm = np.zeros((n_harm, n_mas))
    #-- corrected clm and slm
    Y_lm = np.zeros((n_harm, n_files))
    #-- sensitivity kernel
    A_lm = np.zeros((n_harm, n_mas))
    #-- Satellite error harmonics
    delta_lm = np.zeros((n_harm))
    #-- Initializing output Mascon time-series
    mascon = np.zeros((n_mas, n_files))
    #-- Mascon satellite error component
    M_delta = np.zeros((n_mas))
    #-- Initializing conversion factors
    #-- factor for converting to coefficients of mass
    fact = np.zeros((n_harm))
    #-- smoothing factor
    wt_lm = np.zeros((n_harm))

    #-- ii is a counter variable for building the mascon column array
    ii = 0
    #-- Creating column array of clm/slm coefficients
    #-- Order is [C00...C6060,S11...S6060]
    #-- Calculating factor to convert geoid spherical harmonic coefficients
    #-- to coefficients of mass (Wahr, 1998)
    coeff = rho_e * rad_e / 3.0
    #-- Switching between Cosine and Sine Stokes
    for cs, csharm in enumerate(['clm', 'slm']):
        #-- copy cosine and sin harmonics
        mascon_harm = getattr(mascon_Ylms, csharm)
        grace_harm = getattr(GRACE_Ylms, csharm)
        GIA_harm = getattr(GIA_Ylms, csharm)
        remove_harm = getattr(remove_Ylms, csharm)
        construct_harm = getattr(construct_Ylms, csharm)
        delta_harm = getattr(delta_Ylms, csharm)
        #-- for each spherical harmonic degree
        #-- +1 to include LMAX
        for l in range(LMIN, LMAX + 1):
            #-- for each spherical harmonic order
            #-- Sine Stokes for (m=0) = 0
            mm = np.min([MMAX, l])
            #-- +1 to include l or MMAX (whichever is smaller)
            for m in range(cs, mm + 1):
                #-- Mascon Spherical Harmonics
                M_lm[ii, :] = np.copy(mascon_harm[l, m, :])
                #-- GRACE Spherical Harmonics
                #-- Correcting GRACE Harmonics for GIA and Removed Terms
                Y_lm[ii,:] = grace_harm[l,m,:] - GIA_harm[l,m,:] - \
                    remove_harm[l,m,:] - construct_harm[l,m,:]
                #-- GRACE delta spherical harmonics
                delta_lm[ii] = np.copy(delta_harm[l, m])
                #-- degree dependent factor to convert to mass
                fact[ii] = (2.0 * l + 1.0) / (1.0 + kl[l])
                #-- degree dependent smoothing
                wt_lm[ii] = np.copy(wt[l])
                #-- add 1 to counter
                ii += 1

    #-- Converting mascon coefficients to fit method
    if (FIT_METHOD == 1):
        #-- Fitting Sensitivity Kernel as mass coefficients
        #-- converting M_lm to mass coefficients of the kernel
        for i in range(n_harm):
            MA_lm[i, :] = M_lm[i, :] * wt_lm[i] * fact[i]
        fit_factor = wt_lm * fact
    else:
        #-- Fitting Sensitivity Kernel as geoid coefficients
        for i in range(n_harm):
            MA_lm[:, :] = M_lm[i, :] * wt_lm[i]
        fit_factor = wt_lm * np.ones((n_harm))

    #-- Fitting the sensitivity kernel from the input kernel
    for i in range(n_harm):
        #-- setting kern_i equal to 1 for d/o
        kern_i = np.zeros((n_harm))
        #-- converting to mass coefficients if specified
        kern_i[i] = 1.0 * fit_factor[i]
        #-- spherical harmonics solution for the
        #-- mascon sensitivity kernels
        #-- Least Squares Solutions: Inv(X'.X).(X'.Y)
        kern_lm = np.linalg.lstsq(MA_lm, kern_i, rcond=-1)[0]
        for k in range(n_mas):
            A_lm[i, k] = kern_lm[k] * total_area[k]

    #-- for each mascon
    for k in range(n_mas):
        #-- Multiply the Satellite error (noise of a smoothed time-series
        #-- with annual and semi-annual components) by the sensitivity kernel
        #-- Converting to Gigatonnes
        M_delta[k] = np.sqrt(np.sum((delta_lm * A_lm[:, k])**2)) / 1e15

        #-- output filename format (for both LMAX==MMAX and LMAX != MMAX cases):
        #-- mascon name, GRACE dataset, GIA model, LMAX, (MMAX,)
        #-- Gaussian smoothing, filter flag, remove reconstructed fields flag
        #-- output GRACE error file
        file_out = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}{9}.txt'.format(
            mascon_name[k], dset_str, gia_str.upper(), atm_str, ocean_str,
            LMAX, order_str, gw_str, ds_str, construct_str)

        #-- Output mascon datafiles
        #-- Will output each mascon time series
        #-- month, date, mascon mass [Gt], satellite error [Gt], mascon area [km^2]
        #-- open output mascon time-series file
        fid = open(os.path.join(DIRECTORY, file_out), 'w')
        #-- for each date
        formatting_string = '{0:03d} {1:12.4f} {2:16.10f} {3:16.10f} {4:16.5f}'
        for t, mon in enumerate(GRACE_Ylms.month):
            #-- Summing over all spherical harmonics for mascon k, and time t
            #-- multiplies by the degree dependent factor to convert
            #-- the harmonics into mass coefficients
            #-- Converting mascon mass time-series from g to gigatonnes
            mascon[k, t] = np.sum(A_lm[:, k] * Y_lm[:, t]) / 1e15
            #-- output to file
            args = (mon, GRACE_Ylms.time[t], mascon[k, t], M_delta[k],
                    total_area[k] / 1e10)
            print(formatting_string.format(*args), file=fid)
        #-- close the output file
        fid.close()
        #-- change the permissions mode
        os.chmod(os.path.join(DIRECTORY, file_out), MODE)
        #-- add output files to list object
        output_files.append(os.path.join(DIRECTORY, file_out))

    #-- return the list of output files
    return output_files
def gen_point_load(data, lon, lat, LMAX=60, MMAX=None, UNITS=1, LOVE=None):
    """
    Calculates spherical harmonic coefficients for point masses

    Arguments
    ---------
    data: data magnitude
    lon: longitude of points
    lat: latitude of points

    Keyword arguments
    -----------------
    LMAX: Upper bound of Spherical Harmonic Degrees
    MMAX: Upper bound of Spherical Harmonic Orders
    UNITS: input data units
        1: grams of mass (default)
        2: gigatonnes of mass
    LOVE: input load Love numbers up to degree LMAX (hl,kl,ll)

    Returns
    -------
    clm: cosine spherical harmonic coefficients
    slm: sine spherical harmonic coefficients
    l: spherical harmonic degree to LMAX
    m: spherical harmonic order to MMAX
    """

    #-- upper bound of spherical harmonic orders (default == LMAX)
    if MMAX is None:
        MMAX = np.copy(LMAX)

    #-- number of input data points
    npts = len(data.flatten())
    #-- convert output longitude and latitude into radians
    phi = np.pi * lon.flatten() / 180.0
    theta = np.pi * (90.0 - lat.flatten()) / 180.0

    #-- SH Degree dependent factors to convert into geodesy normalized SH's
    #-- use splat operator to extract arrays of kl, hl, and ll Love Numbers
    factors = units(lmax=LMAX).spatial(*LOVE)
    #-- extract degree dependent factor for specific units
    int_fact = np.zeros((npts))
    if (UNITS == 1):
        #-- Default Parameter: Input in g
        dfactor = factors.cmwe / (factors.rad_e**2)
        int_fact[:] = 1.0
    elif (UNITS == 2):
        #-- Input in gigatonnes (Gt)
        dfactor = factors.cmwe / (factors.rad_e**2)
        int_fact[:] = 1e15
    #-- flattened form of data converted to units
    D = int_fact * data.flatten()

    #-- output harmonics
    Ylms = {}
    Ylms['clm'] = np.zeros((LMAX + 1, MMAX + 1))
    Ylms['slm'] = np.zeros((LMAX + 1, MMAX + 1))
    Ylms['l'] = np.arange(LMAX + 1)
    Ylms['m'] = np.arange(MMAX + 1)
    #-- for each degree l
    for l in range(LMAX + 1):
        m1 = np.min([l, MMAX]) + 1
        SPH = spherical_harmonic_matrix(l, D, phi, theta, dfactor[l])
        #-- truncate to spherical harmonic order and save to output
        Ylms['clm'][l, :m1] = SPH.real[:m1]
        Ylms['slm'][l, :m1] = SPH.imag[:m1]
    #-- output harmonics
    return Ylms