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 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)
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: Gigatonnes of mass
        3: kg/m^2
        list: custom degree-dependent unit conversion factor
    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.int64(LMIN)
    LMAX = np.int64(LMAX)
    #-- upper bound of spherical harmonic orders (default = LMAX)
    MMAX = np.copy(LMAX) if (MMAX is None) else MMAX

    #-- grid dimensions
    nlat = np.int64(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 fully normalized SH's
    #-- use splat operator to extract arrays of kl, hl, and ll Love Numbers
    factors = gravity_toolkit.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
    elif isinstance(UNITS, (list, np.ndarray)):
        #-- custom units
        dfactor = np.copy(UNITS)
        int_fact[:] = np.sin(th) * dphi * dth
    else:
        raise ValueError('Unknown units {0}'.format(UNITS))

    #-- 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
    Ylms = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX)
    Ylms.clm = np.zeros((LMAX + 1, MMAX + 1))
    Ylms.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 fully normalized coefficients
        Ylms.clm[l, m] = dfactor[l] * yclm[l, m]
        Ylms.slm[l, m] = dfactor[l] * yslm[l, m]

    #-- return the output spherical harmonics object
    return Ylms
Example #4
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)
    }
Example #5
0
def convert_harmonics(INPUT_FILE,
                      OUTPUT_FILE,
                      LMAX=None,
                      MMAX=None,
                      UNITS=None,
                      LOVE_NUMBERS=0,
                      REFERENCE=None,
                      DDEG=None,
                      INTERVAL=None,
                      MISSING=False,
                      FILL_VALUE=None,
                      HEADER=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)

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

    #-- read spatial file in data format
    #-- expand dimensions
    if (DATAFORM == 'ascii'):
        #-- ascii (.txt)
        input_spatial = spatial(spacing=[dlon, dlat], nlat=nlat,
                                nlon=nlon).from_ascii(
                                    INPUT_FILE, header=HEADER).expand_dims()
    elif (DATAFORM == 'netCDF4'):
        #-- netcdf (.nc)
        input_spatial = spatial().from_netCDF4(INPUT_FILE).expand_dims()
    elif (DATAFORM == 'HDF5'):
        #-- HDF5 (.H5)
        input_spatial = spatial().from_HDF5(INPUT_FILE).expand_dims()
    #-- convert missing values to zero
    input_spatial.replace_invalid(0.0)
    #-- input data shape
    nlat, nlon, nt = input_spatial.shape

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

    #-- calculate associated Legendre polynomials
    th = (90.0 - input_spatial.lat) * np.pi / 180.0
    PLM, dPLM = plm_holmes(LMAX, np.cos(th))
    #-- date count array
    counter = np.arange(nt)

    #-- allocate for output spherical harmonics
    Ylms = harmonics(lmax=LMAX, mmax=MMAX)
    Ylms.time = input_spatial.time.copy()
    Ylms.month = np.array(12.0 * (Ylms.time - 2002.0), dtype='i') + 1
    Ylms.clm = np.zeros((LMAX + 1, LMAX + 1, nt))
    Ylms.slm = np.zeros((LMAX + 1, LMAX + 1, nt))
    for t in range(nt):
        #-- convert spatial field to spherical harmonics
        output_Ylms = gen_stokes(input_spatial.data[:, :, t].T,
                                 input_spatial.lon,
                                 input_spatial.lat,
                                 UNITS=UNITS,
                                 LMIN=0,
                                 LMAX=LMAX,
                                 MMAX=MMAX,
                                 PLM=PLM,
                                 LOVE=LOVE)
        Ylms.clm[:, :, t] = output_Ylms['clm'][:, :].copy()
        Ylms.slm[:, :, t] = output_Ylms['slm'][:, :].copy()

    #-- 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}'.format(INPUT_FILE, OUTPUT_FILE))
    #-- outputting data to file
    if (DATAFORM == 'ascii'):
        #-- ascii (.txt)
        Ylms.to_ascii(OUTPUT_FILE)
    elif (DATAFORM == 'netCDF4'):
        #-- netCDF4 (.nc)
        Ylms.to_netCDF4(OUTPUT_FILE)
    elif (DATAFORM == 'HDF5'):
        #-- HDF5 (.H5)
        Ylms.to_HDF5(OUTPUT_FILE)
    #-- change output permissions level to MODE
    os.chmod(OUTPUT_FILE, MODE)
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
        list: custom unit conversion factor
    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 = gravity_toolkit.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
    elif isinstance(UNITS,(list,np.ndarray)):
        #-- custom units 
        unit_conv = np.copy(UNITS)
    else:
        raise ValueError('Unknown units {0}'.format(UNITS))

    #-- 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,_ = 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 = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX)
    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 object
    return Ylms
