def clenshaw_summation(clm, slm, lon, lat, RAD=0, UNITS=0, LMAX=0, LOVE=None, ASTYPE=np.float128, SCALE=1e-280): """ Calculates the spatial field for a series of spherical harmonics for a sequence of ungridded points Arguments --------- clm: cosine spherical harmonic coefficients slm: sine spherical harmonic coefficients lon: longitude of points lat: latitude of points Keyword arguments ----------------- RAD: Gaussian smoothing radius (km) UNITS: output data units 1: cm of water thickness 2: mm of geoid height 3: mm of elastic crustal deformation 4: microGal gravitational perturbation 5: Pa, equivalent surface pressure in Pascals 6: cm of viscoelastic rustal uplift (GIA) LMAX: Upper bound of Spherical Harmonic Degrees LOVE: input load Love numbers up to degree LMAX (hl,kl,ll) Returns ------- spatial: calculated spatial field for latitude and longitude """ #-- check if lat and lon are the same size if (len(lat) != len(lon)): raise ValueError('Incompatable vector dimensions (lon, lat)') #-- calculate colatitude and longitude in radians th = (90.0 - lat)*np.pi/180.0 phi = np.squeeze(lon*np.pi/180.0) #-- calculate cos and sin of colatitudes t = np.cos(th) u = np.sin(th) #-- dimensions of theta and phi npts = len(th) #-- Gaussian Smoothing if (RAD != 0): wl = 2.0*np.pi*gauss_weights(RAD,LMAX) else: #-- else = 1 wl = np.ones((LMAX+1)) #-- Setting units factor for output #-- extract arrays of kl, hl, and ll Love Numbers factors = units(lmax=LMAX).harmonic(*LOVE) #-- dfactor computes the degree dependent coefficients if (UNITS == 0): #-- 0: keep original scale dfactor = factors.norm elif (UNITS == 1): #-- 1: cmH2O, centimeters water equivalent dfactor = factors.cmwe elif (UNITS == 2): #-- 2: mmGH, mm geoid height dfactor = factors.mmGH elif (UNITS == 3): #-- 3: mmCU, mm elastic crustal deformation dfactor = factors.mmCU elif (UNITS == 4): #-- 4: micGal, microGal gravity perturbations dfactor = factors.microGal elif (UNITS == 5): #-- 5: Pa, equivalent surface pressure in Pascals dfactor = factors.Pa elif (UNITS == 6): #-- 6: cmVCU, cm viscoelastic crustal uplift (GIA ONLY) dfactor = factors.cmVCU else: raise ValueError(('UNITS is invalid:\n1: cmH2O\n2: mmGH\n3: mmCU ' '(elastic)\n4:microGal\n5: Pa\n6: cmVCU (viscoelastic)')) #-- calculate arrays for clenshaw summations over colatitudes s_m_c = np.zeros((npts,LMAX*2+2)) for m in range(LMAX, -1, -1): #-- convolve harmonics with unit factors and smoothing s_m_c[:,2*m:2*m+2] = clenshaw_s_m(t, dfactor*wl, m, clm, slm, LMAX) #-- calculate cos(phi) cos_phi_2 = 2.0*np.cos(phi) #-- matrix of cos/sin m*phi summation cos_m_phi = np.zeros((npts,LMAX+2),dtype=ASTYPE) sin_m_phi = np.zeros((npts,LMAX+2),dtype=ASTYPE) #-- initialize matrix with values at lmax+1 and lmax cos_m_phi[:,LMAX+1] = np.cos(ASTYPE(LMAX + 1)*phi) sin_m_phi[:,LMAX+1] = np.sin(ASTYPE(LMAX + 1)*phi) cos_m_phi[:,LMAX] = np.cos(ASTYPE(LMAX)*phi) sin_m_phi[:,LMAX] = np.sin(ASTYPE(LMAX)*phi) #-- calculate summation for order LMAX s_m = s_m_c[:,2*LMAX]*cos_m_phi[:,LMAX] + s_m_c[:,2*LMAX+1]*sin_m_phi[:,LMAX] #-- iterate to calculate complete summation for m in range(LMAX-1, 0, -1): cos_m_phi[:,m] = cos_phi_2*cos_m_phi[:,m+1] - cos_m_phi[:,m+2] sin_m_phi[:,m] = cos_phi_2*sin_m_phi[:,m+1] - sin_m_phi[:,m+2] #-- calculate summation for order m a_m = np.sqrt((2.0*m+3.0)/(2.0*m+2.0)) s_m = a_m*u*s_m + s_m_c[:,2*m]*cos_m_phi[:,m] + s_m_c[:,2*m+1]*sin_m_phi[:,m] #-- calculate spatial field spatial = np.sqrt(3.0)*u*s_m + s_m_c[:,0] #-- return the calculated spatial field return spatial
def combine_harmonics(INPUT_FILE, OUTPUT_FILE, LMAX=None, MMAX=None, LOVE_NUMBERS=0, REFERENCE=None, RAD=None, DESTRIPE=False, UNITS=None, DDEG=None, INTERVAL=None, BOUNDS=None, REDISTRIBUTE=False, LSMASK=None, MEAN_FILE=None, DATAFORM=None, VERBOSE=False, MODE=0o775): #-- verify that output directory exists DIRECTORY = os.path.abspath(os.path.dirname(OUTPUT_FILE)) if not os.access(DIRECTORY, os.F_OK): os.makedirs(DIRECTORY, MODE, exist_ok=True) #-- read input spherical harmonic coefficients from file in DATAFORM if (DATAFORM == 'ascii'): input_Ylms = harmonics().from_ascii(INPUT_FILE) elif (DATAFORM == 'netCDF4'): #-- read input netCDF4 file (.nc) input_Ylms = harmonics().from_netCDF4(INPUT_FILE) elif (DATAFORM == 'HDF5'): #-- read input HDF5 file (.H5) input_Ylms = harmonics().from_HDF5(INPUT_FILE) #-- reform harmonic dimensions to be l,m,t #-- truncate to degree and order LMAX, MMAX input_Ylms = input_Ylms.truncate(lmax=LMAX, mmax=MMAX).expand_dims() #-- remove mean file from input Ylms if MEAN_FILE and (DATAFORM == 'ascii'): mean_Ylms = harmonics().from_ascii(MEAN_FILE, date=False) input_Ylms.subtract(mean_Ylms) elif MEAN_FILE and (DATAFORM == 'netCDF4'): #-- read input netCDF4 file (.nc) mean_Ylms = harmonics().from_netCDF4(MEAN_FILE, date=False) input_Ylms.subtract(mean_Ylms) elif MEAN_FILE and (DATAFORM == 'HDF5'): #-- read input HDF5 file (.H5) mean_Ylms = harmonics().from_HDF5(MEAN_FILE, date=False) input_Ylms.subtract(mean_Ylms) #-- read arrays of kl, hl, and ll Love Numbers hl, kl, ll = load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE) #-- distribute total mass uniformly over the ocean if REDISTRIBUTE: #-- read Land-Sea Mask and convert to spherical harmonics ocean_Ylms = ocean_stokes(LSMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll)) #-- calculate ratio between total mass and a uniformly distributed #-- layer of water over the ocean ratio = input_Ylms.clm[0, 0, :] / ocean_Ylms['clm'][0, 0] #-- for each spherical harmonic for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX for l in range(m, LMAX + 1): #-- LMAX+1 to include LMAX #-- remove the ratio*ocean Ylms from Ylms #-- note: x -= y is equivalent to x = x - y input_Ylms.clm[l, m, :] -= ratio * ocean_Ylms['clm'][l, m] input_Ylms.slm[l, m, :] -= ratio * ocean_Ylms['slm'][l, m] #-- if using a decorrelation filter (Isabella's destriping Routine) if DESTRIPE: input_Ylms = input_Ylms.destripe() #-- Gaussian smoothing if (RAD != 0): wt = 2.0 * np.pi * gauss_weights(RAD, LMAX) else: wt = np.ones((LMAX + 1)) #-- Output spatial data grid = spatial() grid.time = np.copy(input_Ylms.time) grid.month = np.copy(input_Ylms.month) #-- Output Degree Spacing if (len(DDEG) == 1): #-- dlon == dlat dlon = DDEG dlat = DDEG else: #-- dlon != dlat dlon, dlat = DDEG #-- Output Degree Interval if (INTERVAL == 1): #-- (0:360,90:-90) nlon = np.int((360.0 / dlon) + 1.0) nlat = np.int((180.0 / dlat) + 1.0) grid.lon = dlon * np.arange(0, nlon) grid.lat = 90.0 - dlat * np.arange(0, nlat) elif (INTERVAL == 2): #-- (Degree spacing)/2 grid.lon = np.arange(dlon / 2.0, 360 + dlon / 2.0, dlon) grid.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat) nlon = len(grid.lon) nlat = len(grid.lat) elif (INTERVAL == 3): #-- non-global grid set with BOUNDS parameter minlon, maxlon, minlat, maxlat = BOUNDS.copy() grid.lon = np.arange(minlon + dlon / 2.0, maxlon + dlon / 2.0, dlon) grid.lat = np.arange(maxlat - dlat / 2.0, minlat - dlat / 2.0, -dlat) nlon = len(grid.lon) nlat = len(grid.lat) #-- Setting units factor for output #-- dfactor computes the degree dependent coefficients if (UNITS == 1): #-- 1: cmwe, centimeters water equivalent dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).cmwe elif (UNITS == 2): #-- 2: mmGH, mm geoid height dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmGH elif (UNITS == 3): #-- 3: mmCU, mm elastic crustal deformation dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmCU elif (UNITS == 4): #-- 4: micGal, microGal gravity perturbations dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).microGal elif (UNITS == 5): #-- 5: Pa, equivalent surface pressure in Pascals dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).Pa else: raise ValueError(('UNITS is invalid:\n1: cmwe\n2: mmGH\n3: mmCU ' '(elastic)\n4:microGal\n5: Pa')) #-- Computing plms for converting to spatial domain theta = (90.0 - grid.lat) * np.pi / 180.0 PLM, dPLM = plm_holmes(LMAX, np.cos(theta)) #-- output spatial grid nt = len(input_Ylms.time) grid.data = np.zeros((nlat, nlon, nt)) #-- converting harmonics to truncated, smoothed coefficients in output units for t in range(nt): #-- spherical harmonics for time t Ylms = input_Ylms.index(t) Ylms.convolve(dfactor * wt) #-- convert spherical harmonics to output spatial grid grid.data[:, :, t] = harmonic_summation(Ylms.clm, Ylms.slm, grid.lon, grid.lat, LMAX=LMAX, PLM=PLM).T #-- if verbose output: print input and output file names if VERBOSE: print('{0}:'.format(os.path.basename(sys.argv[0]))) print('{0} -->\n\t{1}\n'.format(INPUT_FILE, OUTPUT_FILE)) #-- outputting data to file output_data(grid.squeeze(), FILENAME=OUTPUT_FILE, DATAFORM=DATAFORM, UNITS=UNITS) #-- change output permissions level to MODE os.chmod(OUTPUT_FILE, MODE)
def gen_pressure_stokes(PG, R, lon, lat, LMAX=60, MMAX=None, PLM=None, LOVE=None): """ Converts pressure fields from the spatial domain to spherical harmonic coefficients Arguments --------- PG: pressure/gravity ratio R: radius lon: longitude array lat: latitude array Keyword arguments ----------------- LMAX: Upper bound of Spherical Harmonic Degrees MMAX: Upper bound of Spherical Harmonic Orders PLM: input Legendre polynomials LOVE: input load Love numbers up to degree LMAX (hl,kl,ll) Returns ------- clm: cosine spherical harmonic coefficients slm: sine spherical harmonic coefficients l: spherical harmonic degree to LMAX m: spherical harmonic order to MMAX """ #-- converting LMAX to integer LMAX = np.int(LMAX) #-- upper bound of spherical harmonic orders (default = LMAX) MMAX = np.copy(LMAX) if not MMAX else MMAX #-- grid dimensions nlat = np.int(len(lat)) #-- grid step dlon = np.abs(lon[1] - lon[0]) dlat = np.abs(lat[1] - lat[0]) #-- longitude degree spacing in radians dphi = dlon * np.pi / 180.0 #-- colatitude degree spacing in radians dth = dlat * np.pi / 180.0 #-- reformatting longitudes to range 0:360 (if previously -180:180) lon = np.squeeze(lon.copy()) if np.any(lon < 0): lon_ind, = np.nonzero(lon < 0) lon[lon_ind] += 360.0 #-- Longitude in radians phi = lon[np.newaxis, :] * np.pi / 180.0 #-- Colatitude in radians th = (90.0 - np.squeeze(lat.copy())) * np.pi / 180.0 #-- For gridded data: dmat = original data matrix sz = np.shape(PG) #-- reforming data to lonXlat if input latXlon PG = np.transpose(PG) if (sz[0] == nlat) else PG R = np.transpose(R) if (sz[0] == nlat) else R #-- Coefficient for calculating Stokes coefficients from pressure field #-- extract arrays of kl, hl, and ll Love Numbers factors = units(lmax=LMAX).spatial(*LOVE) #-- Earth Parameters #-- Average Radius of the Earth [m] rad_e = factors.rad_e / 100.0 #-- SH Degree dependent factors with indirect loading components dfactor = factors.mmwe #-- Calculating cos/sin of phi arrays #-- output [m,phi] m = np.arange(MMAX + 1) ccos = np.cos(np.dot(m[:, np.newaxis], phi)) ssin = np.sin(np.dot(m[:, np.newaxis], phi)) #-- Calculates fully-normalized Legendre Polynomials with plm_holmes.py #-- Output is plm[l,m,th] plm = np.zeros((LMAX + 1, MMAX + 1, nlat)) #-- added option to precompute plms to improve computational speed if PLM is None: #-- if plms are not pre-computed: calculate Legendre polynomials PLM, dPLM = plm_holmes(LMAX, np.cos(th)) #-- Multiplying by integration factors [sin(theta)*dtheta*dphi] #-- truncate legendre polynomials to spherical harmonic order MMAX m = np.arange(MMAX + 1) for j in range(0, nlat): plm[:, m, j] = PLM[:, m, j] * np.sin(th[j]) * dphi * dth #-- Initializing preliminary spherical harmonic matrices yclm = np.zeros((LMAX + 1, MMAX + 1)) yslm = np.zeros((LMAX + 1, MMAX + 1)) #-- Initializing output spherical harmonic matrices clm = np.zeros((LMAX + 1, MMAX + 1)) slm = np.zeros((LMAX + 1, MMAX + 1)) for l in range(0, LMAX + 1): #-- equivalent to 0:LMAX mm = np.min([MMAX, l]) #-- truncate to MMAX if specified (if l > MMAX) m = np.arange(0, mm + 1) #-- mm+1 elements between 0 and mm #-- Multiplying gridded data with sin/cos of m#phis #-- This will sum through all phis in the dot product #-- output [m,theta] pfactor = PG * (R / rad_e)**(l + 2) dcos = np.dot(ccos, pfactor) dsin = np.dot(ssin, pfactor) #-- Summing product of plms and data over all latitudes #-- axis=1 signifies the direction of the summation (colatitude (th)) #-- ycos and ysin are the SH coefficients before normalizing yclm[l, m] = np.sum(plm[l, m, :] * dcos[m, :], axis=1) yslm[l, m] = np.sum(plm[l, m, :] * dsin[m, :], axis=1) #-- Multiplying by factors to normalize clm[l, m] = dfactor[l] * yclm[l, m] slm[l, m] = dfactor[l] * yslm[l, m] #-- return the harmonics return { 'clm': clm, 'slm': slm, 'l': np.arange(LMAX + 1), 'm': np.arange(MMAX + 1) }
def gen_disc_load(data, lon, lat, area, LMAX=60, MMAX=None, PLM=None, LOVE=None): """ Calculates spherical harmonic coefficients for a uniform disc load Arguments --------- data: data magnitude in gigatonnes lon: longitude of disc center lat: latitude of disc center area: area of disc in km^2 Keyword arguments ----------------- LMAX: Upper bound of Spherical Harmonic Degrees MMAX: Upper bound of Spherical Harmonic Orders PLM: input Legendre polynomials LOVE: input load Love numbers up to degree LMAX (hl,kl,ll) Returns ------- clm: cosine spherical harmonic coefficients slm: sine spherical harmonic coefficients l: spherical harmonic degree to LMAX m: spherical harmonic order to MMAX """ #-- upper bound of spherical harmonic orders (default = LMAX) if MMAX is None: MMAX = np.copy(LMAX) #-- Earth Parameters factors = units(lmax=LMAX) rho_e = factors.rho_e #-- Average Density of the Earth [g/cm^3] rad_e = factors.rad_e #-- Average Radius of the Earth [cm] #-- convert lon and lat to radians phi = lon * np.pi / 180.0 #-- Longitude in radians th = (90.0 - lat) * np.pi / 180.0 #-- Colatitude in radians #-- convert input area into cm^2 and then divide by area of a half sphere #-- alpha will be 1 - the ratio of the input area with the half sphere alpha = (1.0 - 1e10 * area / (2.0 * np.pi * rad_e**2)) #-- Input data is in gigatonnes (Gt) #-- 1e15 converts from Gt to grams, 1e10 converts from km^2 to cm^2 unit_conv = 1e15 / (1e10 * area) #-- Coefficient for calculating Stokes coefficients for a disc load #-- From Jacob et al (2012), Farrell (1972) and Longman (1962) coeff = 3.0 / (rad_e * rho_e) #-- extract arrays of kl, hl, and ll Love Numbers hl, kl, ll = LOVE #-- calculate array of l values ranging from 0 to LMAX (harmonic degrees) #-- LMAX+1 as there are LMAX+1 elements between 0 and LMAX l = np.arange(LMAX + 1) #-- calculate SH degree dependent factors to convert from coefficients #-- of mass into normalized geoid coefficients #-- NOTE: these are not the normal factors for converting to geoid due #-- to the square of the denominator #-- kl[l] is the Load Love Number of degree l dfactor = (1.0 + kl[l]) / ((1.0 + 2.0 * l)**2) #-- Calculating plms of the disc #-- allocating for constructed array pl_alpha = np.zeros((LMAX + 1)) #-- l=0 is a special case (P(-1) = 1, P(1) = cos(alpha)) pl_alpha[0] = (1.0 - alpha) / 2.0 #-- for all other degrees: calculate the legendre polynomials up to LMAX+1 pl_matrix, dpl_matrix = legendre_polynomials(LMAX + 1, alpha) for l in range(1, LMAX + 1): #-- LMAX+1 to include LMAX #-- from Longman (1962) and Jacob et al (2012) #-- unnormalizing Legendre polynomials #-- sqrt(2*l - 1) == sqrt(2*(l-1) + 1) #-- sqrt(2*l + 3) == sqrt(2*(l+1) + 1) pl_lower = pl_matrix[l - 1] / np.sqrt(2.0 * l - 1.0) pl_upper = pl_matrix[l + 1] / np.sqrt(2.0 * l + 3.0) pl_alpha[l] = (pl_lower - pl_upper) / 2.0 #-- Calculate Legendre Polynomials using Holmes and Featherstone relation #-- this would be the plm for the center of the disc load #-- used to rotate the disc load to point lat/lon if PLM is None: plmout, dplm = plm_holmes(LMAX, np.cos(th)) #-- truncate precomputed plms to order plmout = np.squeeze(plmout[:, :MMAX + 1, :]) else: #-- truncate precomputed plms to degree and order plmout = PLM[:LMAX + 1, :MMAX + 1] #-- calculate array of m values ranging from 0 to MMAX (harmonic orders) #-- MMAX+1 as there are MMAX+1 elements between 0 and MMAX m = np.arange(MMAX + 1) #-- Multiplying by the units conversion factor (unit_conv) to #-- convert from the input units into cmH2O equivalent #-- Multiplying point mass data (converted to cmH2O) with sin/cos of m*phis #-- data normally is 1 for a uniform 1cm water equivalent layer #-- but can be a mass point if reconstructing a spherical harmonic field #-- NOTE: NOT a matrix multiplication as data (and phi) is a single point dcos = unit_conv * data * np.cos(m * phi) dsin = unit_conv * data * np.sin(m * phi) #-- Multiplying by plm_alpha (F_l from Jacob 2012) plm = np.zeros((LMAX + 1, MMAX + 1)) #-- Initializing preliminary spherical harmonic matrices yclm = np.zeros((LMAX + 1, MMAX + 1)) yslm = np.zeros((LMAX + 1, MMAX + 1)) #-- Initializing output spherical harmonic matrices Ylms = {} Ylms['l'] = np.arange(LMAX + 1) Ylms['m'] = np.arange(MMAX + 1) Ylms['clm'] = np.zeros((LMAX + 1, MMAX + 1)) Ylms['slm'] = np.zeros((LMAX + 1, MMAX + 1)) for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX l = np.arange(m, LMAX + 1) #-- LMAX+1 to include LMAX #-- rotate disc load to be centered at lat/lon plm[l, m] = plmout[l, m] * pl_alpha[l] #-- multiplying clm by cos(m*phi) and slm by sin(m*phi) #-- to get a field of spherical harmonics yclm[l, m] = plm[l, m] * dcos[m] yslm[l, m] = plm[l, m] * dsin[m] #-- multiplying by coefficients to convert to geoid coefficients Ylms['clm'][l, m] = coeff * dfactor[l] * yclm[l, m] Ylms['slm'][l, m] = coeff * dfactor[l] * yslm[l, m] #-- return the output spherical harmonics return Ylms
def mascon_reconstruct(DSET, LMAX, RAD, START=None, END=None, MMAX=None, DESTRIPE=False, LOVE_NUMBERS=0, REFERENCE=None, GIA=None, GIA_FILE=None, ATM=False, DATAFORM=None, MASCON_FILE=None, REDISTRIBUTE_MASCONS=False, RECONSTRUCT_FILE=None, LANDMASK=None, OUTPUT_DIRECTORY=None, MODE=0o775): #-- for datasets not GSM: will add a label for the dataset dset_str = '' if (DSET == 'GSM') else '_{0}'.format(DSET) #-- atmospheric ECMWF "jump" flag (if ATM) atm_str = '_wATM' if ATM else '' #-- Gaussian smoothing string for radius RAD gw_str = '_r{0:0.0f}km'.format(RAD) if (RAD != 0) else '' #-- input GIA spherical harmonic datafiles GIA_Ylms_rate = read_GIA_model(GIA_FILE, GIA=GIA, LMAX=LMAX, MMAX=MMAX) gia_str = '_{0}'.format(GIA_Ylms_rate['title']) if GIA else '' #-- output string for both LMAX==MMAX and LMAX != MMAX cases MMAX = np.copy(LMAX) if not MMAX else MMAX order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else '' #-- filter grace coefficients flag ds_str = '_FL' if DESTRIPE else '' #-- output filename suffix suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') #-- file parser for reading index files #-- removes commented lines (can comment out files in the index) #-- removes empty lines (if there are extra empty lines) parser = re.compile(r'^(?!\#|\%|$)', re.VERBOSE) #-- create initial reconstruct index for calc_mascon.py fid = open(RECONSTRUCT_FILE, 'w') #-- output file format file_format = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}_{9:03d}-{10:03d}.{11}' #-- read load love numbers hl, kl, ll = load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE) #-- Earth Parameters factors = units(lmax=LMAX).harmonic(hl, kl, ll) #-- Average Density of the Earth [g/cm^3] rho_e = factors.rho_e #-- Average Radius of the Earth [cm] rad_e = factors.rad_e #-- Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_MASCONS: #-- read Land-Sea Mask and convert to spherical harmonics ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll)) ocean_str = '_OCN' else: #-- not distributing uniformly over ocean ocean_str = '' #-- input mascon spherical harmonic datafiles with open(MASCON_FILE, 'r') as f: mascon_files = [l for l in f.read().splitlines() if parser.match(l)] for k, fi in enumerate(mascon_files): #-- read mascon spherical harmonics if (DATAFORM == 'ascii'): #-- ascii (.txt) Ylms = harmonics().from_ascii(os.path.expanduser(fi), date=False) elif (DATAFORM == 'netCDF4'): #-- netcdf (.nc) Ylms = harmonics().from_netCDF4(os.path.expanduser(fi), date=False) elif (DATAFORM == 'HDF5'): #-- HDF5 (.H5) Ylms = harmonics().from_HDF5(os.path.expanduser(fi), date=False) #-- Calculating the total mass of each mascon (1 cmwe uniform) total_area = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0 #-- distribute mascon mass uniformly over the ocean if REDISTRIBUTE_MASCONS: #-- calculate ratio between total mascon mass and #-- a uniformly distributed cm of water over the ocean ratio = Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0] #-- for each spherical harmonic for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX for l in range(m, LMAX + 1): #-- LMAX+1 to include LMAX #-- remove ratio*ocean Ylms from mascon Ylms #-- note: x -= y is equivalent to x = x - y Ylms.clm[l, m] -= ratio * ocean_Ylms.clm[l, m] Ylms.slm[l, m] -= ratio * ocean_Ylms.slm[l, m] #-- truncate mascon spherical harmonics to d/o LMAX/MMAX Ylms = Ylms.truncate(lmax=LMAX, mmax=MMAX) #-- mascon base is the file without directory or suffix mascon_base = os.path.basename(fi) mascon_base = os.path.splitext(mascon_base)[0] #-- if lower case, will capitalize mascon_base = mascon_base.upper() #-- if mascon name contains degree and order info, remove mascon_name = mascon_base.replace('_L{0:d}'.format(LMAX), '') #-- input filename format (for both LMAX==MMAX and LMAX != MMAX cases): #-- mascon name, GRACE dataset, GIA model, LMAX, (MMAX,) #-- Gaussian smoothing, filter flag, remove reconstructed fields flag #-- output GRACE error file args = (mascon_name, dset_str, gia_str.upper(), atm_str, ocean_str, LMAX, order_str, gw_str, ds_str) file_input = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}.txt'.format(*args) mascon_data_input = np.loadtxt( os.path.join(OUTPUT_DIRECTORY, file_input)) #-- convert mascon time-series from Gt to cmwe mascon_sigma = 1e15 * mascon_data_input[:, 2] / total_area #-- mascon time-series Ylms mascon_Ylms = Ylms.scale(mascon_sigma) mascon_Ylms.time = mascon_data_input[:, 1].copy() mascon_Ylms.month = mascon_data_input[:, 0].astype(np.int64) #-- output to file: no ascii option args = (mascon_name, dset_str, gia_str.upper(), atm_str, ocean_str, LMAX, order_str, gw_str, ds_str, START, END, suffix[DATAFORM]) FILE = file_format.format(*args) #-- output harmonics to file if (DATAFORM == 'netCDF4'): #-- netcdf (.nc) mascon_Ylms.to_netCDF4(os.path.join(OUTPUT_DIRECTORY, FILE)) elif (DATAFORM == 'HDF5'): #-- HDF5 (.H5) mascon_Ylms.to_HDF5(os.path.join(OUTPUT_DIRECTORY, FILE)) #-- print file name to index print(tilde_compress(os.path.join(OUTPUT_DIRECTORY, FILE)), file=fid) #-- change the permissions mode os.chmod(os.path.join(OUTPUT_DIRECTORY, FILE), MODE) #-- close the reconstruct index fid.close() #-- change the permissions mode of the index file os.chmod(RECONSTRUCT_FILE, MODE)
def least_squares_mascons(parameters, LOVE_NUMBERS=0, REFERENCE=None, MODE=0o775): #-- convert parameters to variables #-- index file of data files (containing path) #-- path.expanduser = tilde expansion of path INDEX_FILE = os.path.expanduser(parameters['INDEX_FILE']) DATAFORM = parameters['DATAFORM'] #-- spherical harmonic degree range LMIN = np.int(parameters['LMIN']) LMAX = np.int(parameters['LMAX']) #-- maximum spherical harmonic order if (parameters['MMAX'].title() == 'None'): MMAX = np.copy(LMAX) else: MMAX = np.int(parameters['MMAX']) #-- gaussian smoothing radius RAD = np.float(parameters['RAD']) #-- filter coefficients for stripe effects DESTRIPE = parameters['DESTRIPE'] in ('Y', 'y') #-- index of mascons spherical harmonics MASCON_INDEX = os.path.expanduser(parameters['MASCON_INDEX']) #-- mascon distribution over the ocean MASCON_OCEAN = parameters['MASCON_OCEAN'] in ('Y', 'y') #-- destribute total mass of spherical harmonics over the ocean REDISTRIBUTE = parameters['REDISTRIBUTE'] in ('Y', 'y') #-- calculating with data errors DATA_ERROR = parameters['DATA_ERROR'] in ('Y', 'y') #-- input data have dates DATE = parameters['DATE'] in ('Y', 'y') #-- output directory DIRECTORY = os.path.expanduser(parameters['DIRECTORY']) #-- 1: fit mass, 2: fit geoid FIT_METHOD = np.int(parameters['FIT_METHOD']) #-- Recursively create output directory if not currently existing if (not os.access(DIRECTORY, os.F_OK)): os.makedirs(DIRECTORY, mode=MODE, exist_ok=True) #-- list object of output files for file logs (full path) output_files = [] #-- read arrays of kl, hl, and ll Love Numbers hl, kl, ll = load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF') #-- Earth Parameters factors = units(lmax=LMAX).harmonic(hl, kl, ll) #-- Average Density of the Earth [g/cm^3] rho_e = factors.rho_e #-- Average Radius of the Earth [cm] rad_e = factors.rad_e #-- Calculating the Gaussian smoothing for radius RAD if (RAD != 0): wt = 2.0 * np.pi * gauss_weights(RAD, LMAX) gw_str = '_r{0:0.0f}km'.format(RAD) else: #-- else = 1 wt = np.ones((LMAX + 1)) gw_str = '' #-- output string for both LMAX==MMAX and LMAX != MMAX cases order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else '' #-- output string for destriped harmonics ds_str = '_FL' if DESTRIPE else '' #-- Read Ocean function and convert to Ylms for redistribution if (MASCON_OCEAN | REDISTRIBUTE): #-- read Land-Sea Mask and convert to spherical harmonics LANDMASK = os.path.expanduser(parameters['LANDMASK']) ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll)) ocean_str = '_OCN' else: #-- not distributing uniformly over ocean ocean_str = '' #-- input spherical harmonic datafile index #-- correspond file names with GRACE month #-- this allows additional months to be in the index data_Ylms = harmonics().from_index(INDEX_FILE, DATAFORM, date=DATE) #-- number of files within the index n_files = data_Ylms.shape[-1] #-- truncate to degree and order data_Ylms = data_Ylms.truncate(lmax=LMAX, mmax=MMAX) #-- Total mass in the simulated input data in gigatonnes rmass = 4.0 * np.pi * (rad_e** 3.0) * rho_e * data_Ylms.clm[0, 0, :] / 3.0 / 1e15 #-- distribute Ylms uniformly over the ocean if REDISTRIBUTE: #-- calculate ratio between total removed mass and #-- a uniformly distributed cm of water over the ocean ratio = data_Ylms.clm[0, 0, :] / ocean_Ylms['clm'][0, 0] #-- for each spherical harmonic for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX for l in range(m, LMAX + 1): #-- LMAX+1 to include LMAX #-- remove the ratio*ocean Ylms from Ylms #-- note: x -= y is equivalent to x = x - y data_Ylms.clm[l, m, :] -= ratio * ocean_Ylms['clm'][l, m] data_Ylms.slm[l, m, :] -= ratio * ocean_Ylms['slm'][l, m] #-- filter data coefficients if DESTRIPE: data_Ylms = data_Ylms.destripe() #-- input mascon spherical harmonic datafiles with open(MASCON_INDEX, 'r') as f: mascon_files = f.read().splitlines() #-- number of mascons n_mas = len(mascon_files) #-- spatial area of the mascon area_tot = np.zeros((n_mas)) #-- name of each mascon mascon_name = [] #-- for each valid file in the index (iterate over mascons) mascon_list = [] for k in range(n_mas): #-- read mascon spherical harmonics if (DATAFORM == 'ascii'): #-- ascii (.txt) Ylms = harmonics().from_ascii(mascon_files[k], date=False) elif (DATAFORM == 'netCDF4'): #-- netcdf (.nc) Ylms = harmonics().from_netCDF4(mascon_files[k], date=False) elif (DATAFORM == 'HDF5'): #-- HDF5 (.H5) Ylms = harmonics().from_HDF5(mascon_files[k], date=False) #-- Calculating the total mass of each mascon (1 cmH2O uniform) area_tot[k] = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0 #-- distribute MASCON mass uniformly over the ocean if MASCON_OCEAN: #-- calculate ratio between total mascon mass and #-- a uniformly distributed cm of water over the ocean ratio = Ylms.clm[0, 0] / ocean_Ylms['clm'][0, 0] #-- for each spherical harmonic for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX for l in range(m, LMAX + 1): #-- LMAX+1 to include LMAX #-- remove ratio*ocean Ylms from mascon Ylms #-- note: x -= y is equivalent to x = x - y Ylms.clm[l, m] -= ratio * ocean_Ylms['clm'][l, m] Ylms.slm[l, m] -= ratio * ocean_Ylms['slm'][l, m] #-- truncate mascon spherical harmonics to d/o LMAX/MMAX and add to list mascon_list.append(Ylms.truncate(lmax=LMAX, mmax=MMAX)) #-- mascon base is the file without directory or suffix mascon_base = os.path.basename(mascon_files[k]) mascon_base = os.path.splitext(mascon_base)[0] #-- if lower case, will capitalize mascon_base = mascon_base.upper() #-- if mascon name contains degree and order info, remove mascon_name.append(mascon_base.replace('_L{0:d}'.format(LMAX), '')) #-- create single harmonics object from list mascon_Ylms = harmonics().from_list(mascon_list, date=False) #-- Calculating the number of cos and sin harmonics between LMIN and LMAX #-- taking into account MMAX (if MMAX == LMAX then LMAX-MMAX=0) n_harm = np.int(LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX)**2 - (LMAX - MMAX)) #-- Initialing harmonics for least squares fitting #-- mascon kernel M_lm = np.zeros((n_harm, n_mas)) #-- mascon kernel converted to output unit MA_lm = np.zeros((n_harm, n_mas)) #-- corrected clm and slm Y_lm = np.zeros((n_harm, n_files)) #-- sensitivity kernel A_lm = np.zeros((n_harm, n_mas)) #-- Initializing output Mascon time-series mascon = np.zeros((n_mas, n_files)) #-- Initializing conversion factors #-- factor for converting to coefficients of mass fact = np.zeros((n_harm)) #-- smoothing factor wt_lm = np.zeros((n_harm)) #-- ii is a counter variable for building the mascon column array ii = 0 #-- Creating column array of clm/slm coefficients #-- Order is [C00...C6060,S11...S6060] coeff = rho_e * rad_e / 3.0 #-- Switching between Cosine and Sine Stokes for cs, csharm in enumerate(['clm', 'slm']): #-- copy cosine and sin harmonics mascon_harm = getattr(mascon_Ylms, csharm) data_harm = getattr(data_Ylms, csharm) #-- for each spherical harmonic degree #-- +1 to include LMAX for l in range(LMIN, LMAX + 1): #-- for each spherical harmonic order #-- Sine Stokes for (m=0) = 0 mm = np.min([MMAX, l]) #-- +1 to include l or MMAX (whichever is smaller) for m in range(cs, mm + 1): #-- Mascon Spherical Harmonics M_lm[ii, :] = np.copy(mascon_harm[l, m, :]) #-- Data Spherical Harmonics Y_lm[ii, :] = np.copy(data_harm[l, m, :]) #-- degree dependent factor to convert to mass fact[ii] = (2.0 * l + 1.0) / (1.0 + kl[l]) #-- degree dependent smoothing wt_lm[ii] = np.copy(wt[l]) #-- add 1 to counter ii += 1 #-- free up memory from data harmonics data_Ylms.clm = None data_Ylms.slm = None #-- Converting mascon coefficients to fit method if (FIT_METHOD == 1): #-- Fitting Sensitivity Kernel as mass coefficients #-- converting M_lm to mass coefficients of the kernel for i in range(n_harm): MA_lm[i, :] = wt_lm[i] * M_lm[i, :] * fact[i] fit_factor = wt_lm * fact else: #-- Fitting Sensitivity Kernel as geoid coefficients MA_lm[:, :] = np.copy(M_lm) fit_factor = wt_lm * np.ones((n_harm)) #-- Fitting the sensitivity kernel from the input kernel for i in range(n_harm): #-- setting kern_i equal to 1 for d/o kern_i = np.zeros((n_harm)) #-- converting to mass coefficients if specified kern_i[i] = 1.0 * fit_factor[i] #-- spherical harmonics solution for the #-- mascon sensitivity kernels #-- Least Squares Solutions: Inv(X'.X).(X'.Y) kern_lm = np.linalg.lstsq(MA_lm, kern_i, rcond=-1)[0] for k in range(n_mas): A_lm[i, k] = kern_lm[k] * area_tot[k] #-- output formatting string if containing date variables if DATE: formatting_string = '{0:03d} {1:12.4f} {2:16.10f} {3:16.5f} {4:16.10f}' else: formatting_string = '{0:16.10f} {1:16.5f} {2:16.10f}' #-- for each mascon for k in range(n_mas): #-- output filename format: #-- mascon name, LMAX, Gaussian smoothing radii, filter flag file_out = '{0}{1}_L{2:d}{3}{4}{5}{6}.txt'.format( parameters['FILENAME'], mascon_name[k], LMAX, order_str, gw_str, ds_str, ocean_str) #-- add output files to list object output_files.append(os.path.join(DIRECTORY, file_out)) #-- Output mascon datafiles #-- Will output each mascon mass series #-- if with dates: #-- month, date, mascon mass, mascon area #-- else: #-- mascon mass, mascon area #-- open output mascon time-series file fid = open(os.path.join(DIRECTORY, file_out), 'w') #-- for each date for f in range(n_files): #-- Summing over all spherical harmonics for mascon k, and time t #-- multiplies by the degree dependent factor to convert #-- the harmonics into mass coefficients #-- Converting mascon mass time-series from g to gigatonnes if DATA_ERROR: #-- if input data are errors (i.e. estimated dealiasing errors) mascon[k, f] = np.sqrt(np.sum( (A_lm[:, k] * Y_lm[:, f])**2)) / 1e15 else: mascon[k, f] = np.sum(A_lm[:, k] * Y_lm[:, f]) / 1e15 #-- output to file if DATE: #-- if files contain date information args = (data_Ylms.month[f], data_Ylms.time[f], mascon[k, f], area_tot[k] / 1e10, rmass[f]) print(formatting_string.format(*args), file=fid) else: #-- just print the time-series and mascon areas args = (mascon[k, f], area_tot[k] / 1e10, rmass[f]) print(formatting_string.format(*args), file=fid) #-- close the output file fid.close() #-- change the permissions mode of the output file os.chmod(os.path.join(DIRECTORY, file_out), MODE) #-- add output files to list object output_files.append(os.path.join(DIRECTORY, file_out)) #-- return the list of output files return output_files
def gen_spherical_cap(data, lon, lat, LMAX=60, MMAX=None, AREA=0, RAD_CAP=0, RAD_KM=0, UNITS=1, PLM=None, LOVE=None): """ Calculates spherical harmonic coefficients for a spherical cap Arguments --------- data: data magnitude lon: longitude of spherical cap center lat: latitude of spherical cap center Keyword arguments ----------------- LMAX: Upper bound of Spherical Harmonic Degrees MMAX: Upper bound of Spherical Harmonic Orders AREA: spherical cap area in cm^2 UNITS: input data units 1: cm of water thickness (default) 2: gigatonnes of mass 3: kg/m^2 PLM: input Legendre polynomials LOVE: input load Love numbers up to degree LMAX (hl,kl,ll) Returns ------- clm: cosine spherical harmonic coefficients slm: sine spherical harmonic coefficients l: spherical harmonic degree to LMAX m: spherical harmonic order to MMAX """ #-- upper bound of spherical harmonic orders (default = LMAX) if MMAX is None: MMAX = np.copy(LMAX) #-- Earth Parameters factors = units(lmax=LMAX) rho_e = factors.rho_e #-- Average Density of the Earth [g/cm^3] rad_e = factors.rad_e #-- Average Radius of the Earth [cm] #-- convert lon and lat to radians phi = lon * np.pi / 180.0 #-- Longitude in radians th = (90.0 - lat) * np.pi / 180.0 #-- Colatitude in radians #-- Converting input area into an equivalent spherical cap radius #-- Following Jacob et al. (2012) Equation 4 and 5 #-- alpha is the vertical semi-angle subtending a cone at the #-- center of the earth if (RAD_CAP != 0): #-- if given spherical cap radius in degrees #-- converting to radians alpha = RAD_CAP * np.pi / 180.0 elif (AREA != 0): #-- if given spherical cap area in cm^2 #-- radius in centimeters radius_cm = np.sqrt(AREA / np.pi) #-- Calculating angular radius of spherical cap alpha = (radius_cm / rad_e) elif (RAD_KM != 0): #-- if given spherical cap radius in kilometers #-- Calculating angular radius of spherical cap alpha = (1e5 * RAD_KM) / rad_e else: raise ValueError('Input RAD_CAP, AREA or RAD_KM of spherical cap') #-- Calculate factor to convert from input units into cmH2O equivalent #-- Default input is for inputs already in cmH2O (unit_conv = 1) if (UNITS == 1): #-- Input data is in cm water equivalent (cmH2O) unit_conv = 1.0 elif (UNITS == 2): #-- Input data is in gigatonnes (Gt) #-- calculate spherical cap area from angular radius area = np.pi * (alpha * rad_e)**2 #-- the 1.e15 converts from gigatons/cm^2 to cm of water #-- 1 g/cm^3 = 1000 kg/m^3 = density water #-- 1 Gt = 1 Pg = 1.e15 g unit_conv = 1.e15 / area elif (UNITS == 3): #-- Input data is in kg/m^2 #-- 1 kg = 1000 g #-- 1 m^2 = 100*100 cm^2 = 1e4 cm^2 unit_conv = 0.1 else: raise ValueError('UNITS (1: cmH2O, 2: Gt, 3: kg/m^2)') #-- Coefficient for calculating Stokes coefficients for a spherical cap #-- From Jacob et al (2012), Farrell (1972) and Longman (1962) coeff = 3.0 / (rad_e * rho_e) #-- extract arrays of kl, hl, and ll Love Numbers hl, kl, ll = LOVE #-- calculate array of l values ranging from 0 to LMAX (harmonic degrees) #-- LMAX+1 as there are LMAX+1 elements between 0 and LMAX l = np.arange(LMAX + 1) #-- calculate SH degree dependent factors to convert from coefficients #-- of mass into normalized geoid coefficients #-- NOTE: these are not the normal factors for converting to geoid due #-- to the square of the denominator #-- kl[l] is the Load Love Number of degree l dfactor = (1.0 + kl[l]) / ((1.0 + 2.0 * l)**2) #-- Calculating plms of the spherical caps #-- From Longman et al. (1962) #-- pl_alpha = F(alpha) from Jacob 2011 #-- pl_alpha is purely zonal and depends only on the size of the cap #-- allocating for constructed array pl_alpha = np.zeros((LMAX + 1)) #-- l=0 is a special case (P(-1) = 1, P(1) = cos(alpha)) pl_alpha[0] = (1.0 - np.cos(alpha)) / 2.0 #-- for all other degrees: calculate the legendre polynomials up to LMAX+1 pl_matrix, dpl_matrix = legendre_polynomials(LMAX + 1, np.cos(alpha)) for l in range(1, LMAX + 1): #-- LMAX+1 to include LMAX #-- from Longman (1962) and Jacob et al (2012) #-- unnormalizing Legendre polynomials #-- sqrt(2*l - 1) == sqrt(2*(l-1) + 1) #-- sqrt(2*l + 3) == sqrt(2*(l+1) + 1) pl_lower = pl_matrix[l - 1] / np.sqrt(2.0 * l - 1.0) pl_upper = pl_matrix[l + 1] / np.sqrt(2.0 * l + 3.0) pl_alpha[l] = (pl_lower - pl_upper) / 2.0 #-- Calculating Legendre Polynomials #-- added option to precompute plms to improve computational speed #-- this would be the plm for the center of the spherical cap #-- used to rotate the spherical cap to point lat/lon if PLM is None: plmout, dplm = plm_holmes(LMAX, np.cos(th)) #-- truncate precomputed plms to order plmout = np.squeeze(plmout[:, :MMAX + 1, :]) else: #-- truncate precomputed plms to degree and order plmout = PLM[:LMAX + 1, :MMAX + 1] #-- calculate array of m values ranging from 0 to MMAX (harmonic orders) #-- MMAX+1 as there are MMAX+1 elements between 0 and MMAX m = np.arange(MMAX + 1) #-- Multiplying by the units conversion factor (unit_conv) to #-- convert from the input units into cmH2O equivalent #-- Multiplying point mass data (converted to cmH2O) with sin/cos of m*phis #-- data normally is 1 for a uniform 1cm water equivalent layer #-- but can be a mass point if reconstructing a spherical harmonic field #-- NOTE: NOT a matrix multiplication as data (and phi) is a single point dcos = unit_conv * data * np.cos(m * phi) dsin = unit_conv * data * np.sin(m * phi) #-- Multiplying by plm_alpha (F_l from Jacob 2012) plm = np.zeros((LMAX + 1, MMAX + 1)) #-- Initializing preliminary spherical harmonic matrices yclm = np.zeros((LMAX + 1, MMAX + 1)) yslm = np.zeros((LMAX + 1, MMAX + 1)) #-- Initializing output spherical harmonic matrices Ylms = {} Ylms['l'] = np.arange(LMAX + 1) Ylms['m'] = np.arange(MMAX + 1) Ylms['clm'] = np.zeros((LMAX + 1, MMAX + 1)) Ylms['slm'] = np.zeros((LMAX + 1, MMAX + 1)) for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX l = np.arange(m, LMAX + 1) #-- LMAX+1 to include LMAX #-- rotate spherical cap to be centered at lat/lon plm[l, m] = plmout[l, m] * pl_alpha[l] #-- multiplying clm by cos(m*phi) and slm by sin(m*phi) #-- to get a field of spherical harmonics yclm[l, m] = plm[l, m] * dcos[m] yslm[l, m] = plm[l, m] * dsin[m] #-- multiplying by coefficients to convert to geoid coefficients Ylms['clm'][l, m] = coeff * dfactor[l] * yclm[l, m] Ylms['slm'][l, m] = coeff * dfactor[l] * yslm[l, m] #-- return the output spherical harmonics return Ylms
def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, START=None, END=None, MISSING=None, LMIN=None, MMAX=None, LOVE_NUMBERS=0, REFERENCE=None, DESTRIPE=False, UNITS=None, DDEG=None, INTERVAL=None, BOUNDS=None, GIA=None, GIA_FILE=None, ATM=False, POLE_TIDE=False, DEG1=None, DEG1_FILE=None, MODEL_DEG1=False, SLR_C20=None, SLR_21=None, SLR_22=None, SLR_C30=None, SLR_C50=None, DATAFORM=None, MEAN_FILE=None, MEANFORM=None, REMOVE_FILES=None, REMOVE_FORMAT=None, REDISTRIBUTE_REMOVED=False, LANDMASK=None, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, VERBOSE=False, MODE=0o775): #-- recursively create output directory if not currently existing if not os.access(OUTPUT_DIRECTORY, os.F_OK): os.makedirs(OUTPUT_DIRECTORY, mode=MODE, exist_ok=True) #-- list object of output files for file logs (full path) output_files = [] #-- file information suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') #-- read arrays of kl, hl, and ll Love Numbers hl, kl, ll = load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE) #-- Calculating the Gaussian smoothing for radius RAD if (RAD != 0): wt = 2.0 * np.pi * gauss_weights(RAD, LMAX) gw_str = '_r{0:0.0f}km'.format(RAD) else: #-- else = 1 wt = np.ones((LMAX + 1)) gw_str = '' #-- flag for spherical harmonic order MMAX = np.copy(LMAX) if not MMAX else MMAX order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else '' #-- reading GRACE months for input date range #-- replacing low-degree harmonics with SLR values if specified #-- include degree 1 (geocenter) harmonics if specified #-- correcting for Pole-Tide and Atmospheric Jumps if specified Ylms = grace_input_months(base_dir, PROC, DREL, DSET, LMAX, START, END, MISSING, SLR_C20, DEG1, MMAX=MMAX, SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C50=SLR_C50, DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, POLE_TIDE=POLE_TIDE) #-- convert to harmonics object and remove mean if specified GRACE_Ylms = harmonics().from_dict(Ylms) GRACE_Ylms.directory = Ylms['directory'] #-- use a mean file for the static field to remove if MEAN_FILE: #-- read data form for input mean file (ascii, netCDF4, HDF5, gfc) mean_Ylms = harmonics().from_file(MEAN_FILE, format=MEANFORM, date=False) #-- remove the input mean GRACE_Ylms.subtract(mean_Ylms) else: GRACE_Ylms.mean(apply=True) #-- date information of GRACE/GRACE-FO coefficients nfiles = len(GRACE_Ylms.time) #-- filter GRACE/GRACE-FO coefficients if DESTRIPE: #-- destriping GRACE/GRACE-FO coefficients ds_str = '_FL' GRACE_Ylms = GRACE_Ylms.destripe() else: #-- using standard GRACE/GRACE-FO harmonics ds_str = '' #-- input GIA spherical harmonic datafiles GIA_Ylms_rate = read_GIA_model(GIA_FILE, GIA=GIA, LMAX=LMAX, MMAX=MMAX) gia_str = '_{0}'.format(GIA_Ylms_rate['title']) if GIA else '' #-- calculate the monthly mass change from GIA GIA_Ylms = GRACE_Ylms.zeros_like() GIA_Ylms.time[:] = np.copy(GRACE_Ylms.time) GIA_Ylms.month[:] = np.copy(GRACE_Ylms.month) #-- monthly GIA calculated by gia_rate*time elapsed #-- finding change in GIA each month for t in range(nfiles): GIA_Ylms.clm[:, :, t] = GIA_Ylms_rate['clm'] * (GIA_Ylms.time[t] - 2003.3) GIA_Ylms.slm[:, :, t] = GIA_Ylms_rate['slm'] * (GIA_Ylms.time[t] - 2003.3) #-- default file prefix if not FILE_PREFIX: fargs = (PROC, DREL, DSET, Ylms['title'], gia_str) FILE_PREFIX = '{0}_{1}_{2}{3}{4}_'.format(*fargs) #-- Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_REMOVED: #-- read Land-Sea Mask and convert to spherical harmonics ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll)) ocean_str = '_OCN' else: ocean_str = '' #-- input spherical harmonic datafiles to be removed from the GRACE data #-- Remove sets of Ylms from the GRACE data before returning remove_Ylms = GRACE_Ylms.zeros_like() remove_Ylms.time[:] = np.copy(GRACE_Ylms.time) remove_Ylms.month[:] = np.copy(GRACE_Ylms.month) if REMOVE_FILES: #-- extend list if a single format was entered for all files if len(REMOVE_FORMAT) < len(REMOVE_FILES): REMOVE_FORMAT = REMOVE_FORMAT * len(REMOVE_FILES) #-- for each file to be removed for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): #-- ascii (.txt) #-- netCDF4 (.nc) #-- HDF5 (.H5) Ylms = harmonics().from_file(REMOVE_FILE, format=REMOVEFORM) elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): #-- read from index file _, removeform = REMOVEFORM.split('-') #-- index containing files in data format Ylms = harmonics().from_index(REMOVE_FILE, format=removeform) #-- reduce to GRACE/GRACE-FO months and truncate to degree and order Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX, mmax=MMAX) #-- distribute removed Ylms uniformly over the ocean if REDISTRIBUTE_REMOVED: #-- calculate ratio between total removed mass and #-- a uniformly distributed cm of water over the ocean ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0] #-- for each spherical harmonic for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX for l in range(m, LMAX + 1): #-- LMAX+1 to include LMAX #-- remove the ratio*ocean Ylms from Ylms #-- note: x -= y is equivalent to x = x - y Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m] Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m] #-- filter removed coefficients if DESTRIPE: Ylms = Ylms.destripe() #-- add data for month t and INDEX_FILE to the total #-- remove_clm and remove_slm matrices #-- redistributing the mass over the ocean if specified remove_Ylms.add(Ylms) #-- Output spatial data object grid = spatial() #-- Output Degree Spacing dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) #-- Output Degree Interval if (INTERVAL == 1): #-- (-180:180,90:-90) nlon = np.int64((360.0 / dlon) + 1.0) nlat = np.int64((180.0 / dlat) + 1.0) grid.lon = -180 + dlon * np.arange(0, nlon) grid.lat = 90.0 - dlat * np.arange(0, nlat) elif (INTERVAL == 2): #-- (Degree spacing)/2 grid.lon = np.arange(-180 + dlon / 2.0, 180 + dlon / 2.0, dlon) grid.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat) nlon = len(grid.lon) nlat = len(grid.lat) elif (INTERVAL == 3): #-- non-global grid set with BOUNDS parameter minlon, maxlon, minlat, maxlat = BOUNDS.copy() grid.lon = np.arange(minlon + dlon / 2.0, maxlon + dlon / 2.0, dlon) grid.lat = np.arange(maxlat - dlat / 2.0, minlat - dlat / 2.0, -dlat) nlon = len(grid.lon) nlat = len(grid.lat) #-- Computing plms for converting to spatial domain theta = (90.0 - grid.lat) * np.pi / 180.0 PLM, dPLM = plm_holmes(LMAX, np.cos(theta)) #-- Earth Parameters #-- output spatial units unit_list = ['cmwe', 'mmGH', 'mmCU', u'\u03BCGal', 'mbar'] unit_name = [ 'Equivalent Water Thickness', 'Geoid Height', 'Elastic Crustal Uplift', 'Gravitational Undulation', 'Equivalent Surface Pressure' ] #-- Setting units factor for output #-- dfactor computes the degree dependent coefficients if (UNITS == 1): #-- 1: cmwe, centimeters water equivalent dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).cmwe elif (UNITS == 2): #-- 2: mmGH, mm geoid height dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmGH elif (UNITS == 3): #-- 3: mmCU, mm elastic crustal deformation dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmCU elif (UNITS == 4): #-- 4: micGal, microGal gravity perturbations dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).microGal elif (UNITS == 5): #-- 5: mbar, millibars equivalent surface pressure dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mbar else: raise ValueError('Invalid units code {0:d}'.format(UNITS)) #-- output file format file_format = '{0}{1}_L{2:d}{3}{4}{5}_{6:03d}.{7}' #-- converting harmonics to truncated, smoothed coefficients in units #-- combining harmonics to calculate output spatial fields for i, grace_month in enumerate(GRACE_Ylms.month): #-- GRACE/GRACE-FO harmonics for time t Ylms = GRACE_Ylms.index(i) #-- Remove GIA rate for time Ylms.subtract(GIA_Ylms.index(i)) #-- Remove monthly files to be removed Ylms.subtract(remove_Ylms.index(i)) #-- smooth harmonics and convert to output units Ylms.convolve(dfactor * wt) #-- convert spherical harmonics to output spatial grid grid.data = harmonic_summation(Ylms.clm, Ylms.slm, grid.lon, grid.lat, LMIN=LMIN, LMAX=LMAX, MMAX=MMAX, PLM=PLM).T #-- copy time variables for month grid.time = np.copy(Ylms.time) grid.month = np.copy(Ylms.month) #-- output monthly files to ascii, netCDF4 or HDF5 args = (FILE_PREFIX, unit_list[UNITS - 1], LMAX, order_str, gw_str, ds_str, grace_month, suffix[DATAFORM]) FILE = os.path.join(OUTPUT_DIRECTORY, file_format.format(*args)) if (DATAFORM == 'ascii'): #-- ascii (.txt) grid.to_ascii(FILE, date=True, verbose=VERBOSE) elif (DATAFORM == 'netCDF4'): #-- netCDF4 grid.to_netCDF4(FILE, date=True, verbose=VERBOSE, units=unit_list[UNITS - 1], longname=unit_name[UNITS - 1], title='GRACE/GRACE-FO Spatial Data') elif (DATAFORM == 'HDF5'): #-- HDF5 grid.to_HDF5(FILE, date=True, verbose=VERBOSE, units=unit_list[UNITS - 1], longname=unit_name[UNITS - 1], title='GRACE/GRACE-FO Spatial Data') #-- set the permissions mode of the output files os.chmod(FILE, MODE) #-- add file to list output_files.append(FILE) #-- return the list of output files return output_files
def gen_stokes(data, lon, lat, LMIN=0, LMAX=60, MMAX=None, UNITS=1, PLM=None, LOVE=None): """ Converts data from the spatial domain to spherical harmonic coefficients Arguments --------- data: data matrix lon: longitude array lat: latitude array Keyword arguments ----------------- LMIN: Lower bound of Spherical Harmonic Degrees LMAX: Upper bound of Spherical Harmonic Degrees MMAX: Upper bound of Spherical Harmonic Orders UNITS: input data units 1: cm of water thickness 2: Gtons of mass 3: kg/m^2 PLM: input Legendre polynomials LOVE: input load Love numbers up to degree LMAX (hl,kl,ll) Returns ------- clm: cosine spherical harmonic coefficients slm: sine spherical harmonic coefficients l: spherical harmonic degree to LMAX m: spherical harmonic order to MMAX """ #-- converting LMIN and LMAX to integer LMIN = np.int(LMIN) LMAX = np.int(LMAX) #-- upper bound of spherical harmonic orders (default = LMAX) MMAX = np.copy(LMAX) if (MMAX is None) else MMAX #-- grid dimensions nlat = np.int(len(lat)) #-- grid step dlon = np.abs(lon[1] - lon[0]) dlat = np.abs(lat[1] - lat[0]) #-- longitude degree spacing in radians dphi = dlon * np.pi / 180.0 #-- colatitude degree spacing in radians dth = dlat * np.pi / 180.0 #-- reformatting longitudes to range 0:360 (if previously -180:180) lon = np.squeeze(lon.copy()) if np.any(lon < 0): lon_ind, = np.nonzero(lon < 0) lon[lon_ind] += 360.0 #-- Longitude in radians phi = lon[np.newaxis, :] * np.pi / 180.0 #-- Colatitude in radians th = (90.0 - np.squeeze(lat.copy())) * np.pi / 180.0 #-- reforming data to lonXlat if input latXlon sz = np.shape(data) data = data.T if (sz[0] == nlat) else np.copy(data) #-- SH Degree dependent factors to convert into geodesy normalized SH's #-- use splat operator to extract arrays of kl, hl, and ll Love Numbers factors = units(lmax=LMAX).spatial(*LOVE) #-- extract degree dependent factor for specific units #-- calculate integration factors for theta and phi #-- Multiplying sin(th) with differentials of theta and phi #-- to calculate the integration factor at each latitude int_fact = np.zeros((nlat)) if (UNITS == 1): #-- Default Parameter: Input in cm w.e. (g/cm^2) dfactor = factors.cmwe int_fact[:] = np.sin(th) * dphi * dth elif (UNITS == 2): #-- Input in gigatonnes (Gt) dfactor = factors.cmwe #-- rad_e: Average Radius of the Earth [cm] int_fact[:] = 1e15 / (factors.rad_e**2) elif (UNITS == 3): #-- Input in kg/m^2 (mm w.e.) dfactor = factors.mmwe int_fact[:] = np.sin(th) * dphi * dth else: #-- default is cm w.e. (g/cm^2) dfactor = factors.cmwe int_fact[:] = np.sin(th) * dphi * dth #-- Calculating cos/sin of phi arrays #-- output [m,phi] m = np.arange(MMAX + 1) ccos = np.cos(np.dot(m[:, np.newaxis], phi)) ssin = np.sin(np.dot(m[:, np.newaxis], phi)) #-- Calculating fully-normalized Legendre Polynomials #-- Output is plm[l,m,th] plm = np.zeros((LMAX + 1, MMAX + 1, nlat)) #-- added option to precompute plms to improve computational speed if PLM is None: #-- if plms are not pre-computed: calculate Legendre polynomials PLM, dPLM = plm_holmes(LMAX, np.cos(th)) #-- Multiplying by integration factors [sin(theta)*dtheta*dphi] #-- truncate legendre polynomials to spherical harmonic order MMAX for j in range(0, nlat): plm[:, m, j] = PLM[:, m, j] * int_fact[j] #-- Initializing preliminary spherical harmonic matrices yclm = np.zeros((LMAX + 1, MMAX + 1)) yslm = np.zeros((LMAX + 1, MMAX + 1)) #-- Initializing output spherical harmonic matrices clm = np.zeros((LMAX + 1, MMAX + 1)) slm = np.zeros((LMAX + 1, MMAX + 1)) #-- Multiplying gridded data with sin/cos of m#phis #-- This will sum through all phis in the dot product #-- output [m,theta] dcos = np.dot(ccos, data) dsin = np.dot(ssin, data) for l in range(LMIN, LMAX + 1): #-- equivalent to LMIN:LMAX mm = np.min([MMAX, l]) #-- truncate to MMAX if specified (if l > MMAX) m = np.arange(0, mm + 1) #-- mm+1 elements between 0 and mm #-- Summing product of plms and data over all latitudes #-- axis=1 signifies the direction of the summation yclm[l, m] = np.sum(plm[l, m, :] * dcos[m, :], axis=1) yslm[l, m] = np.sum(plm[l, m, :] * dsin[m, :], axis=1) #-- Multiplying by factors to convert to geodesy normalized coefficients clm[l, m] = dfactor[l] * yclm[l, m] slm[l, m] = dfactor[l] * yslm[l, m] return { 'clm': clm, 'slm': slm, 'l': np.arange(LMAX + 1), 'm': np.arange(MMAX + 1) }
def calc_sensitivity_kernel(LMAX, RAD, LMIN=None, MMAX=None, LOVE_NUMBERS=0, REFERENCE=None, DATAFORM=None, MASCON_FILE=None, REDISTRIBUTE_MASCONS=False, FIT_METHOD=0, LANDMASK=None, DDEG=None, INTERVAL=None, OUTPUT_DIRECTORY=None, MODE=0o775): #-- file information suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') #-- file parser for reading index files #-- removes commented lines (can comment out files in the index) #-- removes empty lines (if there are extra empty lines) parser = re.compile(r'^(?!\#|\%|$)', re.VERBOSE) #-- Create output Directory if not currently existing if (not os.access(OUTPUT_DIRECTORY, os.F_OK)): os.mkdir(OUTPUT_DIRECTORY) #-- list object of output files for file logs (full path) output_files = [] #-- read arrays of kl, hl, and ll Love Numbers hl, kl, ll = load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE) #-- Earth Parameters factors = units(lmax=LMAX).harmonic(hl, kl, ll) #-- Average Density of the Earth [g/cm^3] rho_e = factors.rho_e #-- Average Radius of the Earth [cm] rad_e = factors.rad_e #-- input/output string for both LMAX==MMAX and LMAX != MMAX cases MMAX = np.copy(LMAX) if not MMAX else MMAX order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else '' #-- Calculating the Gaussian smoothing for radius RAD if (RAD != 0): wt = 2.0 * np.pi * gauss_weights(RAD, LMAX) gw_str = '_r{0:0.0f}km'.format(RAD) else: #-- else = 1 wt = np.ones((LMAX + 1)) gw_str = '' #-- Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_MASCONS: #-- read Land-Sea Mask and convert to spherical harmonics ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll)) ocean_str = '_OCN' else: #-- not distributing uniformly over ocean ocean_str = '' #-- input mascon spherical harmonic datafiles with open(MASCON_FILE, 'r') as f: mascon_files = [l for l in f.read().splitlines() if parser.match(l)] #-- number of mascons n_mas = len(mascon_files) #-- spatial area of the mascon total_area = np.zeros((n_mas)) #-- name of each mascon mascon_name = [] #-- for each valid file in the index (iterate over mascons) mascon_list = [] for k, fi in enumerate(mascon_files): #-- read mascon spherical harmonics Ylms = harmonics().from_file(os.path.expanduser(fi), format=DATAFORM, date=False) #-- Calculating the total mass of each mascon (1 cmwe uniform) total_area[k] = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0 #-- distribute mascon mass uniformly over the ocean if REDISTRIBUTE_MASCONS: #-- calculate ratio between total mascon mass and #-- a uniformly distributed cm of water over the ocean ratio = Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0] #-- for each spherical harmonic for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX for l in range(m, LMAX + 1): #-- LMAX+1 to include LMAX #-- remove ratio*ocean Ylms from mascon Ylms #-- note: x -= y is equivalent to x = x - y Ylms.clm[l, m] -= ratio * ocean_Ylms.clm[l, m] Ylms.slm[l, m] -= ratio * ocean_Ylms.slm[l, m] #-- truncate mascon spherical harmonics to d/o LMAX/MMAX and add to list mascon_list.append(Ylms.truncate(lmax=LMAX, mmax=MMAX)) #-- mascon base is the file without directory or suffix mascon_base = os.path.basename(mascon_files[k]) mascon_base = os.path.splitext(mascon_base)[0] #-- if lower case, will capitalize mascon_base = mascon_base.upper() #-- if mascon name contains degree and order info, remove mascon_name.append(mascon_base.replace('_L{0:d}'.format(LMAX), '')) #-- create single harmonics object from list mascon_Ylms = harmonics().from_list(mascon_list, date=False) #-- Output spatial data object grid = spatial() #-- Output Degree Spacing dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) #-- Output Degree Interval if (INTERVAL == 1): #-- (-180:180,90:-90) n_lon = np.int64((360.0 / dlon) + 1.0) n_lat = np.int64((180.0 / dlat) + 1.0) grid.lon = -180 + dlon * np.arange(0, n_lon) grid.lat = 90.0 - dlat * np.arange(0, n_lat) elif (INTERVAL == 2): #-- (Degree spacing)/2 grid.lon = np.arange(-180 + dlon / 2.0, 180 + dlon / 2.0, dlon) grid.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat) n_lon = len(grid.lon) n_lat = len(grid.lat) #-- Computing plms for converting to spatial domain theta = (90.0 - grid.lat) * np.pi / 180.0 PLM, dPLM = plm_holmes(LMAX, np.cos(theta)) #-- Calculating the number of cos and sin harmonics between LMIN and LMAX #-- taking into account MMAX (if MMAX == LMAX then LMAX-MMAX=0) n_harm = np.int64(LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX)**2 - (LMAX - MMAX)) #-- Initialing harmonics for least squares fitting #-- mascon kernel M_lm = np.zeros((n_harm, n_mas)) #-- mascon kernel converted to output unit MA_lm = np.zeros((n_harm, n_mas)) #-- sensitivity kernel A_lm = np.zeros((n_harm, n_mas)) #-- Initializing conversion factors #-- factor for converting to smoothed coefficients of mass fact = np.zeros((n_harm)) #-- factor for converting back into geoid coefficients fact_inv = np.zeros((n_harm)) #-- smoothing factor wt_lm = np.zeros((n_harm)) #-- ii is a counter variable for building the mascon column array ii = 0 #-- Creating column array of clm/slm coefficients #-- Order is [C00...C6060,S11...S6060] #-- Calculating factor to convert geoid spherical harmonic coefficients #-- to coefficients of mass (Wahr, 1998) coeff = rho_e * rad_e / 3.0 coeff_inv = 0.75 / (np.pi * rho_e * rad_e**3) #-- Switching between Cosine and Sine Stokes for cs, csharm in enumerate(['clm', 'slm']): #-- copy cosine and sin harmonics mascon_harm = getattr(mascon_Ylms, csharm) #-- for each spherical harmonic degree #-- +1 to include LMAX for l in range(LMIN, LMAX + 1): #-- for each spherical harmonic order #-- Sine Stokes for (m=0) = 0 mm = np.min([MMAX, l]) #-- +1 to include l or MMAX (whichever is smaller) for m in range(cs, mm + 1): #-- Mascon Spherical Harmonics M_lm[ii, :] = np.copy(mascon_harm[l, m, :]) #-- degree dependent factor to convert to mass fact[ii] = (2.0 * l + 1.0) / (1.0 + kl[l]) #-- degree dependent factor to convert from mass fact_inv[ii] = coeff_inv * (1.0 + kl[l]) / (2.0 * l + 1.0) #-- degree dependent smoothing wt_lm[ii] = np.copy(wt[l]) #-- add 1 to counter ii += 1 #-- Converting mascon coefficients to fit method if (FIT_METHOD == 1): #-- Fitting Sensitivity Kernel as mass coefficients #-- converting M_lm to mass coefficients of the kernel for i in range(n_harm): MA_lm[i, :] = M_lm[i, :] * wt_lm[i] * fact[i] fit_factor = wt_lm * fact inv_fit_factor = np.copy(fact_inv) else: #-- Fitting Sensitivity Kernel as geoid coefficients for i in range(n_harm): MA_lm[:, :] = M_lm[i, :] * wt_lm[i] fit_factor = wt_lm * np.ones((n_harm)) inv_fit_factor = np.ones((n_harm)) #-- Fitting the sensitivity kernel from the input kernel for i in range(n_harm): #-- setting kern_i equal to 1 for d/o kern_i = np.zeros((n_harm)) #-- converting to mass coefficients if specified kern_i[i] = 1.0 * fit_factor[i] #-- spherical harmonics solution for the #-- mascon sensitivity kernels #-- Least Squares Solutions: Inv(X'.X).(X'.Y) kern_lm = np.linalg.lstsq(MA_lm, kern_i, rcond=-1)[0] for k in range(n_mas): A_lm[i, k] = kern_lm[k] * total_area[k] #-- for each mascon for k in range(n_mas): #-- reshaping harmonics of sensitivity kernel to LMAX+1,MMAX+1 #-- calculating the spatial sensitivity kernel of each mascon #-- kernel calculated as outlined in Tiwari (2009) and Jacobs (2012) #-- Initializing output sensitivity kernel (both spatial and Ylms) kern_Ylms = harmonics(lmax=LMAX, mmax=MMAX) kern_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) kern_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) kern_Ylms.time = total_area[k] #-- counter variable for deconstructing the mascon column arrays ii = 0 #-- Switching between Cosine and Sine Stokes for cs, csharm in enumerate(['clm', 'slm']): #-- for each spherical harmonic degree #-- +1 to include LMAX for l in range(LMIN, LMAX + 1): #-- for each spherical harmonic order #-- Sine Stokes for (m=0) = 0 mm = np.min([MMAX, l]) #-- +1 to include l or MMAX (whichever is smaller) for m in range(cs, mm + 1): #-- inv_fit_factor: normalize from mass harmonics temp = getattr(kern_Ylms, csharm) temp[l, m] = inv_fit_factor[ii] * A_lm[ii, k] #-- add 1 to counter ii += 1 #-- convert spherical harmonics to output spatial grid grid.data = harmonic_summation(kern_Ylms.clm, kern_Ylms.slm, grid.lon, grid.lat, LMAX=LMAX, MMAX=MMAX, PLM=PLM).T grid.time = total_area[k] #-- output names for sensitivity kernel Ylm and spatial files #-- for both LMAX==MMAX and LMAX != MMAX cases args = (mascon_name[k], ocean_str, LMAX, order_str, gw_str, suffix[DATAFORM]) FILE1 = '{0}_SKERNEL_CLM{1}_L{2:d}{3}{4}.{5}'.format(*args) FILE2 = '{0}_SKERNEL{1}_L{2:d}{3}{4}.{5}'.format(*args) #-- output sensitivity kernel to file if (DATAFORM == 'ascii'): #-- ascii (.txt) kern_Ylms.to_ascii(os.path.join(OUTPUT_DIRECTORY, FILE1), date=False) grid.to_ascii(os.path.join(OUTPUT_DIRECTORY, FILE2), date=False, units='unitless', longname='Sensitivity_Kernel') elif (DATAFORM == 'netCDF4'): #-- netCDF4 (.nc) kern_Ylms.to_netCDF4(os.path.join(OUTPUT_DIRECTORY, FILE1), date=False) grid.to_netCDF4(os.path.join(OUTPUT_DIRECTORY, FILE2), date=False, units='unitless', longname='Sensitivity_Kernel') elif (DATAFORM == 'HDF5'): #-- netcdf (.H5) kern_Ylms.to_HDF5(os.path.join(OUTPUT_DIRECTORY, FILE1), date=False) grid.to_HDF5(os.path.join(OUTPUT_DIRECTORY, FILE2), date=False, units='unitless', longname='Sensitivity_Kernel') #-- change the permissions mode os.chmod(os.path.join(OUTPUT_DIRECTORY, FILE1), MODE) os.chmod(os.path.join(OUTPUT_DIRECTORY, FILE2), MODE) #-- add output files to list object output_files.append(os.path.join(OUTPUT_DIRECTORY, FILE1)) output_files.append(os.path.join(OUTPUT_DIRECTORY, FILE2)) #-- return the list of output files return output_files
def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, START=None, END=None, MISSING=None, LMIN=None, MMAX=None, LOVE_NUMBERS=0, REFERENCE=None, DESTRIPE=False, DDEG=None, INTERVAL=None, GIA=None, GIA_FILE=None, ATM=False, POLE_TIDE=False, DEG1=None, DEG1_FILE=None, MODEL_DEG1=False, SLR_C20=None, SLR_21=None, SLR_22=None, SLR_C30=None, SLR_C50=None, DATAFORM=None, MEAN_FILE=None, MEANFORM=None, REMOVE_FILES=None, REMOVE_FORMAT=None, REDISTRIBUTE_REMOVED=False, SCALE_FILE=None, ERROR_FILE=None, POWER_FILE=None, LANDMASK=None, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, VERBOSE=False, MODE=0o775): #-- recursively create output Directory if not currently existing if not os.access(OUTPUT_DIRECTORY, os.F_OK): os.makedirs(OUTPUT_DIRECTORY, mode=MODE, exist_ok=True) #-- list object of output files for file logs (full path) output_files = [] #-- file information suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') #-- output file format file_format = '{0}{1}{2}_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}' #-- read arrays of kl, hl, and ll Love Numbers hl,kl,ll = load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE) #-- atmospheric ECMWF "jump" flag (if ATM) atm_str = '_wATM' if ATM else '' #-- output string for both LMAX==MMAX and LMAX != MMAX cases MMAX = np.copy(LMAX) if not MMAX else MMAX order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else '' #-- output spatial units unit_str = 'cmwe' unit_name = 'Equivalent Water Thickness' #-- invalid value fill_value = -9999.0 #-- Calculating the Gaussian smoothing for radius RAD if (RAD != 0): wt = 2.0*np.pi*gauss_weights(RAD,LMAX) gw_str = '_r{0:0.0f}km'.format(RAD) else: #-- else = 1 wt = np.ones((LMAX+1)) gw_str = '' #-- Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_REMOVED: #-- read Land-Sea Mask and convert to spherical harmonics ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl,kl,ll)) #-- Grid spacing dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) #-- Grid dimensions if (INTERVAL == 1):#-- (0:360, 90:-90) nlon = np.int64((360.0/dlon)+1.0) nlat = np.int64((180.0/dlat)+1.0) elif (INTERVAL == 2):#-- degree spacing/2 nlon = np.int64((360.0/dlon)) nlat = np.int64((180.0/dlat)) #-- read data for input scale files (ascii, netCDF4, HDF5) if (DATAFORM == 'ascii'): kfactor = spatial(spacing=[dlon,dlat],nlat=nlat,nlon=nlon).from_ascii( SCALE_FILE,date=False) k_error = spatial(spacing=[dlon,dlat],nlat=nlat,nlon=nlon).from_ascii( ERROR_FILE,date=False) k_power = spatial(spacing=[dlon,dlat],nlat=nlat,nlon=nlon).from_ascii( POWER_FILE,date=False) elif (DATAFORM == 'netCDF4'): kfactor = spatial().from_netCDF4(SCALE_FILE,date=False) k_error = spatial().from_netCDF4(ERROR_FILE,date=False) k_power = spatial().from_netCDF4(POWER_FILE,date=False) elif (DATAFORM == 'HDF5'): kfactor = spatial().from_HDF5(SCALE_FILE,date=False) k_error = spatial().from_HDF5(ERROR_FILE,date=False) k_power = spatial().from_HDF5(POWER_FILE,date=False) #-- input data shape nlat,nlon = kfactor.shape #-- input GRACE/GRACE-FO spherical harmonic datafiles for date range #-- replacing low-degree harmonics with SLR values if specified #-- include degree 1 (geocenter) harmonics if specified #-- correcting for Pole-Tide and Atmospheric Jumps if specified Ylms = grace_input_months(base_dir, PROC, DREL, DSET, LMAX, START, END, MISSING, SLR_C20, DEG1, MMAX=MMAX, SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C50=SLR_C50, DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, POLE_TIDE=POLE_TIDE) #-- create harmonics object from GRACE/GRACE-FO data GRACE_Ylms = harmonics().from_dict(Ylms) GRACE_Ylms.directory = Ylms['directory'] #-- use a mean file for the static field to remove if MEAN_FILE: #-- read data form for input mean file (ascii, netCDF4, HDF5, gfc) mean_Ylms = harmonics().from_file(MEAN_FILE,format=MEANFORM,date=False) #-- remove the input mean GRACE_Ylms.subtract(mean_Ylms) else: GRACE_Ylms.mean(apply=True) #-- date information of GRACE/GRACE-FO coefficients nfiles = len(GRACE_Ylms.time) #-- filter GRACE/GRACE-FO coefficients if DESTRIPE: #-- destriping GRACE/GRACE-FO coefficients ds_str = '_FL' GRACE_Ylms = GRACE_Ylms.destripe() else: #-- using standard GRACE/GRACE-FO harmonics ds_str = '' #-- input GIA spherical harmonic datafiles GIA_Ylms_rate = read_GIA_model(GIA_FILE,GIA=GIA,LMAX=LMAX,MMAX=MMAX) gia_str = '_{0}'.format(GIA_Ylms_rate['title']) if GIA else '' #-- calculate the monthly mass change from GIA GIA_Ylms = GRACE_Ylms.zeros_like() GIA_Ylms.time[:] = np.copy(GRACE_Ylms.time) GIA_Ylms.month[:] = np.copy(GRACE_Ylms.month) #-- monthly GIA calculated by gia_rate*time elapsed #-- finding change in GIA each month for t in range(nfiles): GIA_Ylms.clm[:,:,t] = GIA_Ylms_rate['clm']*(GIA_Ylms.time[t]-2003.3) GIA_Ylms.slm[:,:,t] = GIA_Ylms_rate['slm']*(GIA_Ylms.time[t]-2003.3) #-- default file prefix if not FILE_PREFIX: fargs = (PROC,DREL,DSET,Ylms['title'],gia_str) FILE_PREFIX = '{0}_{1}_{2}{3}{4}_'.format(*fargs) #-- input spherical harmonic datafiles to be removed from the GRACE data #-- Remove sets of Ylms from the GRACE data before returning remove_Ylms = GRACE_Ylms.zeros_like() remove_Ylms.time[:] = np.copy(GRACE_Ylms.time) remove_Ylms.month[:] = np.copy(GRACE_Ylms.month) if REMOVE_FILES: #-- extend list if a single format was entered for all files if len(REMOVE_FORMAT) < len(REMOVE_FILES): REMOVE_FORMAT = REMOVE_FORMAT*len(REMOVE_FILES) #-- for each file to be removed for REMOVE_FILE,REMOVEFORM in zip(REMOVE_FILES,REMOVE_FORMAT): if REMOVEFORM in ('ascii','netCDF4','HDF5'): #-- ascii (.txt) #-- netCDF4 (.nc) #-- HDF5 (.H5) Ylms = harmonics().from_file(REMOVE_FILE, format=REMOVEFORM) elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'): #-- read from index file _,removeform = REMOVEFORM.split('-') #-- index containing files in data format Ylms = harmonics().from_index(REMOVE_FILE, format=removeform) #-- reduce to GRACE/GRACE-FO months and truncate to degree and order Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX,mmax=MMAX) #-- distribute removed Ylms uniformly over the ocean if REDISTRIBUTE_REMOVED: #-- calculate ratio between total removed mass and #-- a uniformly distributed cm of water over the ocean ratio = Ylms.clm[0,0,:]/ocean_Ylms.clm[0,0] #-- for each spherical harmonic for m in range(0,MMAX+1):#-- MMAX+1 to include MMAX for l in range(m,LMAX+1):#-- LMAX+1 to include LMAX #-- remove the ratio*ocean Ylms from Ylms #-- note: x -= y is equivalent to x = x - y Ylms.clm[l,m,:] -= ratio*ocean_Ylms.clm[l,m] Ylms.slm[l,m,:] -= ratio*ocean_Ylms.slm[l,m] #-- filter removed coefficients if DESTRIPE: Ylms = Ylms.destripe() #-- add data for month t and INDEX_FILE to the total #-- remove_clm and remove_slm matrices #-- redistributing the mass over the ocean if specified remove_Ylms.add(Ylms) #-- calculating GRACE/GRACE-FO error (Wahr et al. 2006) #-- output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) args = (PROC,DREL,DSET,LMAX,order_str,ds_str,atm_str,GRACE_Ylms.month[0], GRACE_Ylms.month[-1], suffix[DATAFORM]) delta_format = '{0}_{1}_{2}_DELTA_CLM_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}' DELTA_FILE = os.path.join(GRACE_Ylms.directory,delta_format.format(*args)) #-- check full path of the GRACE directory for delta file #-- if file was previously calculated: will read file #-- else: will calculate the GRACE/GRACE-FO error if not os.access(DELTA_FILE, os.F_OK): #-- add output delta file to list object output_files.append(DELTA_FILE) #-- Delta coefficients of GRACE time series (Error components) delta_Ylms = harmonics(lmax=LMAX,mmax=MMAX) delta_Ylms.clm = np.zeros((LMAX+1,MMAX+1)) delta_Ylms.slm = np.zeros((LMAX+1,MMAX+1)) #-- Smoothing Half-Width (CNES is a 10-day solution) #-- All other solutions are monthly solutions (HFWTH for annual = 6) if ((PROC == 'CNES') and (DREL in ('RL01','RL02'))): HFWTH = 19 else: HFWTH = 6 #-- Equal to the noise of the smoothed time-series #-- for each spherical harmonic order for m in range(0,MMAX+1):#-- MMAX+1 to include MMAX #-- for each spherical harmonic degree for l in range(m,LMAX+1):#-- LMAX+1 to include LMAX #-- Delta coefficients of GRACE time series for cs,csharm in enumerate(['clm','slm']): #-- calculate GRACE Error (Noise of smoothed time-series) #-- With Annual and Semi-Annual Terms val1 = getattr(GRACE_Ylms, csharm) smth = tssmooth(GRACE_Ylms.time, val1[l,m,:], HFWTH=HFWTH) #-- number of smoothed points nsmth = len(smth['data']) tsmth = np.mean(smth['time']) #-- GRACE delta Ylms #-- variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth) #-- save GRACE/GRACE-FO delta harmonics to file delta_Ylms.time = np.copy(tsmth) delta_Ylms.month = np.int64(nsmth) delta_Ylms.to_file(DELTA_FILE,format=DATAFORM) else: #-- read GRACE/GRACE-FO delta harmonics from file delta_Ylms = harmonics().from_file(DELTA_FILE,format=DATAFORM) #-- copy time and number of smoothed fields tsmth = np.squeeze(delta_Ylms.time) nsmth = np.int64(delta_Ylms.month) #-- Output spatial data object grid = spatial() grid.lon = np.copy(kfactor.lon) grid.lat = np.copy(kfactor.lat) grid.time = np.zeros((nfiles)) grid.month = np.zeros((nfiles),dtype=np.int64) grid.data = np.zeros((nlat,nlon,nfiles)) grid.mask = np.zeros((nlat,nlon,nfiles),dtype=bool) #-- Computing plms for converting to spatial domain phi = grid.lon[np.newaxis,:]*np.pi/180.0 theta = (90.0-grid.lat)*np.pi/180.0 PLM,dPLM = plm_holmes(LMAX,np.cos(theta)) #-- square of legendre polynomials truncated to order MMAX mm = np.arange(0,MMAX+1) PLM2 = PLM[:,mm,:]**2 #-- dfactor is the degree dependent coefficients #-- for converting to centimeters water equivalent (cmwe) dfactor = units(lmax=LMAX).harmonic(hl,kl,ll).cmwe #-- converting harmonics to truncated, smoothed coefficients in units #-- combining harmonics to calculate output spatial fields for i,gm in enumerate(GRACE_Ylms.month): #-- GRACE/GRACE-FO harmonics for time t Ylms = GRACE_Ylms.index(i) #-- Remove GIA rate for time Ylms.subtract(GIA_Ylms.index(i)) #-- Remove monthly files to be removed Ylms.subtract(remove_Ylms.index(i)) #-- smooth harmonics and convert to output units Ylms.convolve(dfactor*wt) #-- convert spherical harmonics to output spatial grid grid.data[:,:,i] = harmonic_summation(Ylms.clm, Ylms.slm, grid.lon, grid.lat, LMAX=LMAX, MMAX=MMAX, PLM=PLM).T #-- copy time variables for month grid.time[i] = np.copy(Ylms.time) grid.month[i] = np.copy(Ylms.month) #-- update spacing and dimensions grid.update_spacing() grid.update_extents() grid.update_dimensions() #-- scale output data with kfactor grid = grid.scale(kfactor.data) grid.replace_invalid(fill_value, mask=kfactor.mask) #-- output monthly files to ascii, netCDF4 or HDF5 args = (FILE_PREFIX,'',unit_str,LMAX,order_str,gw_str,ds_str, grid.month[0],grid.month[-1],suffix[DATAFORM]) FILE=os.path.join(OUTPUT_DIRECTORY,file_format.format(*args)) if (DATAFORM == 'ascii'): #-- ascii (.txt) grid.to_ascii(FILE, date=True, verbose=VERBOSE) elif (DATAFORM == 'netCDF4'): #-- netCDF4 grid.to_netCDF4(FILE, date=True, verbose=VERBOSE, units=unit_str, longname=unit_name, title='GRACE/GRACE-FO Spatial Data') elif (DATAFORM == 'HDF5'): #-- HDF5 grid.to_HDF5(FILE, date=True, verbose=VERBOSE, units=unit_str, longname=unit_name, title='GRACE/GRACE-FO Spatial Data') #-- set the permissions mode of the output files os.chmod(FILE, MODE) #-- add file to list output_files.append(FILE) #-- calculate power of scaled GRACE/GRACE-FO data scaled_power = grid.sum(power=2.0).power(0.5) #-- calculate residual leakage errors #-- scaled by ratio of GRACE and synthetic power ratio = scaled_power.scale(k_power.power(-1).data) error = k_error.scale(ratio.data) #-- output monthly error files to ascii, netCDF4 or HDF5 args = (FILE_PREFIX,'ERROR_',unit_str,LMAX,order_str,gw_str,ds_str, grid.month[0],grid.month[-1],suffix[DATAFORM]) FILE = os.path.join(OUTPUT_DIRECTORY,file_format.format(*args)) if (DATAFORM == 'ascii'): #-- ascii (.txt) error.to_ascii(FILE, date=False, verbose=VERBOSE) elif (DATAFORM == 'netCDF4'): #-- netCDF4 error.to_netCDF4(FILE, date=False, verbose=VERBOSE, units=unit_str, longname=unit_name, title='GRACE/GRACE-FO Scaling Error') elif (DATAFORM == 'HDF5'): #-- HDF5 error.to_HDF5(FILE, date=False, verbose=VERBOSE, units=unit_str, longname=unit_name, title='GRACE/GRACE-FO Scaling Error') #-- set the permissions mode of the output files os.chmod(FILE, MODE) #-- add file to list output_files.append(FILE) #-- Output spatial data object delta = spatial() delta.lon = np.copy(kfactor.lon) delta.lat = np.copy(kfactor.lat) delta.time = np.copy(tsmth) delta.month = np.copy(nsmth) delta.data = np.zeros((nlat,nlon)) delta.mask = np.zeros((nlat,nlon),dtype=bool) #-- calculate scaled spatial error #-- Calculating cos(m*phi)^2 and sin(m*phi)^2 m = delta_Ylms.m[:,np.newaxis] ccos = np.cos(np.dot(m,phi))**2 ssin = np.sin(np.dot(m,phi))**2 #-- truncate delta harmonics to spherical harmonic range Ylms = delta_Ylms.truncate(LMAX,lmin=LMIN,mmax=MMAX) #-- convolve delta harmonics with degree dependent factors #-- smooth harmonics and convert to output units Ylms = Ylms.convolve(dfactor*wt).power(2.0).scale(1.0/nsmth) #-- Calculate fourier coefficients d_cos = np.zeros((MMAX+1,nlat))#-- [m,th] d_sin = np.zeros((MMAX+1,nlat))#-- [m,th] #-- Calculating delta spatial values for k in range(0,nlat): #-- summation over all spherical harmonic degrees d_cos[:,k] = np.sum(PLM2[:,:,k]*Ylms.clm, axis=0) d_sin[:,k] = np.sum(PLM2[:,:,k]*Ylms.slm, axis=0) #-- Multiplying by c/s(phi#m) to get spatial error map delta.data[:] = np.sqrt(np.dot(ccos.T,d_cos) + np.dot(ssin.T,d_sin)).T #-- update spacing and dimensions delta.update_spacing() delta.update_extents() delta.update_dimensions() #-- scale output harmonic errors with kfactor delta = delta.scale(kfactor.data) delta.replace_invalid(fill_value, mask=kfactor.mask) #-- output monthly files to ascii, netCDF4 or HDF5 args = (FILE_PREFIX,'DELTA_',unit_str,LMAX,order_str,gw_str,ds_str, grid.month[0],grid.month[-1],suffix[DATAFORM]) FILE=os.path.join(OUTPUT_DIRECTORY,file_format.format(*args)) if (DATAFORM == 'ascii'): #-- ascii (.txt) delta.to_ascii(FILE, date=True, verbose=VERBOSE) elif (DATAFORM == 'netCDF4'): #-- netCDF4 delta.to_netCDF4(FILE, date=True, verbose=VERBOSE, units=unit_str, longname=unit_name, title='GRACE/GRACE-FO Spatial Error') elif (DATAFORM == 'HDF5'): #-- HDF5 delta.to_HDF5(FILE, date=True, verbose=VERBOSE, units=unit_str, longname=unit_name, title='GRACE/GRACE-FO Spatial Error') #-- set the permissions mode of the output files os.chmod(FILE, MODE) #-- add file to list output_files.append(FILE) #-- return the list of output files return output_files
def mascon_reconstruct(parameters, LOVE_NUMBERS=0, REFERENCE=None, MODE=0o775): #-- convert parameters into variables #-- Data processing center PROC = parameters['PROC'] #-- Data Release DREL = parameters['DREL'] #-- GRACE dataset DSET = parameters['DSET'] #-- Date Range START_MON = np.int(parameters['START']) END_MON = np.int(parameters['END']) #-- spherical harmonic parameters LMAX = np.int(parameters['LMAX']) #-- maximum spherical harmonic order if (parameters['MMAX'].title() == 'None'): MMAX = np.copy(LMAX) else: MMAX = np.int(parameters['MMAX']) #-- gaussian smoothing radius RAD = np.float(parameters['RAD']) #-- filtered coefficients for stripe effects DESTRIPE = parameters['DESTRIPE'] in ('Y', 'y') #-- ECMWF jump corrections ATM = parameters['ATM'] in ('Y', 'y') #-- Glacial Isostatic Adjustment file to read GIA = parameters['GIA'] if (parameters['GIA'].title() != 'None') else None GIA_FILE = os.path.expanduser(parameters['GIA_FILE']) #-- input/output data format (ascii, netCDF4, HDF5) DATAFORM = parameters['DATAFORM'] #-- index of mascons spherical harmonics #-- path.expanduser = tilde expansion of path MASCON_INDEX = os.path.expanduser(parameters['MASCON_INDEX']) #-- mascon distribution over the ocean MASCON_OCEAN = parameters['MASCON_OCEAN'] in ('Y', 'y') #-- for datasets not GSM: will add a label for the dataset dset_str = '' if (DSET == 'GSM') else '_{0}'.format(DSET) #-- atmospheric ECMWF "jump" flag (if ATM) atm_str = '_wATM' if ATM else '' #-- Gaussian smoothing string for radius RAD gw_str = '_r{0:0.0f}km'.format(RAD) if (RAD != 0) else '' #-- input GIA spherical harmonic datafiles GIA_Ylms_rate = read_GIA_model(GIA_FILE, GIA=GIA, LMAX=LMAX, MMAX=MMAX) gia_str = '_{0}'.format(GIA_Ylms_rate['title']) if GIA else '' #-- output string for both LMAX==MMAX and LMAX != MMAX cases order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else '' #-- filter grace coefficients flag ds_str = '_FL' if DESTRIPE else '' #-- output filename suffix suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') #-- GRACE output filename prefix #-- mascon directory for GRACE product, processing and date range DIRECTORY = os.path.expanduser(parameters['DIRECTORY']) #-- create initial reconstruct index for calc_mascon.py fid = open(os.path.expanduser(parameters['RECONSTRUCT_INDEX']), 'w') HOME = os.path.expanduser('~') #-- output file format file_format = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}_{9:03d}-{10:03d}.{11}' #-- read load love numbers hl, kl, ll = load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE) #-- Earth Parameters factors = units(lmax=LMAX).harmonic(hl, kl, ll) #-- Average Density of the Earth [g/cm^3] rho_e = factors.rho_e #-- Average Radius of the Earth [cm] rad_e = factors.rad_e #-- Read Ocean function and convert to Ylms for redistribution if MASCON_OCEAN: #-- read Land-Sea Mask and convert to spherical harmonics LANDMASK = os.path.expanduser(parameters['LANDMASK']) ocean_Ylms = ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll)) ocean_str = '_OCN' else: #-- not distributing uniformly over ocean ocean_str = '' #-- input mascon spherical harmonic datafiles with open(MASCON_INDEX, 'r') as f: mascon_files = f.read().splitlines() for k, fi in enumerate(mascon_files): #-- read mascon spherical harmonics if (DATAFORM == 'ascii'): #-- ascii (.txt) Ylms = harmonics().from_ascii(os.path.expanduser(fi), date=False) elif (DATAFORM == 'netCDF4'): #-- netcdf (.nc) Ylms = harmonics().from_netCDF4(os.path.expanduser(fi), date=False) elif (DATAFORM == 'HDF5'): #-- HDF5 (.H5) Ylms = harmonics().from_HDF5(os.path.expanduser(fi), date=False) #-- Calculating the total mass of each mascon (1 cmwe uniform) total_area = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0 #-- distribute MASCON mass uniformly over the ocean if MASCON_OCEAN: #-- calculate ratio between total mascon mass and #-- a uniformly distributed cm of water over the ocean ratio = Ylms.clm[0, 0] / ocean_Ylms['clm'][0, 0] #-- for each spherical harmonic for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX for l in range(m, LMAX + 1): #-- LMAX+1 to include LMAX #-- remove ratio*ocean Ylms from mascon Ylms #-- note: x -= y is equivalent to x = x - y Ylms.clm[l, m] -= ratio * ocean_Ylms['clm'][l, m] Ylms.slm[l, m] -= ratio * ocean_Ylms['slm'][l, m] #-- truncate mascon spherical harmonics to d/o LMAX/MMAX Ylms = Ylms.truncate(lmax=LMAX, mmax=MMAX) #-- mascon base is the file without directory or suffix mascon_base = os.path.basename(fi) mascon_base = os.path.splitext(mascon_base)[0] #-- if lower case, will capitalize mascon_base = mascon_base.upper() #-- if mascon name contains degree and order info, remove mascon_name = mascon_base.replace('_L{0:d}'.format(LMAX), '') #-- input filename format (for both LMAX==MMAX and LMAX != MMAX cases): #-- mascon name, GRACE dataset, GIA model, LMAX, (MMAX,) #-- Gaussian smoothing, filter flag, remove reconstructed fields flag #-- output GRACE error file args = (mascon_name, dset_str, gia_str.upper(), atm_str, ocean_str, LMAX, order_str, gw_str, ds_str) file_input = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}.txt'.format(*args) mascon_data_input = np.loadtxt(os.path.join(DIRECTORY, file_input)) nmon = np.shape(mascon_data_input)[0] #-- convert mascon time-series from Gt to cmwe mascon_sigma = 1e15 * mascon_data_input[:, 2] / total_area #-- mascon time-series Ylms mascon_Ylms = Ylms.scale(mascon_sigma) mascon_Ylms.time = mascon_data_input[:, 1].copy() mascon_Ylms.month = mascon_data_input[:, 0].astype(np.int) #-- output to file: no ascii option args = (mascon_name, dset_str, gia_str.upper(), atm_str, ocean_str, LMAX, order_str, gw_str, ds_str, START_MON, END_MON, suffix[DATAFORM]) FILE = file_format.format(*args) #-- output harmonics to file if (DATAFORM == 'netCDF4'): #-- netcdf (.nc) mascon_Ylms.to_netCDF4(os.path.join(DIRECTORY, FILE)) elif (DATAFORM == 'HDF5'): #-- HDF5 (.H5) mascon_Ylms.to_HDF5(os.path.join(DIRECTORY, FILE)) #-- print file name to index print(os.path.join(DIRECTORY, FILE).replace(HOME, '~'), file=fid) #-- change the permissions mode os.chmod(os.path.join(DIRECTORY, FILE), MODE) #-- close the reconstruct index fid.close() #-- change the permissions mode of the index file os.chmod(os.path.expanduser(parameters['RECONSTRUCT_INDEX']), MODE)
def grace_spatial_error(base_dir, parameters, LOVE_NUMBERS=0, REFERENCE=None, VERBOSE=False, MODE=0o775): #-- Data processing center PROC = parameters['PROC'] #-- Data Release DREL = parameters['DREL'] #-- GRACE dataset DSET = parameters['DSET'] #-- Date Range and missing months start_mon = np.int(parameters['START']) end_mon = np.int(parameters['END']) missing = np.array(parameters['MISSING'].split(','), dtype=np.int) #-- minimum degree LMIN = np.int(parameters['LMIN']) #-- maximum degree and order LMAX = np.int(parameters['LMAX']) if (parameters['MMAX'].title() == 'None'): MMAX = np.copy(LMAX) else: MMAX = np.int(parameters['MMAX']) #-- SLR C2,0 and C3,0 SLR_C20 = parameters['SLR_C20'] SLR_C30 = parameters['SLR_C30'] #-- Degree 1 correction DEG1 = parameters['DEG1'] MODEL_DEG1 = parameters['MODEL_DEG1'] in ('Y', 'y') #-- ECMWF jump corrections ATM = parameters['ATM'] in ('Y', 'y') #-- Pole Tide correction from Wahr et al. (2015) POLE_TIDE = parameters['POLE_TIDE'] in ('Y', 'y') #-- smoothing radius RAD = np.int(parameters['RAD']) #-- destriped coefficients DESTRIPE = parameters['DESTRIPE'] in ('Y', 'y') #-- output spatial units UNITS = np.int(parameters['UNITS']) #-- output degree spacing #-- can enter dlon and dlat as [dlon,dlat] or a single value DDEG = np.squeeze(np.array(parameters['DDEG'].split(','), dtype='f')) #-- output degree interval (0:360, 90:-90) or (degree spacing/2) INTERVAL = np.int(parameters['INTERVAL']) #-- output data format (ascii, netCDF4, HDF5) DATAFORM = parameters['DATAFORM'] #-- output directory and base filename DIRECTORY = os.path.expanduser(parameters['DIRECTORY']) FILENAME = parameters['FILENAME'] #-- recursively create output directory if not currently existing if (not os.access(DIRECTORY, os.F_OK)): os.makedirs(DIRECTORY, mode=MODE, exist_ok=True) #-- list object of output files for file logs (full path) output_files = [] #-- file information suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') #-- read arrays of kl, hl, and ll Love Numbers hl, kl, ll = load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE) #-- Calculating the Gaussian smoothing for radius RAD if (RAD != 0): wt = 2.0 * np.pi * gauss_weights(RAD, LMAX) gw_str = '_r{0:0.0f}km'.format(RAD) else: #-- else = 1 wt = np.ones((LMAX + 1)) gw_str = '' #-- flag for spherical harmonic order order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else '' #-- atmospheric ECMWF "jump" flag (if ATM) atm_str = '_wATM' if ATM else '' #-- reading GRACE months for input range with grace_input_months.py #-- replacing SLR and Degree 1 if specified #-- correcting for Pole-Tide and Atmospheric Jumps if specified Ylms = grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, missing, SLR_C20, DEG1, MMAX=MMAX, SLR_C30=SLR_C30, MODEL_DEG1=MODEL_DEG1, ATM=ATM, POLE_TIDE=POLE_TIDE) #-- convert to harmonics object and remove mean if specified GRACE_Ylms = harmonics().from_dict(Ylms) grace_dir = Ylms['directory'] #-- mean parameters MEAN = parameters['MEAN'] in ('Y', 'y') #-- use a mean file for the static field to remove if (parameters['MEAN_FILE'].title() == 'None'): mean_Ylms = GRACE_Ylms.mean(apply=MEAN) else: #-- read data form for input mean file (ascii, netCDF4, HDF5, gfc) if (parameters['MEANFORM'] == 'ascii'): mean_Ylms = harmonics().from_ascii(parameters['MEAN_FILE'], date=False) elif (parameters['MEANFORM'] == 'netCDF4'): mean_Ylms = harmonics().from_netCDF4(parameters['MEAN_FILE'], date=False) elif (parameters['MEANFORM'] == 'HDF5'): mean_Ylms = harmonics().from_HDF5(parameters['MEAN_FILE'], date=False) elif (parameters['MEANFORM'] == 'gfc'): mean_Ylms = harmonics().from_gfc(parameters['MEAN_FILE']) #-- remove the input mean if MEAN: GRACE_Ylms.subtract(mean_Ylms) #-- date information of GRACE/GRACE-FO coefficients nfiles = len(GRACE_Ylms.time) #-- filter GRACE/GRACE-FO coefficients if DESTRIPE: #-- destriping GRACE/GRACE-FO coefficients ds_str = '_FL' GRACE_Ylms = GRACE_Ylms.destripe() else: #-- using standard GRACE/GRACE-FO harmonics ds_str = '' #-- calculating GRACE error (Wahr et al 2006) #-- output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) args = (PROC, DREL, DSET, LMAX, order_str, ds_str, atm_str, GRACE_Ylms.month[0], GRACE_Ylms.month[-1], suffix[DATAFORM]) delta_format = '{0}_{1}_{2}_DELTA_CLM_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}' DELTA_FILE = delta_format.format(*args) #-- full path of the GRACE directory #-- if file was previously calculated, will read file #-- else will calculate the GRACE error if (not os.access(os.path.join(grace_dir, DELTA_FILE), os.F_OK)): #-- add output delta file to list object output_files.append(os.path.join(grace_dir, DELTA_FILE)) #-- Delta coefficients of GRACE time series (Error components) delta_Ylms = harmonics(lmax=LMAX, mmax=MMAX) delta_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) delta_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) #-- Smoothing Half-Width (CNES is a 10-day solution) #-- 365/10/2 = 18.25 (next highest is 19) #-- All other solutions are monthly solutions (HFWTH for annual = 6) if ((PROC == 'CNES') and (DREL in ('RL01', 'RL02'))): HFWTH = 19 else: HFWTH = 6 #-- Equal to the noise of the smoothed time-series #-- for each spherical harmonic order for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX #-- for each spherical harmonic degree for l in range(m, LMAX + 1): #-- LMAX+1 to include LMAX #-- Delta coefficients of GRACE time series for cs, csharm in enumerate(['clm', 'slm']): #-- Constrained GRACE Error (Noise of smoothed time-series) #-- With Annual and Semi-Annual Terms val1 = getattr(GRACE_Ylms, csharm) smth = tssmooth(GRACE_Ylms.time, val1[l, m, :], HFWTH=HFWTH) #-- number of smoothed points nsmth = len(smth['data']) #-- GRACE delta Ylms #-- variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) val2[l, m] = np.sqrt(np.sum(smth['noise']**2) / nsmth) #-- save GRACE DELTA to file delta_Ylms.time = np.copy(nsmth) delta_Ylms.month = np.copy(nsmth) if (DATAFORM == 'ascii'): #-- ascii (.txt) delta_Ylms.to_ascii(os.path.join(grace_dir, DELTA_FILE)) elif (DATAFORM == 'netCDF4'): #-- netcdf (.nc) delta_Ylms.to_netCDF4(os.path.join(grace_dir, DELTA_FILE)) elif (DATAFORM == 'HDF5'): #-- HDF5 (.H5) delta_Ylms.to_HDF5(os.path.join(grace_dir, DELTA_FILE)) #-- set the permissions mode of the output harmonics file os.chmod(os.path.join(grace_dir, DELTA_FILE), MODE) #-- append delta harmonics file to output files list output_files.append(os.path.join(grace_dir, DELTA_FILE)) else: #-- read GRACE DELTA spherical harmonics datafile if (DATAFORM == 'ascii'): #-- ascii (.txt) delta_Ylms = harmonics().from_ascii( os.path.join(grace_dir, DELTA_FILE)) elif (DATAFORM == 'netCDF4'): #-- netcdf (.nc) delta_Ylms = harmonics().from_netCDF4( os.path.join(grace_dir, DELTA_FILE)) elif (DATAFORM == 'HDF5'): #-- HDF5 (.H5) delta_Ylms = harmonics().from_HDF5( os.path.join(grace_dir, DELTA_FILE)) #-- truncate grace delta clm and slm to d/o LMAX/MMAX delta_Ylms = delta_Ylms.truncate(lmax=LMAX, mmax=MMAX) nsmth = np.int(delta_Ylms.time) #-- Output spatial data object delta = spatial() #-- Output Degree Spacing dlon, dlat = (DDEG, DDEG) if (np.ndim(DDEG) == 0) else (DDEG[0], DDEG[1]) #-- Output Degree Interval if (INTERVAL == 1): #-- (-180:180,90:-90) nlon = np.int((360.0 / dlon) + 1.0) nlat = np.int((180.0 / dlat) + 1.0) delta.lon = -180 + dlon * np.arange(0, nlon) delta.lat = 90.0 - dlat * np.arange(0, nlat) elif (INTERVAL == 2): #-- (Degree spacing)/2 delta.lon = np.arange(-180 + dlon / 2.0, 180 + dlon / 2.0, dlon) delta.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat) nlon = len(delta.lon) nlat = len(delta.lat) #-- Earth Parameters factors = units(lmax=LMAX).harmonic(hl, kl, ll) #-- output spatial units unit_list = ['cmwe', 'mmGH', 'mmCU', u'\u03BCGal', 'mbar'] unit_name = [ 'Equivalent Water Thickness', 'Geoid Height', 'Elastic Crustal Uplift', 'Gravitational Undulation', 'Equivalent Surface Pressure' ] #-- dfactor is the degree dependent coefficients #-- for specific spherical harmonic output units if (UNITS == 1): #-- 1: cmwe, centimeters water equivalent dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).cmwe elif (UNITS == 2): #-- 2: mmGH, millimeters geoid height dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmGH elif (UNITS == 3): #-- 3: mmCU, millimeters elastic crustal deformation dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mmCU elif (UNITS == 4): #-- 4: micGal, microGal gravity perturbations dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).microGal elif (UNITS == 5): #-- 5: mbar, millibar equivalent surface pressure dfactor = units(lmax=LMAX).harmonic(hl, kl, ll).mbar #-- Computing plms for converting to spatial domain phi = delta.lon[np.newaxis, :] * np.pi / 180.0 theta = (90.0 - delta.lat) * np.pi / 180.0 PLM, dPLM = plm_holmes(LMAX, np.cos(theta)) #-- square of legendre polynomials truncated to order MMAX mm = np.arange(0, MMAX + 1) PLM2 = PLM[:, mm, :]**2 #-- Calculating cos(m*phi)^2 and sin(m*phi)^2 m = delta_Ylms.m[:, np.newaxis] ccos = np.cos(np.dot(m, phi))**2 ssin = np.sin(np.dot(m, phi))**2 #-- truncate delta harmonics to spherical harmonic range Ylms = delta_Ylms.truncate(LMAX, lmin=LMIN, mmax=MMAX) #-- convolve delta harmonics with degree dependent factors #-- smooth harmonics and convert to output units Ylms = Ylms.convolve(dfactor * wt).power(2.0).scale(1.0 / nsmth) #-- Calculate fourier coefficients d_cos = np.zeros((MMAX + 1, nlat)) #-- [m,th] d_sin = np.zeros((MMAX + 1, nlat)) #-- [m,th] #-- Calculating delta spatial values for k in range(0, nlat): #-- summation over all spherical harmonic degrees d_cos[:, k] = np.sum(PLM2[:, :, k] * Ylms.clm, axis=0) d_sin[:, k] = np.sum(PLM2[:, :, k] * Ylms.slm, axis=0) #-- Multiplying by c/s(phi#m) to get spatial maps (lon,lat) delta.data = np.sqrt(np.dot(ccos.T, d_cos) + np.dot(ssin.T, d_sin)).T #-- output file format file_format = '{0}{1}_L{2:d}{3}{4}{5}_ERR_{6:03d}-{7:03d}.{8}' #-- output error file to ascii, netCDF4 or HDF5 args = (FILENAME, unit_list[UNITS - 1], LMAX, order_str, gw_str, ds_str, GRACE_Ylms.month[0], GRACE_Ylms.month[-1], suffix[DATAFORM]) FILE = os.path.join(DIRECTORY, file_format.format(*args)) if (DATAFORM == 'ascii'): #-- ascii (.txt) delta.to_ascii(FILE, date=False, verbose=VERBOSE) elif (DATAFORM == 'netCDF4'): #-- netCDF4 delta.to_netCDF4(FILE, date=False, verbose=VERBOSE, units=unit_list[UNITS - 1], longname=unit_name[UNITS - 1], title='GRACE/GRACE-FO Spatial Error') elif (DATAFORM == 'HDF5'): #-- HDF5 delta.to_HDF5(FILE, date=False, verbose=VERBOSE, units=unit_list[UNITS - 1], longname=unit_name[UNITS - 1], title='GRACE/GRACE-FO Spatial Error') #-- set the permissions mode of the output files os.chmod(FILE, MODE) #-- add file to list output_files.append(FILE) #-- return the list of output files return output_files
def calc_mascon(base_dir, parameters, LOVE_NUMBERS=0, REFERENCE=None, MODE=0o775): #-- convert parameters to variables #-- Data processing center PROC = parameters['PROC'] #-- Data Release DREL = parameters['DREL'] #-- GRACE dataset DSET = parameters['DSET'] #-- Date Range and missing months start_mon = np.int(parameters['START']) end_mon = np.int(parameters['END']) missing = np.array(parameters['MISSING'].split(','), dtype=np.int) #-- spherical harmonic degree range LMIN = np.int(parameters['LMIN']) LMAX = np.int(parameters['LMAX']) #-- maximum spherical harmonic order if (parameters['MMAX'].title() == 'None'): MMAX = np.copy(LMAX) else: MMAX = np.int(parameters['MMAX']) #-- degree 1 coefficients #-- None: No degree 1 #-- Tellus: GRACE/GRACE-FO TN-13 from PO.DAAC #-- https://grace.jpl.nasa.gov/data/get-data/geocenter/ #-- SLR: satellite laser ranging from CSR #-- ftp://ftp.csr.utexas.edu/pub/slr/geocenter/ #-- SLF: Sutterley and Velicogna, Remote Sensing (2019) #-- https://www.mdpi.com/2072-4292/11/18/2108 DEG1 = parameters['DEG1'] #-- replace C20, C30 with coefficients from SLR SLR_C20 = parameters['SLR_C20'] SLR_C30 = parameters['SLR_C30'] #-- ECMWF jump corrections ATM = parameters['ATM'] in ('Y', 'y') #-- Pole-Tide from Wahr et al. (2015) POLE_TIDE = parameters['POLE_TIDE'] in ('Y', 'y') #-- Glacial Isostatic Adjustment file to read GIA = parameters['GIA'] if (parameters['GIA'].title() != 'None') else None GIA_FILE = os.path.expanduser(parameters['GIA_FILE']) #-- remove a set of spherical harmonics from the GRACE data REMOVE_INDEX = parameters['REMOVE_INDEX'] REDISTRIBUTE_REMOVED = parameters['REDISTRIBUTE_REMOVED'] in ('Y', 'y') #-- remove reconstructed fields RECONSTRUCT = parameters['RECONSTRUCT'] in ('Y', 'y') #-- gaussian smoothing radius RAD = np.float(parameters['RAD']) #-- filter coefficients for stripe effects DESTRIPE = parameters['DESTRIPE'] in ('Y', 'y') #-- input/output data format (ascii, netCDF4, HDF5) DATAFORM = parameters['DATAFORM'] #-- index of mascons spherical harmonics #-- path.expanduser = tilde expansion of path MASCON_INDEX = os.path.expanduser(parameters['MASCON_INDEX']) #-- output directory for mascon time series files DIRECTORY = os.path.expanduser(parameters['DIRECTORY']) #-- 1: fit mass, 2: fit geoid FIT_METHOD = np.int(parameters['FIT_METHOD']) #-- mascon redistribution MASCON_OCEAN = parameters['MASCON_OCEAN'] in ('Y', 'y') #-- recursively create output Directory if not currently existing if (not os.access(DIRECTORY, os.F_OK)): os.makedirs(DIRECTORY, mode=MODE, exist_ok=True) #-- list object of output files for file logs (full path) output_files = [] #-- file information suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') #-- read arrays of kl, hl, and ll Love Numbers hl, kl, ll = load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE) #-- Earth Parameters factors = units(lmax=LMAX).harmonic(hl, kl, ll) #-- Average Density of the Earth [g/cm^3] rho_e = factors.rho_e #-- Average Radius of the Earth [cm] rad_e = factors.rad_e #-- for datasets not GSM: will add a label for the dataset dset_str = '' if (DSET == 'GSM') else '_{0}'.format(DSET) #-- atmospheric ECMWF "jump" flag (if ATM) atm_str = '_wATM' if ATM else '' #-- output string for both LMAX==MMAX and LMAX != MMAX cases order_str = 'M{0:d}'.format(MMAX) if (MMAX != LMAX) else '' #-- Calculating the Gaussian smoothing for radius RAD if (RAD != 0): wt = 2.0 * np.pi * gauss_weights(RAD, LMAX) gw_str = '_r{0:0.0f}km'.format(RAD) else: #-- else = 1 wt = np.ones((LMAX + 1)) gw_str = '' #-- Read Ocean function and convert to Ylms for redistribution if (MASCON_OCEAN | REDISTRIBUTE_REMOVED): #-- read Land-Sea Mask and convert to spherical harmonics LSMASK = os.path.expanduser(parameters['LANDMASK']) ocean_Ylms = ocean_stokes(LSMASK, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll)) ocean_str = '_OCN' else: #-- not distributing uniformly over ocean ocean_str = '' #-- input GRACE/GRACE-FO spherical harmonic datafiles #-- reading GRACE months for input range with grace_input_months.py #-- replacing SLR and Degree 1 if specified #-- correcting for Pole-Tide and Atmospheric Jumps if specified Ylms = grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, missing, SLR_C20, DEG1, MMAX=MMAX, SLR_C30=SLR_C30, MODEL_DEG1=True, ATM=ATM, POLE_TIDE=POLE_TIDE) #-- full path to directory for specific GRACE/GRACE-FO product grace_dir = Ylms['directory'] #-- create harmonics object from GRACE/GRACE-FO data GRACE_Ylms = harmonics().from_dict(Ylms) #-- use a mean file for the static field to remove if (parameters['MEAN_FILE'].title() == 'None'): GRACE_Ylms.mean(apply=True) else: #-- read data form for input mean file (ascii, netCDF4, HDF5, gfc) if (parameters['MEANFORM'] == 'ascii'): mean_Ylms = harmonics().from_ascii(parameters['MEAN_FILE'], date=False) elif (parameters['MEANFORM'] == 'netCDF4'): mean_Ylms = harmonics().from_netCDF4(parameters['MEAN_FILE'], date=False) elif (parameters['MEANFORM'] == 'HDF5'): mean_Ylms = harmonics().from_HDF5(parameters['MEAN_FILE'], date=False) elif (parameters['MEANFORM'] == 'gfc'): mean_Ylms = harmonics().from_gfc(parameters['MEAN_FILE']) #-- remove the input mean GRACE_Ylms.subtract(mean_Ylms) #-- date information of GRACE/GRACE-FO coefficients n_files = len(GRACE_Ylms.time) #-- filter GRACE/GRACE-FO coefficients if DESTRIPE: #-- destriping GRACE/GRACE-FO coefficients ds_str = '_FL' GRACE_Ylms = GRACE_Ylms.destripe() else: #-- using standard GRACE/GRACE-FO harmonics ds_str = '' #-- input GIA spherical harmonic datafiles GIA_Ylms_rate = read_GIA_model(GIA_FILE, GIA=GIA, LMAX=LMAX, MMAX=MMAX) gia_str = '_{0}'.format(GIA_Ylms_rate['title']) if GIA else '' #-- calculate the monthly mass change from GIA GIA_Ylms = GRACE_Ylms.zeros_like() GIA_Ylms.time[:] = np.copy(GRACE_Ylms.time) GIA_Ylms.month[:] = np.copy(GRACE_Ylms.month) #-- monthly GIA calculated by gia_rate*time elapsed #-- finding change in GIA each month for t in range(n_files): GIA_Ylms.clm[:, :, t] = GIA_Ylms_rate['clm'] * (GIA_Ylms.time[t] - 2003.3) GIA_Ylms.slm[:, :, t] = GIA_Ylms_rate['slm'] * (GIA_Ylms.time[t] - 2003.3) #-- input spherical harmonic datafiles to be removed from the GRACE data #-- Remove sets of Ylms from the GRACE data before returning remove_Ylms = GRACE_Ylms.zeros_like() remove_Ylms.time[:] = np.copy(GRACE_Ylms.time) remove_Ylms.month[:] = np.copy(GRACE_Ylms.month) if (parameters['REMOVE_INDEX'].title() != 'None'): #-- for each file index separated by commas for REMOVE_INDEX in parameters['REMOVE_INDEX'].split(','): Ylms = harmonics().from_index(REMOVE_INDEX, DATAFORM) #-- reduce to GRACE/GRACE-FO months and truncate to degree and order Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX, mmax=MMAX) #-- distribute removed Ylms uniformly over the ocean if REDISTRIBUTE_REMOVED: #-- calculate ratio between total removed mass and #-- a uniformly distributed cm of water over the ocean ratio = Ylms.clm[0, 0, :] / ocean_Ylms['clm'][0, 0] #-- for each spherical harmonic for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX for l in range(m, LMAX + 1): #-- LMAX+1 to include LMAX #-- remove the ratio*ocean Ylms from Ylms #-- note: x -= y is equivalent to x = x - y Ylms.clm[l, m, :] -= ratio * ocean_Ylms['clm'][l, m] Ylms.slm[l, m, :] -= ratio * ocean_Ylms['slm'][l, m] #-- filter removed coefficients if DESTRIPE: Ylms = Ylms.destripe() #-- add data for month t and INDEX_FILE to the total #-- remove_clm and remove_slm matrices #-- redistributing the mass over the ocean if specified remove_Ylms.add(Ylms) #-- input reconstructed spherical harmonic datafiles construct_Ylms = GRACE_Ylms.zeros_like() construct_Ylms.time[:] = np.copy(GRACE_Ylms.time) construct_Ylms.month[:] = np.copy(GRACE_Ylms.month) if RECONSTRUCT: #-- input index for reconstructed spherical harmonic datafiles RECONSTRUCT_INDEX = os.path.expanduser(parameters['RECONSTRUCT_INDEX']) with open(RECONSTRUCT_INDEX, 'r') as f: reconstruct_files = f.read().splitlines() #-- remove commented lines (can comment out files in the index) reconstruct_files = [ f for f in reconstruct_files if not re.match('#', f) ] #-- for each valid file in the index (iterate over mascons) for construct_file in reconstruct_files: #-- read reconstructed spherical harmonics if (DATAFORM == 'ascii'): #-- ascii (.txt) Ylms = harmonics().from_ascii(construct_file) elif (DATAFORM == 'netCDF4'): #-- netcdf (.nc) Ylms = harmonics().from_netCDF4(construct_file) elif (DATAFORM == 'HDF5'): #-- HDF5 (.H5) Ylms = harmonics().from_HDF5(construct_file) #-- truncate clm and slm matrices to LMAX/MMAX #-- add harmonics object to total construct_Ylms.add(Ylms.truncate(lmax=LMAX, mmax=MMAX)) #-- filter reconstructed coefficients if DESTRIPE: construct_Ylms = construct_Ylms.destripe() #-- set flag for removing reconstructed coefficients construct_str = '_LEAKAGE' else: #-- set flag for not removing the reconstructed coefficients construct_str = '' #-- input mascon spherical harmonic datafiles with open(MASCON_INDEX, 'r') as f: mascon_files = f.read().splitlines() #-- number of mascons n_mas = len(mascon_files) #-- spatial area of the mascon total_area = np.zeros((n_mas)) #-- name of each mascon mascon_name = [] #-- for each valid file in the index (iterate over mascons) mascon_list = [] for k, fi in enumerate(mascon_files): #-- read mascon spherical harmonics if (DATAFORM == 'ascii'): #-- ascii (.txt) Ylms = harmonics().from_ascii(os.path.expanduser(fi), date=False) elif (DATAFORM == 'netCDF4'): #-- netcdf (.nc) Ylms = harmonics().from_netCDF4(os.path.expanduser(fi), date=False) elif (DATAFORM == 'HDF5'): #-- HDF5 (.H5) Ylms = harmonics().from_HDF5(os.path.expanduser(fi), date=False) #-- Calculating the total mass of each mascon (1 cmwe uniform) total_area[k] = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0 #-- distribute MASCON mass uniformly over the ocean if MASCON_OCEAN: #-- calculate ratio between total mascon mass and #-- a uniformly distributed cm of water over the ocean ratio = Ylms.clm[0, 0] / ocean_Ylms['clm'][0, 0] #-- for each spherical harmonic for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX for l in range(m, LMAX + 1): #-- LMAX+1 to include LMAX #-- remove ratio*ocean Ylms from mascon Ylms #-- note: x -= y is equivalent to x = x - y Ylms.clm[l, m] -= ratio * ocean_Ylms['clm'][l, m] Ylms.slm[l, m] -= ratio * ocean_Ylms['slm'][l, m] #-- truncate mascon spherical harmonics to d/o LMAX/MMAX and add to list mascon_list.append(Ylms.truncate(lmax=LMAX, mmax=MMAX)) #-- mascon base is the file without directory or suffix mascon_base = os.path.basename(mascon_files[k]) mascon_base = os.path.splitext(mascon_base)[0] #-- if lower case, will capitalize mascon_base = mascon_base.upper() #-- if mascon name contains degree and order info, remove mascon_name.append(mascon_base.replace('_L{0:d}'.format(LMAX), '')) #-- create single harmonics object from list mascon_Ylms = harmonics().from_list(mascon_list, date=False) #-- calculating GRACE/GRACE-FO error (Wahr et al. 2006) #-- output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) args = (PROC, DREL, DSET, LMAX, order_str, ds_str, atm_str, GRACE_Ylms.month[0], GRACE_Ylms.month[-1], suffix[DATAFORM]) delta_format = '{0}_{1}_{2}_DELTA_CLM_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}' DELTA_FILE = delta_format.format(*args) #-- check full path of the GRACE directory for delta file #-- if file was previously calculated: will read file #-- else: will calculate the GRACE/GRACE-FO error if (not os.access(os.path.join(grace_dir, DELTA_FILE), os.F_OK)): #-- add output delta file to list object output_files.append(os.path.join(grace_dir, DELTA_FILE)) #-- Delta coefficients of GRACE time series (Error components) delta_Ylms = harmonics(lmax=LMAX, mmax=MMAX) delta_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) delta_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) #-- Smoothing Half-Width (CNES is a 10-day solution) #-- All other solutions are monthly solutions (HFWTH for annual = 6) if ((PROC == 'CNES') and (DREL in ('RL01', 'RL02'))): HFWTH = 19 else: HFWTH = 6 #-- Equal to the noise of the smoothed time-series #-- for each spherical harmonic order for m in range(0, MMAX + 1): #-- MMAX+1 to include MMAX #-- for each spherical harmonic degree for l in range(m, LMAX + 1): #-- LMAX+1 to include LMAX #-- Delta coefficients of GRACE time series for cs, csharm in enumerate(['clm', 'slm']): #-- calculate GRACE Error (Noise of smoothed time-series) #-- With Annual and Semi-Annual Terms val1 = getattr(GRACE_Ylms, csharm) smth = tssmooth(GRACE_Ylms.time, val1[l, m, :], HFWTH=HFWTH) #-- number of smoothed points nsmth = len(smth['data']) tsmth = np.mean(smth['time']) #-- GRACE delta Ylms #-- variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) val2[l, m] = np.sqrt(np.sum(smth['noise']**2) / nsmth) #-- save GRACE/GRACE-FO delta harmonics to file delta_Ylms.time = np.copy(tsmth) delta_Ylms.month = np.int(nsmth) if (DATAFORM == 'ascii'): #-- ascii (.txt) delta_Ylms.to_ascii(os.path.join(grace_dir, DELTA_FILE)) elif (DATAFORM == 'netCDF4'): #-- netcdf (.nc) delta_Ylms.to_netCDF4(os.path.join(grace_dir, DELTA_FILE)) elif (DATAFORM == 'HDF5'): #-- HDF5 (.H5) delta_Ylms.to_HDF5(os.path.join(grace_dir, DELTA_FILE)) else: #-- read GRACE/GRACE-FO delta harmonics from file if (DATAFORM == 'ascii'): #-- ascii (.txt) delta_Ylms = harmonics().from_ascii( os.path.join(grace_dir, DELTA_FILE)) elif (DATAFORM == 'netCDF4'): #-- netcdf (.nc) delta_Ylms = harmonics().from_netCDF4( os.path.join(grace_dir, DELTA_FILE)) elif (DATAFORM == 'HDF5'): #-- HDF5 (.H5) delta_Ylms = harmonics().from_HDF5( os.path.join(grace_dir, DELTA_FILE)) #-- truncate GRACE/GRACE-FO delta clm and slm to d/o LMAX/MMAX delta_Ylms = delta_Ylms.truncate(lmax=LMAX, mmax=MMAX) tsmth = np.squeeze(delta_Ylms.time) nsmth = np.int(delta_Ylms.month) #-- Calculating the number of cos and sin harmonics between LMIN and LMAX #-- taking into account MMAX (if MMAX == LMAX then LMAX-MMAX=0) n_harm = np.int(LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX)**2 - (LMAX - MMAX)) #-- Initialing harmonics for least squares fitting #-- mascon kernel M_lm = np.zeros((n_harm, n_mas)) #-- mascon kernel converted to output unit MA_lm = np.zeros((n_harm, n_mas)) #-- corrected clm and slm Y_lm = np.zeros((n_harm, n_files)) #-- sensitivity kernel A_lm = np.zeros((n_harm, n_mas)) #-- Satellite error harmonics delta_lm = np.zeros((n_harm)) #-- Initializing output Mascon time-series mascon = np.zeros((n_mas, n_files)) #-- Mascon satellite error component M_delta = np.zeros((n_mas)) #-- Initializing conversion factors #-- factor for converting to coefficients of mass fact = np.zeros((n_harm)) #-- smoothing factor wt_lm = np.zeros((n_harm)) #-- ii is a counter variable for building the mascon column array ii = 0 #-- Creating column array of clm/slm coefficients #-- Order is [C00...C6060,S11...S6060] #-- Calculating factor to convert geoid spherical harmonic coefficients #-- to coefficients of mass (Wahr, 1998) coeff = rho_e * rad_e / 3.0 #-- Switching between Cosine and Sine Stokes for cs, csharm in enumerate(['clm', 'slm']): #-- copy cosine and sin harmonics mascon_harm = getattr(mascon_Ylms, csharm) grace_harm = getattr(GRACE_Ylms, csharm) GIA_harm = getattr(GIA_Ylms, csharm) remove_harm = getattr(remove_Ylms, csharm) construct_harm = getattr(construct_Ylms, csharm) delta_harm = getattr(delta_Ylms, csharm) #-- for each spherical harmonic degree #-- +1 to include LMAX for l in range(LMIN, LMAX + 1): #-- for each spherical harmonic order #-- Sine Stokes for (m=0) = 0 mm = np.min([MMAX, l]) #-- +1 to include l or MMAX (whichever is smaller) for m in range(cs, mm + 1): #-- Mascon Spherical Harmonics M_lm[ii, :] = np.copy(mascon_harm[l, m, :]) #-- GRACE Spherical Harmonics #-- Correcting GRACE Harmonics for GIA and Removed Terms Y_lm[ii,:] = grace_harm[l,m,:] - GIA_harm[l,m,:] - \ remove_harm[l,m,:] - construct_harm[l,m,:] #-- GRACE delta spherical harmonics delta_lm[ii] = np.copy(delta_harm[l, m]) #-- degree dependent factor to convert to mass fact[ii] = (2.0 * l + 1.0) / (1.0 + kl[l]) #-- degree dependent smoothing wt_lm[ii] = np.copy(wt[l]) #-- add 1 to counter ii += 1 #-- Converting mascon coefficients to fit method if (FIT_METHOD == 1): #-- Fitting Sensitivity Kernel as mass coefficients #-- converting M_lm to mass coefficients of the kernel for i in range(n_harm): MA_lm[i, :] = M_lm[i, :] * wt_lm[i] * fact[i] fit_factor = wt_lm * fact else: #-- Fitting Sensitivity Kernel as geoid coefficients for i in range(n_harm): MA_lm[:, :] = M_lm[i, :] * wt_lm[i] fit_factor = wt_lm * np.ones((n_harm)) #-- Fitting the sensitivity kernel from the input kernel for i in range(n_harm): #-- setting kern_i equal to 1 for d/o kern_i = np.zeros((n_harm)) #-- converting to mass coefficients if specified kern_i[i] = 1.0 * fit_factor[i] #-- spherical harmonics solution for the #-- mascon sensitivity kernels #-- Least Squares Solutions: Inv(X'.X).(X'.Y) kern_lm = np.linalg.lstsq(MA_lm, kern_i, rcond=-1)[0] for k in range(n_mas): A_lm[i, k] = kern_lm[k] * total_area[k] #-- for each mascon for k in range(n_mas): #-- Multiply the Satellite error (noise of a smoothed time-series #-- with annual and semi-annual components) by the sensitivity kernel #-- Converting to Gigatonnes M_delta[k] = np.sqrt(np.sum((delta_lm * A_lm[:, k])**2)) / 1e15 #-- output filename format (for both LMAX==MMAX and LMAX != MMAX cases): #-- mascon name, GRACE dataset, GIA model, LMAX, (MMAX,) #-- Gaussian smoothing, filter flag, remove reconstructed fields flag #-- output GRACE error file file_out = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}{9}.txt'.format( mascon_name[k], dset_str, gia_str.upper(), atm_str, ocean_str, LMAX, order_str, gw_str, ds_str, construct_str) #-- Output mascon datafiles #-- Will output each mascon time series #-- month, date, mascon mass [Gt], satellite error [Gt], mascon area [km^2] #-- open output mascon time-series file fid = open(os.path.join(DIRECTORY, file_out), 'w') #-- for each date formatting_string = '{0:03d} {1:12.4f} {2:16.10f} {3:16.10f} {4:16.5f}' for t, mon in enumerate(GRACE_Ylms.month): #-- Summing over all spherical harmonics for mascon k, and time t #-- multiplies by the degree dependent factor to convert #-- the harmonics into mass coefficients #-- Converting mascon mass time-series from g to gigatonnes mascon[k, t] = np.sum(A_lm[:, k] * Y_lm[:, t]) / 1e15 #-- output to file args = (mon, GRACE_Ylms.time[t], mascon[k, t], M_delta[k], total_area[k] / 1e10) print(formatting_string.format(*args), file=fid) #-- close the output file fid.close() #-- change the permissions mode os.chmod(os.path.join(DIRECTORY, file_out), MODE) #-- add output files to list object output_files.append(os.path.join(DIRECTORY, file_out)) #-- return the list of output files return output_files
def gen_point_load(data, lon, lat, LMAX=60, MMAX=None, UNITS=1, LOVE=None): """ Calculates spherical harmonic coefficients for point masses Arguments --------- data: data magnitude lon: longitude of points lat: latitude of points Keyword arguments ----------------- LMAX: Upper bound of Spherical Harmonic Degrees MMAX: Upper bound of Spherical Harmonic Orders UNITS: input data units 1: grams of mass (default) 2: gigatonnes of mass LOVE: input load Love numbers up to degree LMAX (hl,kl,ll) Returns ------- clm: cosine spherical harmonic coefficients slm: sine spherical harmonic coefficients l: spherical harmonic degree to LMAX m: spherical harmonic order to MMAX """ #-- upper bound of spherical harmonic orders (default == LMAX) if MMAX is None: MMAX = np.copy(LMAX) #-- number of input data points npts = len(data.flatten()) #-- convert output longitude and latitude into radians phi = np.pi * lon.flatten() / 180.0 theta = np.pi * (90.0 - lat.flatten()) / 180.0 #-- SH Degree dependent factors to convert into geodesy normalized SH's #-- use splat operator to extract arrays of kl, hl, and ll Love Numbers factors = units(lmax=LMAX).spatial(*LOVE) #-- extract degree dependent factor for specific units int_fact = np.zeros((npts)) if (UNITS == 1): #-- Default Parameter: Input in g dfactor = factors.cmwe / (factors.rad_e**2) int_fact[:] = 1.0 elif (UNITS == 2): #-- Input in gigatonnes (Gt) dfactor = factors.cmwe / (factors.rad_e**2) int_fact[:] = 1e15 #-- flattened form of data converted to units D = int_fact * data.flatten() #-- output harmonics Ylms = {} Ylms['clm'] = np.zeros((LMAX + 1, MMAX + 1)) Ylms['slm'] = np.zeros((LMAX + 1, MMAX + 1)) Ylms['l'] = np.arange(LMAX + 1) Ylms['m'] = np.arange(MMAX + 1) #-- for each degree l for l in range(LMAX + 1): m1 = np.min([l, MMAX]) + 1 SPH = spherical_harmonic_matrix(l, D, phi, theta, dfactor[l]) #-- truncate to spherical harmonic order and save to output Ylms['clm'][l, :m1] = SPH.real[:m1] Ylms['slm'][l, :m1] = SPH.imag[:m1] #-- output harmonics return Ylms