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 mascon_reconstruct(DSET,
                       LMAX,
                       RAD,
                       START=None,
                       END=None,
                       MMAX=None,
                       DESTRIPE=False,
                       LOVE_NUMBERS=0,
                       REFERENCE=None,
                       GIA=None,
                       GIA_FILE=None,
                       ATM=False,
                       DATAFORM=None,
                       MASCON_FILE=None,
                       REDISTRIBUTE_MASCONS=False,
                       RECONSTRUCT_FILE=None,
                       LANDMASK=None,
                       OUTPUT_DIRECTORY=None,
                       MODE=0o775):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    #-- return the list of output files
    return output_files