def convert_harmonics(INPUT_FILE,
                      OUTPUT_FILE,
                      LMAX=None,
                      MMAX=None,
                      UNITS=None,
                      LOVE_NUMBERS=0,
                      REFERENCE=None,
                      DDEG=None,
                      INTERVAL=None,
                      FILL_VALUE=None,
                      HEADER=None,
                      DATAFORM=None,
                      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)

    #-- Grid spacing
    dlon, dlat = (DDEG, DDEG) if (np.ndim(DDEG) == 0) 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 spatial file in data format
    #-- expand dimensions
    if (DATAFORM == 'ascii'):
        #-- ascii (.txt)
        input_spatial = spatial(spacing=[dlon, dlat],
                                nlat=nlat,
                                nlon=nlon,
                                fill_value=FILL_VALUE).from_ascii(
                                    INPUT_FILE, header=HEADER).expand_dims()
    elif (DATAFORM == 'netCDF4'):
        #-- netcdf (.nc)
        input_spatial = spatial().from_netCDF4(INPUT_FILE).expand_dims()
    elif (DATAFORM == 'HDF5'):
        #-- HDF5 (.H5)
        input_spatial = spatial().from_HDF5(INPUT_FILE).expand_dims()
    #-- convert missing values to zero
    input_spatial.replace_invalid(0.0)
    #-- input data shape
    nlat, nlon, nt = input_spatial.shape

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

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

    #-- calculate associated Legendre polynomials
    th = (90.0 - input_spatial.lat) * np.pi / 180.0
    PLM, dPLM = plm_holmes(LMAX, np.cos(th))

    #-- create list of harmonics objects
    Ylms_list = []
    for i, t in enumerate(input_spatial.time):
        #-- convert spatial field to spherical harmonics
        output_Ylms = gen_stokes(input_spatial.data[:, :, i].T,
                                 input_spatial.lon,
                                 input_spatial.lat,
                                 UNITS=UNITS,
                                 LMIN=0,
                                 LMAX=LMAX,
                                 MMAX=MMAX,
                                 PLM=PLM,
                                 LOVE=LOVE)
        output_Ylms.time = np.copy(t)
        output_Ylms.month = calendar_to_grace(t)
        #-- append to list
        Ylms_list.append(output_Ylms)
    #-- convert Ylms list for output spherical harmonics
    Ylms = harmonics().from_list(Ylms_list)
    Ylms_list = None

    #-- outputting data to file
    if (DATAFORM == 'ascii'):
        #-- ascii (.txt)
        Ylms.to_ascii(OUTPUT_FILE)
    elif (DATAFORM == 'netCDF4'):
        #-- netCDF4 (.nc)
        Ylms.to_netCDF4(OUTPUT_FILE)
    elif (DATAFORM == 'HDF5'):
        #-- HDF5 (.H5)
        Ylms.to_HDF5(OUTPUT_FILE)
    #-- change output permissions level to MODE
    os.chmod(OUTPUT_FILE, MODE)
def harmonic_summation(clm1,
                       slm1,
                       lon,
                       lat,
                       LMIN=0,
                       LMAX=0,
                       MMAX=None,
                       PLM=None):
    """
    Converts data from spherical harmonic coefficients to a spatial field

    Arguments
    ---------
    clm1: cosine spherical harmonic coefficients in output units
    slm1: sine spherical harmonic coefficients in output units
    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
    PLM: Fully-normalized associated Legendre polynomials

    Returns
    -------
    spatial: spatial field
    """

    #-- if LMAX is not specified, will use the size of the input harmonics
    if (LMAX == 0):
        LMAX = np.shape(clm1)[0] - 1
    #-- upper bound of spherical harmonic orders (default = LMAX)
    if MMAX is None:
        MMAX = np.copy(LMAX)

    #-- Longitude in radians
    phi = (np.squeeze(lon) * np.pi / 180.0)[np.newaxis, :]
    #-- Colatitude in radians
    th = (90.0 - np.squeeze(lat)) * np.pi / 180.0
    thmax = len(th)

    #--  Calculate fourier coefficients from legendre coefficients
    d_cos = np.zeros((MMAX + 1, thmax))  #-- [m,th]
    d_sin = np.zeros((MMAX + 1, thmax))  #-- [m,th]
    if PLM is None:
        #-- if plms are not pre-computed: calculate Legendre polynomials
        PLM, dPLM = plm_holmes(LMAX, np.cos(th))

    #-- Truncating harmonics to degree and order LMAX
    #-- removing coefficients below LMIN and above MMAX
    mm = np.arange(0, MMAX + 1)
    clm = np.zeros((LMAX + 1, MMAX + 1))
    slm = np.zeros((LMAX + 1, MMAX + 1))
    clm[LMIN:LMAX + 1, mm] = clm1[LMIN:LMAX + 1, mm]
    slm[LMIN:LMAX + 1, mm] = slm1[LMIN:LMAX + 1, mm]
    for k in range(0, thmax):
        #-- summation over all spherical harmonic degrees
        d_cos[:, k] = np.sum(PLM[:, mm, k] * clm[:, mm], axis=0)
        d_sin[:, k] = np.sum(PLM[:, mm, k] * slm[:, mm], axis=0)

    #-- Final signal recovery from fourier coefficients
    m = np.arange(0, MMAX + 1)[:, np.newaxis]
    #-- Calculating cos(m*phi) and sin(m*phi)
    ccos = np.cos(np.dot(m, phi))
    ssin = np.sin(np.dot(m, phi))
    #-- summation of cosine and sine harmonics
    s = np.dot(np.transpose(ccos), d_cos) + np.dot(np.transpose(ssin), d_sin)

    #-- return output data
    return s
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
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 integration(data, lon, lat, LMAX=60, MMAX=None, PLM=0, **kwargs):
    """
    Converts data from the spatial domain to spherical harmonic coefficients

    Arguments
    ---------
    data: data magnitude
    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

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

    #-- dimensions of the longitude and latitude arrays
    nlon = np.int64(len(lon))
    nlat = np.int64(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)
    if np.count_nonzero(lon < 0):
        lon[lon < 0] += 360.0
    #-- calculate longitude and colatitude arrays in radians
    phi = np.reshape(lon,(1,nlon))*np.pi/180.0#-- reshape to 1xnlon
    th = (90.0 - np.squeeze(lat))*np.pi/180.0#-- remove singleton dimensions

    #-- Calculating cos/sin of phi arrays (output [m,phi])
    #-- LMAX+1 as there are LMAX+1 elements between 0 and LMAX
    m = np.arange(MMAX+1)[:, np.newaxis]
    ccos = np.cos(np.dot(m,phi))
    ssin = np.sin(np.dot(m,phi))

    #-- Multiplying sin(th) with differentials of theta and phi
    #-- to calculate the integration factor at each latitude
    int_fact = np.sin(th)*dphi*dth
    coeff = 1.0/(4.0*np.pi)

    #-- Calculate polynomials using Holmes and Featherstone (2002) relation
    plm = np.zeros((LMAX+1,MMAX+1,nlat))
    if (np.ndim(PLM) == 0):
        plmout,dplm = plm_holmes(LMAX,np.cos(th))
    else:
        #-- use precomputed plms to improve computational speed
        #-- or to use a different recursion relation for polynomials
        plmout = PLM

    #-- Multiply plms by integration factors [sin(theta)*dtheta*dphi]
    #-- truncate plms to maximum spherical harmonic order if MMAX < LMAX
    m = np.arange(MMAX+1)
    for j in range(0,nlat):
        plm[:,m,j] = plmout[:,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
    Ylms = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX)
    Ylms.clm = np.zeros((LMAX+1,MMAX+1))
    Ylms.slm = np.zeros((LMAX+1,MMAX+1))
    #-- Multiplying gridded data with sin/cos of m#phis (output [m,theta])
    #-- This will sum through all phis in the dot product
    dcos = np.dot(ccos,data)
    dsin = np.dot(ssin,data)
    for l in range(0,LMAX+1):
        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
        yclm[l,m] = np.sum(plm[l,m,:]*dcos[m,:], axis=1)
        yslm[l,m] = np.sum(plm[l,m,:]*dsin[m,:], axis=1)
        #-- convert to output normalization (4-pi normalized harmonics)
        Ylms.clm[l,m] = coeff*yclm[l,m]
        Ylms.slm[l,m] = coeff*yslm[l,m]

    #-- return the output spherical harmonics object
    return Ylms
Example #12
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 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