From 56d544e1e3f9655814ba9cbfcd2bd377c9db28a8 Mon Sep 17 00:00:00 2001 From: Tyler Sutterley Date: Mon, 13 Jul 2026 12:14:06 -0700 Subject: [PATCH] refactor: `ruff` format refactor: simplify clenshaw summations --- .github/workflows/python-request.yml | 5 + .github/workflows/ruff-format.yml | 18 + access/cnes_grace_sync.py | 165 +- access/esa_costg_swarm_sync.py | 193 ++- access/gfz_icgem_costg_ftp.py | 220 ++- access/gfz_isdc_dealiasing_sync.py | 250 ++- access/gfz_isdc_grace_sync.py | 391 +++-- access/itsg_graz_grace_sync.py | 209 ++- access/podaac_cumulus.py | 405 +++-- dealiasing/aod1b_geocenter.py | 112 +- dealiasing/aod1b_oblateness.py | 114 +- dealiasing/dealiasing_global_uplift.py | 273 +-- dealiasing/dealiasing_monthly_mean.py | 506 ++++-- doc/source/background/sphharm.py | 58 +- doc/source/conf.py | 104 +- doc/source/getting_started/Install.ipynb | 2 +- .../notebooks/GRACE-Geostrophic-Maps.ipynb | 294 ++-- .../notebooks/GRACE-Harmonic-Plots.ipynb | 258 +-- .../notebooks/GRACE-Spatial-Error.ipynb | 216 ++- doc/source/notebooks/GRACE-Spatial-Maps.ipynb | 459 +++-- doc/source/user_guide/NASA-Earthdata.ipynb | 22 +- geocenter/calc_degree_one.py | 1409 ++++++++++----- geocenter/delta_degree_one.py | 962 +++++++---- geocenter/geocenter_compare_tellus.py | 259 ++- geocenter/geocenter_monte_carlo.py | 203 ++- geocenter/geocenter_ocean_models.py | 220 ++- geocenter/geocenter_processing_centers.py | 217 ++- geocenter/geocenter_spatial_maps.py | 496 ++++-- geocenter/kernel_degree_one.py | 476 +++-- geocenter/kernel_degree_one_error.py | 708 +++++--- geocenter/model_degree_one.py | 612 ++++--- geocenter/monte_carlo_degree_one.py | 1225 ++++++++----- gravity_toolkit/SLR/C20.py | 107 +- gravity_toolkit/SLR/C30.py | 64 +- gravity_toolkit/SLR/C40.py | 30 +- gravity_toolkit/SLR/C50.py | 58 +- gravity_toolkit/SLR/CS2.py | 86 +- gravity_toolkit/__init__.py | 29 +- gravity_toolkit/associated_legendre.py | 249 +-- gravity_toolkit/clenshaw_summation.py | 163 +- gravity_toolkit/degree_amplitude.py | 26 +- gravity_toolkit/destripe_harmonics.py | 228 +-- gravity_toolkit/fourier_legendre.py | 354 ++-- gravity_toolkit/gauss_weights.py | 26 +- gravity_toolkit/gen_averaging_kernel.py | 81 +- gravity_toolkit/gen_disc_load.py | 69 +- gravity_toolkit/gen_harmonics.py | 194 ++- gravity_toolkit/gen_point_load.py | 37 +- gravity_toolkit/gen_spherical_cap.py | 85 +- gravity_toolkit/gen_stokes.py | 45 +- gravity_toolkit/geocenter.py | 386 +++-- gravity_toolkit/grace_date.py | 172 +- gravity_toolkit/grace_find_months.py | 12 +- gravity_toolkit/grace_input_months.py | 393 +++-- gravity_toolkit/grace_months_index.py | 75 +- gravity_toolkit/harmonic_gradients.py | 137 +- gravity_toolkit/harmonic_summation.py | 92 +- gravity_toolkit/harmonics.py | 798 +++++---- gravity_toolkit/legendre.py | 100 +- gravity_toolkit/legendre_polynomials.py | 36 +- gravity_toolkit/mascons.py | 150 +- gravity_toolkit/ocean_stokes.py | 82 +- gravity_toolkit/read_GIA_model.py | 267 +-- gravity_toolkit/read_GRACE_harmonics.py | 158 +- gravity_toolkit/read_SLR_harmonics.py | 111 +- gravity_toolkit/read_gfc_harmonics.py | 89 +- gravity_toolkit/read_love_numbers.py | 176 +- gravity_toolkit/sea_level_equation.py | 274 +-- gravity_toolkit/spatial.py | 796 +++++---- gravity_toolkit/time.py | 336 ++-- gravity_toolkit/time_series/amplitude.py | 4 +- gravity_toolkit/time_series/fit.py | 16 +- gravity_toolkit/time_series/lomb_scargle.py | 44 +- gravity_toolkit/time_series/piecewise.py | 196 ++- gravity_toolkit/time_series/regress.py | 205 ++- gravity_toolkit/time_series/savitzky_golay.py | 58 +- gravity_toolkit/time_series/smooth.py | 223 ++- gravity_toolkit/tools.py | 567 +++--- gravity_toolkit/units.py | 134 +- gravity_toolkit/utilities.py | 952 +++++----- gravity_toolkit/version.py | 11 +- mapping/plot_AIS_GrIS_maps.py | 901 ++++++---- mapping/plot_AIS_grid_3maps.py | 810 ++++++--- mapping/plot_AIS_grid_4maps.py | 821 ++++++--- mapping/plot_AIS_grid_maps.py | 806 ++++++--- mapping/plot_AIS_grid_movie.py | 821 ++++++--- mapping/plot_AIS_regional_maps.py | 874 +++++++--- mapping/plot_AIS_regional_movie.py | 890 ++++++---- mapping/plot_GrIS_grid_3maps.py | 788 ++++++--- mapping/plot_GrIS_grid_5maps.py | 779 ++++++--- mapping/plot_GrIS_grid_maps.py | 780 ++++++--- mapping/plot_GrIS_grid_movie.py | 796 ++++++--- mapping/plot_QML_grid_3maps.py | 828 ++++++--- mapping/plot_global_grid_3maps.py | 629 ++++--- mapping/plot_global_grid_4maps.py | 636 ++++--- mapping/plot_global_grid_5maps.py | 641 ++++--- mapping/plot_global_grid_9maps.py | 640 ++++--- mapping/plot_global_grid_maps.py | 652 ++++--- mapping/plot_global_grid_movie.py | 632 ++++--- scripts/calc_SLR_RMS.py | 257 ++- scripts/calc_harmonic_resolution.py | 51 +- scripts/calc_mascon.py | 732 +++++--- scripts/calc_sensitivity_kernel.py | 377 ++-- scripts/combine_HEX_ATM_errors.py | 537 ++++-- scripts/combine_HEX_Caron_errors.py | 430 +++-- scripts/combine_HEX_OBP_errors.py | 540 ++++-- scripts/combine_HEX_SLF_errors.py | 451 +++-- scripts/combine_HEX_TWC_errors.py | 543 ++++-- scripts/combine_HEX_leakage.py | 501 ++++-- scripts/combine_HEX_mascon_timeseries.py | 472 +++-- scripts/combine_HEX_mascons.py | 491 ++++-- scripts/combine_HEX_spherical_caps.py | 558 ++++-- scripts/combine_harmonics.py | 311 ++-- scripts/combine_sea_level_data.py | 163 +- scripts/convert_harmonics.py | 234 ++- scripts/copy_parameter_files.py | 240 ++- scripts/create_public_SLF_data.py | 416 +++-- scripts/create_public_timeseries.py | 520 ++++-- scripts/gia_covariance_errors_caron.py | 358 ++-- scripts/grace_mean_harmonics.py | 436 +++-- scripts/grace_raster_grids.py | 678 +++++--- scripts/grace_spatial_differences.py | 411 +++-- scripts/grace_spatial_error.py | 589 +++++-- scripts/grace_spatial_maps.py | 636 ++++--- scripts/grace_spatial_mask.py | 469 +++-- scripts/grace_spatial_mean.py | 570 ++++-- scripts/make_sea_level_error_shells.py | 505 ++++-- scripts/make_sea_level_mascon_shells.py | 516 ++++-- scripts/make_sea_level_shells.py | 186 +- scripts/mascon_reconstruct.py | 276 ++- scripts/piecewise_grace_maps.py | 563 ++++-- scripts/plot_SLR_azimuthal.py | 365 ++-- scripts/plot_SLR_zonals.py | 459 +++-- scripts/plot_mascon_SLF_combined.py | 724 +++++--- scripts/plot_mascon_SLF_iterations.py | 1538 ++++++++++++----- scripts/plot_mascon_SLF_timeseries.py | 1386 ++++++++++----- scripts/regional_spherical_caps.py | 207 ++- scripts/regress_grace_maps.py | 572 ++++-- scripts/remove_grace_spatial.py | 205 ++- scripts/remove_mascon_reconstruct.py | 28 +- scripts/remove_sea_level_errors.py | 146 +- scripts/remove_sea_level_fields.py | 240 ++- scripts/run_sea_level_equation.py | 311 ++-- scripts/scale_grace_maps.py | 750 +++++--- scripts/sea_level_differences.py | 202 ++- scripts/sea_level_error.py | 279 ++- scripts/sea_level_mean.py | 360 ++-- scripts/sea_level_regress.py | 582 +++++-- scripts/sea_level_stokes.py | 366 ++-- scripts/simple_parallel_shell.py | 65 +- scripts/upload_to_figshare.py | 124 +- setup.py | 17 +- utilities/make_grace_index.py | 121 +- utilities/nominal_grace_date.py | 97 +- utilities/quick_mascon_plot.py | 286 +-- utilities/quick_mascon_regress.py | 219 ++- utilities/run_grace_date.py | 107 +- 157 files changed, 38539 insertions(+), 19000 deletions(-) create mode 100644 .github/workflows/ruff-format.yml diff --git a/.github/workflows/python-request.yml b/.github/workflows/python-request.yml index 95bad2dc..ac94770d 100644 --- a/.github/workflows/python-request.yml +++ b/.github/workflows/python-request.yml @@ -7,7 +7,12 @@ on: pull_request: paths: - gravity_toolkit/** + - access/** + - dealiasing/** + - geocenter/** + - mapping/** - scripts/** + - utilities/** - test/** - .github/workflows/python-request.yml schedule: diff --git a/.github/workflows/ruff-format.yml b/.github/workflows/ruff-format.yml new file mode 100644 index 00000000..609db62b --- /dev/null +++ b/.github/workflows/ruff-format.yml @@ -0,0 +1,18 @@ +name: Ruff Format + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - main + +jobs: + ruff-format: + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@v6 + - name: Format and annotate PR + uses: astral-sh/ruff-action@v3 + with: + version: "latest" + args: "format --check --diff" diff --git a/access/cnes_grace_sync.py b/access/cnes_grace_sync.py index 38dc654c..21c44de8 100755 --- a/access/cnes_grace_sync.py +++ b/access/cnes_grace_sync.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" cnes_grace_sync.py Written by Tyler Sutterley (12/2022) @@ -91,6 +91,7 @@ added functionality for RL01 and RL03 (future release) Written 07/2012 """ + from __future__ import print_function import sys @@ -108,11 +109,13 @@ import posixpath import gravity_toolkit as gravtk + # PURPOSE: sync local GRACE/GRACE-FO files with CNES server -def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, - CLOBBER=False, MODE=None): +def cnes_grace_sync( + DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, CLOBBER=False, MODE=None +): # remote CNES/GRGS host directory - HOST = ['http://gravitegrace.get.obs-mip.fr','grgs.obs-mip.fr','data'] + HOST = ['http://gravitegrace.get.obs-mip.fr', 'grgs.obs-mip.fr', 'data'] # check if directory exists and recursively create if not DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() @@ -127,27 +130,27 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, DSET['RL05'] = ['GSM', 'GAA', 'GAB'] # remote path to tar files on CNES servers - REMOTE = dict(RL01={},RL02={},RL03={},RL04={},RL05={}) + REMOTE = dict(RL01={}, RL02={}, RL03={}, RL04={}, RL05={}) # RL01: GSM and GAC - REMOTE['RL01']['GSM'] = ['RL01','variable','archives'] - REMOTE['RL01']['GAC'] = ['RL01','variable','archives'] + REMOTE['RL01']['GSM'] = ['RL01', 'variable', 'archives'] + REMOTE['RL01']['GAC'] = ['RL01', 'variable', 'archives'] # RL02: GSM, GAA and GAB - REMOTE['RL02']['GSM'] = ['RL02','variable','archives'] - REMOTE['RL02']['GAA'] = ['RL02','variable','archives'] - REMOTE['RL02']['GAB'] = ['RL02','variable','archives'] + REMOTE['RL02']['GSM'] = ['RL02', 'variable', 'archives'] + REMOTE['RL02']['GAA'] = ['RL02', 'variable', 'archives'] + REMOTE['RL02']['GAB'] = ['RL02', 'variable', 'archives'] # RL03: GSM, GAA and GAB - REMOTE['RL03']['GSM'] = ['RL03-v3','archives'] - REMOTE['RL03']['GAA'] = ['RL03','variable','archives'] - REMOTE['RL03']['GAB'] = ['RL03','variable','archives'] + REMOTE['RL03']['GSM'] = ['RL03-v3', 'archives'] + REMOTE['RL03']['GAA'] = ['RL03', 'variable', 'archives'] + REMOTE['RL03']['GAB'] = ['RL03', 'variable', 'archives'] # RL04: GSM - REMOTE['RL04']['GSM'] = ['RL04-v1','archives'] + REMOTE['RL04']['GSM'] = ['RL04-v1', 'archives'] # RL05: GSM, GAA, GAB for GRACE/GRACE-FO - REMOTE['RL05']['GSM'] = ['RL05','archives'] - REMOTE['RL05']['GAA'] = ['RL05','archives'] - REMOTE['RL05']['GAB'] = ['RL05','archives'] + REMOTE['RL05']['GSM'] = ['RL05', 'archives'] + REMOTE['RL05']['GAA'] = ['RL05', 'archives'] + REMOTE['RL05']['GAB'] = ['RL05', 'archives'] # tar file names for each dataset - TAR = dict(RL01={},RL02={},RL03={},RL04={},RL05={}) + TAR = dict(RL01={}, RL02={}, RL03={}, RL04={}, RL05={}) # RL01: GSM and GAC TAR['RL01']['GSM'] = ['GRGS.SH_models.GRACEFORMAT.RL01.tar.gz'] TAR['RL01']['GAC'] = ['GRGS.dealiasing.RL01.tar.gz'] @@ -161,10 +164,14 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, TAR['RL03']['GAB'] = ['GRGS.RL03.dealiasing.monthly.tar.gz'] # RL04: GSM # TAR['RL04']['GSM'] = ['CNES.RL04-v1.monthly.OLD_IERS2010_MEAN_POLE_CONVENTION.tar.gz'] - TAR['RL04']['GSM'] = ['CNES.RL04-v1.monthly.NEW_IERS2010_MEAN_POLE_CONVENTION.tar.gz'] + TAR['RL04']['GSM'] = [ + 'CNES.RL04-v1.monthly.NEW_IERS2010_MEAN_POLE_CONVENTION.tar.gz' + ] # RL05: GSM, GAA and GAB - TAR['RL05']['GSM'] = ['CNES-GRGS.RL05.GRACE.monthly.tar.gz', - 'CNES-GRGS.RL05.GRACE-FO.monthly.tar.gz'] + TAR['RL05']['GSM'] = [ + 'CNES-GRGS.RL05.GRACE.monthly.tar.gz', + 'CNES-GRGS.RL05.GRACE-FO.monthly.tar.gz', + ] TAR['RL05']['GAA'] = ['CNES-GRGS.RL05.monthly.dealiasing.tar.gz'] TAR['RL05']['GAB'] = ['CNES-GRGS.RL05.monthly.dealiasing.tar.gz'] @@ -172,7 +179,7 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, if LOG: # output to log file # format: CNES_sync_2002-04-01.log - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) LOGFILE = DIRECTORY.joinpath(f'CNES_sync_{today}.log') fid1 = LOGFILE.open(mode='w', encoding='utf8') logging.basicConfig(stream=fid1, level=logging.INFO) @@ -200,18 +207,27 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, local_file = DIRECTORY.joinpath('CNES', rl, t) MD5 = gravtk.utilities.get_hash(local_file) # copy remote tar file to local if new or updated - gravtk.utilities.from_http(remote_tar_path, - local=local_file, timeout=TIMEOUT, hash=MD5, chunk=16384, - verbose=True, fid=fid1, mode=MODE) + gravtk.utilities.from_http( + remote_tar_path, + local=local_file, + timeout=TIMEOUT, + hash=MD5, + chunk=16384, + verbose=True, + fid=fid1, + mode=MODE, + ) # Create and submit request to get modification time of file remote_file = posixpath.join(*remote_tar_path) request = gravtk.utilities.urllib2.Request(remote_file) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=TIMEOUT + ) # change modification time to remote time_string = response.headers['last-modified'] - remote_mtime = gravtk.utilities.get_unix_time(time_string, - format='%a, %d %b %Y %H:%M:%S %Z') + remote_mtime = gravtk.utilities.get_unix_time( + time_string, format='%a, %d %b %Y %H:%M:%S %Z' + ) # keep remote modification time of file and local access time os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) @@ -219,7 +235,9 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, tar = tarfile.open(name=local_file, mode='r:gz') # copy files from the tar file into the data directory - member_list=[m for m in tar.getmembers() if re.search(ds,m.name)] + member_list = [ + m for m in tar.getmembers() if re.search(ds, m.name) + ] # for each member of the dataset within the tar file for member in member_list: # local gzipped version of the file @@ -230,8 +248,9 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, tar.close() # find GRACE files and sort by date - grace_files = [f.name for f in local_dir.iterdir() - if re.search(ds, f.name)] + grace_files = [ + f.name for f in local_dir.iterdir() if re.search(ds, f.name) + ] # outputting GRACE filenames to index index_file = local_dir.joinpath('index.txt') with index_file.open(mode='w', encoding='utf8') as fid: @@ -245,6 +264,7 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, fid1.close() LOGFILE.chmod(mode=MODE) + # PURPOSE: copy file from tar file checking if file exists locally # and if the original file is newer than the local file def gzip_copy_file(tar, member, local_file, CLOBBER, MODE): @@ -261,9 +281,9 @@ def gzip_copy_file(tar, member, local_file, CLOBBER, MODE): fileobj = fileID.fileobj fileobj.seek(4) # extract little endian 4 bit unsigned integer - file2_mtime, = struct.unpack(" file2_mtime): + if file1_mtime > file2_mtime: TEST = True OVERWRITE = ' (overwrite)' else: @@ -283,6 +303,7 @@ def gzip_copy_file(tar, member, local_file, CLOBBER, MODE): os.utime(local_file, (local_file.stat().st_atime, file1_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -292,46 +313,78 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, nargs='+', - default=['RL05'], choices=['RL01','RL02','RL03','RL04','RL05'], - help='GRACE/GRACE-FO data release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + nargs='+', + default=['RL05'], + choices=['RL01', 'RL02', 'RL03', 'RL04', 'RL05'], + help='GRACE/GRACE-FO data release', + ) # connection timeout - parser.add_argument('--timeout','-t', - type=int, default=360, - help='Timeout in seconds for blocking operations') + parser.add_argument( + '--timeout', + '-t', + type=int, + default=360, + help='Timeout in seconds for blocking operations', + ) # Output log file in form # CNES_sync_2002-04-01.log - parser.add_argument('--log','-l', - default=False, action='store_true', - help='Output log file') - parser.add_argument('--clobber','-C', - default=False, action='store_true', - help='Overwrite existing data in transfer') + parser.add_argument( + '--log', + '-l', + default=False, + action='store_true', + help='Output log file', + ) + parser.add_argument( + '--clobber', + '-C', + default=False, + action='store_true', + help='Overwrite existing data in transfer', + ) # permissions mode of the directories and files synced (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permission mode of directories and files synced') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permission mode of directories and files synced', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # check internet connection before attempting to run program HOST = 'http://gravitegrace.get.obs-mip.fr' if gravtk.utilities.check_connection(HOST): - cnes_grace_sync(args.directory, DREL=args.release, - TIMEOUT=args.timeout, LOG=args.log, - CLOBBER=args.clobber, MODE=args.mode) + cnes_grace_sync( + args.directory, + DREL=args.release, + TIMEOUT=args.timeout, + LOG=args.log, + CLOBBER=args.clobber, + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/access/esa_costg_swarm_sync.py b/access/esa_costg_swarm_sync.py index 07209153..080baf35 100644 --- a/access/esa_costg_swarm_sync.py +++ b/access/esa_costg_swarm_sync.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" esa_costg_swarm_sync.py Written by Tyler Sutterley (05/2023) Syncs Swarm gravity field products from the ESA Swarm Science Server @@ -36,6 +36,7 @@ Updated 10/2021: using python logging for handling verbose output Written 09/2021 """ + from __future__ import print_function import sys @@ -52,21 +53,29 @@ import lxml.etree import gravity_toolkit as gravtk -# PURPOSE: sync local Swarm files with ESA server -def esa_costg_swarm_sync(DIRECTORY, RELEASE=None, TIMEOUT=None, LOG=False, - LIST=False, CLOBBER=False, CHECKSUM=False, MODE=0o775): +# PURPOSE: sync local Swarm files with ESA server +def esa_costg_swarm_sync( + DIRECTORY, + RELEASE=None, + TIMEOUT=None, + LOG=False, + LIST=False, + CLOBBER=False, + CHECKSUM=False, + MODE=0o775, +): # check if directory exists and recursively create if not DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() # local directory for exact data product - local_dir = DIRECTORY.joinpath('Swarm',RELEASE,'GSM') + local_dir = DIRECTORY.joinpath('Swarm', RELEASE, 'GSM') local_dir.mkdir(mode=MODE, parents=True, exist_ok=True) # create log file with list of synchronized files (or print to terminal) if LOG: # output to log file # format: ESA_Swarm_sync_2002-04-01.log - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) LOGFILE = DIRECTORY.joinpath(f'ESA_Swarm_sync_{today}.log') logging.basicConfig(filename=LOGFILE, level=logging.INFO) logging.info(f'ESA Swarm Sync Log ({today})') @@ -81,8 +90,9 @@ def esa_costg_swarm_sync(DIRECTORY, RELEASE=None, TIMEOUT=None, LOG=False, # compile xml parsers for lxml XMLparser = lxml.etree.XMLParser() # create "opener" (OpenerDirector instance) - gravtk.utilities.build_opener(None, None, - authorization_header=False, urs=HOST) + gravtk.utilities.build_opener( + None, None, authorization_header=False, urs=HOST + ) # All calls to urllib2.urlopen will now use handler # Make sure not to include the protocol in with the URL, or # HTTPPasswordMgrWithDefaultRealm will be confused. @@ -95,20 +105,24 @@ def esa_costg_swarm_sync(DIRECTORY, RELEASE=None, TIMEOUT=None, LOG=False, colnames = [] collastmod = [] # position, maximum number of files to list, flag to check if done - pos,maxfiles,prevmax = (0,500,500) + pos, maxfiles, prevmax = (0, 500, 500) # iterate to get a compiled list of files # will iterate until there are no more files to add to the lists - while (maxfiles == prevmax): + while maxfiles == prevmax: # set previous flag to maximum prevmax = maxfiles # open connection with Swarm science server at remote directory # to list maxfiles number of files at position - parameters = gravtk.utilities.urlencode({'maxfiles':prevmax, - 'pos':pos,'file':posixpath.join('swarm','Level2longterm','EGF')}) - url=posixpath.join(HOST,f'?do=list&{parameters}') + parameters = gravtk.utilities.urlencode( + { + 'maxfiles': prevmax, + 'pos': pos, + 'file': posixpath.join('swarm', 'Level2longterm', 'EGF'), + } + ) + url = posixpath.join(HOST, f'?do=list&{parameters}') request = gravtk.utilities.urllib2.Request(url=url) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen(request, timeout=TIMEOUT) table = json.loads(response.read().decode()) # extend lists with new files colnames.extend([t['name'] for t in table['results']]) @@ -119,22 +133,33 @@ def esa_costg_swarm_sync(DIRECTORY, RELEASE=None, TIMEOUT=None, LOG=False, pos += maxfiles # find lines of valid files - valid_lines = [i for i,f in enumerate(colnames) if R1.match(f)] + valid_lines = [i for i, f in enumerate(colnames) if R1.match(f)] # write each file to an index - index_file = local_dir.joinpath(local_dir,'index.txt') + index_file = local_dir.joinpath(local_dir, 'index.txt') fid = index_file.open(mode='w', encoding='utf8') # for each data and header file for i in valid_lines: # remote and local versions of the file - parameters = gravtk.utilities.urlencode({'file': - posixpath.join('swarm','Level2longterm','EGF',colnames[i])}) - remote_file = posixpath.join(HOST, - f'?do=download&{parameters}') + parameters = gravtk.utilities.urlencode( + { + 'file': posixpath.join( + 'swarm', 'Level2longterm', 'EGF', colnames[i] + ) + } + ) + remote_file = posixpath.join(HOST, f'?do=download&{parameters}') local_file = local_dir.joinpath(colnames[i]) # check that file is not in file system unless overwriting - http_pull_file(remote_file, collastmod[i], local_file, - TIMEOUT=TIMEOUT, LIST=LIST, CLOBBER=CLOBBER, - CHECKSUM=CHECKSUM, MODE=MODE) + http_pull_file( + remote_file, + collastmod[i], + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + CHECKSUM=CHECKSUM, + MODE=MODE, + ) # output Swarm filenames to index print(colnames[i], file=fid) # change permissions of index file @@ -144,10 +169,19 @@ def esa_costg_swarm_sync(DIRECTORY, RELEASE=None, TIMEOUT=None, LOG=False, if LOG: LOGFILE.chmod(mode=MODE) + # PURPOSE: pull file from a remote host checking if file exists locally # and if the remote file is newer than the local file -def http_pull_file(remote_file, remote_mtime, local_file, TIMEOUT=120, - LIST=False, CLOBBER=False, CHECKSUM=False, MODE=0o775): +def http_pull_file( + remote_file, + remote_mtime, + local_file, + TIMEOUT=120, + LIST=False, + CLOBBER=False, + CHECKSUM=False, + MODE=0o775, +): # if file exists in file system: check if remote file is newer TEST = False OVERWRITE = ' (clobber)' @@ -161,22 +195,23 @@ def http_pull_file(remote_file, remote_mtime, local_file, TIMEOUT=120, # There are a wide range of exceptions that can be thrown here # including HTTPError and URLError. req = gravtk.utilities.urllib2.Request(remote_file) - resp = gravtk.utilities.urllib2.urlopen(req,timeout=TIMEOUT) + resp = gravtk.utilities.urllib2.urlopen(req, timeout=TIMEOUT) # copy remote file contents to bytesIO object remote_buffer = io.BytesIO(resp.read()) remote_buffer.seek(0) # generate checksum hash for remote file remote_hash = gravtk.utilities.get_hash(remote_buffer) # compare checksums - if (local_hash != remote_hash): + if local_hash != remote_hash: TEST = True OVERWRITE = f' (checksums: {local_hash} {remote_hash})' elif local_file.exists(): # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -202,8 +237,9 @@ def http_pull_file(remote_file, remote_mtime, local_file, TIMEOUT=120, # There are a range of exceptions that can be thrown here # including HTTPError and URLError. request = gravtk.utilities.urllib2.Request(remote_file) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=TIMEOUT + ) # copy remote file contents to local file with local_file.open(mode='wb') as f: shutil.copyfileobj(response, f, CHUNK) @@ -211,6 +247,7 @@ def http_pull_file(remote_file, remote_mtime, local_file, TIMEOUT=120, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -220,40 +257,72 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # data release - parser.add_argument('--release','-r', - type=str, default='RL01', choices=['RL01'], - help='Data release to sync') + parser.add_argument( + '--release', + '-r', + type=str, + default='RL01', + choices=['RL01'], + help='Data release to sync', + ) # connection timeout - parser.add_argument('--timeout','-t', - type=int, default=360, - help='Timeout in seconds for blocking operations') + parser.add_argument( + '--timeout', + '-t', + type=int, + default=360, + help='Timeout in seconds for blocking operations', + ) # Output log file in form # ESA_Swarm_sync_2002-04-01.log - parser.add_argument('--log','-l', - default=False, action='store_true', - help='Output log file') + parser.add_argument( + '--log', + '-l', + default=False, + action='store_true', + help='Output log file', + ) # sync options - parser.add_argument('--list','-L', - default=False, action='store_true', - help='Only print files that could be transferred') - parser.add_argument('--clobber','-C', - default=False, action='store_true', - help='Overwrite existing data in transfer') - parser.add_argument('--checksum', - default=False, action='store_true', - help='Compare hashes to check for overwriting existing data') + parser.add_argument( + '--list', + '-L', + default=False, + action='store_true', + help='Only print files that could be transferred', + ) + parser.add_argument( + '--clobber', + '-C', + default=False, + action='store_true', + help='Overwrite existing data in transfer', + ) + parser.add_argument( + '--checksum', + default=False, + action='store_true', + help='Compare hashes to check for overwriting existing data', + ) # permissions mode of the directories and files synced (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permission mode of directories and files synced') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permission mode of directories and files synced', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program @@ -263,9 +332,17 @@ def main(): # check internet connection before attempting to run program HOST = 'https://swarm-diss.eo.esa.int' if gravtk.utilities.check_connection(HOST): - esa_costg_swarm_sync(args.directory, RELEASE=args.release, - TIMEOUT=args.timeout, LOG=args.log, LIST=args.list, - CLOBBER=args.clobber, CHECKSUM=args.checksum, MODE=args.mode) + esa_costg_swarm_sync( + args.directory, + RELEASE=args.release, + TIMEOUT=args.timeout, + LOG=args.log, + LIST=args.list, + CLOBBER=args.clobber, + CHECKSUM=args.checksum, + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/access/gfz_icgem_costg_ftp.py b/access/gfz_icgem_costg_ftp.py index 15e75880..33b1aec1 100644 --- a/access/gfz_icgem_costg_ftp.py +++ b/access/gfz_icgem_costg_ftp.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gfz_icgem_costg_ftp.py Written by Tyler Sutterley (05/2023) Syncs GRACE/GRACE-FO/Swarm COST-G data from the GFZ International @@ -45,6 +45,7 @@ Updated 10/2021: using python logging for handling verbose output Written 09/2021 """ + from __future__ import print_function import sys @@ -60,31 +61,41 @@ import posixpath import gravity_toolkit as gravtk + # PURPOSE: create and compile regular expression operator to find files def compile_regex_pattern(MISSION, DSET): - if ((DSET == 'GSM') and (MISSION == 'Swarm')): + if (DSET == 'GSM') and (MISSION == 'Swarm'): # regular expression operators for Swarm data - regex=r'(SW)_(.*?)_(EGF_SHA_2)__(.*?)_(.*?)_(.*?)(\.gfc|\.ZIP)' - elif ((DSET != 'GSM') and (MISSION == 'Swarm')): - regex=r'(GAA|GAB|GAC|GAD)_Swarm_(\d+)_(\d{2})_(\d{4})(\.gfc|\.ZIP)' + regex = r'(SW)_(.*?)_(EGF_SHA_2)__(.*?)_(.*?)_(.*?)(\.gfc|\.ZIP)' + elif (DSET != 'GSM') and (MISSION == 'Swarm'): + regex = r'(GAA|GAB|GAC|GAD)_Swarm_(\d+)_(\d{2})_(\d{4})(\.gfc|\.ZIP)' else: - regex=rf'{DSET}-2_(.*?)\.gfc$' + regex = rf'{DSET}-2_(.*?)\.gfc$' # return the compiled regular expression operator used to find files return re.compile(regex, re.VERBOSE) -# PURPOSE: sync local GRACE/GRACE-FO/Swarm files with GFZ ICGEM server -def gfz_icgem_costg_ftp(DIRECTORY, MISSION=[], RELEASE=None, TIMEOUT=None, - LOG=False, LIST=False, CLOBBER=False, CHECKSUM=False, MODE=None): +# PURPOSE: sync local GRACE/GRACE-FO/Swarm files with GFZ ICGEM server +def gfz_icgem_costg_ftp( + DIRECTORY, + MISSION=[], + RELEASE=None, + TIMEOUT=None, + LOG=False, + LIST=False, + CLOBBER=False, + CHECKSUM=False, + MODE=None, +): # check if directory exists and recursively create if not DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) # dealiasing datasets for each mission DSET = {} - DSET['Grace'] = ['GAC','GSM'] + DSET['Grace'] = ['GAC', 'GSM'] DSET['Grace-FO'] = ['GSM'] - DSET['Swarm'] = ['GAA','GAB','GAC','GAD','GSM'] + DSET['Swarm'] = ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'] # local subdirectory for data LOCAL = {} LOCAL['Grace'] = 'COSTG' @@ -95,7 +106,7 @@ def gfz_icgem_costg_ftp(DIRECTORY, MISSION=[], RELEASE=None, TIMEOUT=None, if LOG: # output to log file # format: GFZ_ICGEM_COST-G_sync_2002-04-01.log - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) LOGFILE = DIRECTORY.joinpath(f'GFZ_ICGEM_COST-G_sync_{today}.log') logging.basicConfig(filename=LOGFILE, level=logging.INFO) logging.info(f'GFZ ICGEM COST-G Sync Log ({today})') @@ -121,33 +132,42 @@ def gfz_icgem_costg_ftp(DIRECTORY, MISSION=[], RELEASE=None, TIMEOUT=None, # compile the regular expression operator to find files R1 = compile_regex_pattern(MISSION, ds) # set the remote path to download files - if ds in ('GAA','GAB','GAC','GAD') and (MISSION == 'Swarm'): - remote_path = [ftp.host,'02_COST-G',MISSION,'GAX_products',ds] - elif ds in ('GAA','GAB','GAC','GAD') and (MISSION != 'Swarm'): - remote_path = [ftp.host,'02_COST-G',MISSION,'GAX_products'] - elif (MISSION == 'Swarm'): - remote_path = [ftp.host,'02_COST-G',MISSION,'40x40'] - elif (MISSION == 'Grace'): - remote_path = [ftp.host,'02_COST-G',MISSION,'unfiltered'] - elif (MISSION == 'Grace-FO'): - remote_path = [ftp.host,'02_COST-G',MISSION] + if ds in ('GAA', 'GAB', 'GAC', 'GAD') and (MISSION == 'Swarm'): + remote_path = [ftp.host, '02_COST-G', MISSION, 'GAX_products', ds] + elif ds in ('GAA', 'GAB', 'GAC', 'GAD') and (MISSION != 'Swarm'): + remote_path = [ftp.host, '02_COST-G', MISSION, 'GAX_products'] + elif MISSION == 'Swarm': + remote_path = [ftp.host, '02_COST-G', MISSION, '40x40'] + elif MISSION == 'Grace': + remote_path = [ftp.host, '02_COST-G', MISSION, 'unfiltered'] + elif MISSION == 'Grace-FO': + remote_path = [ftp.host, '02_COST-G', MISSION] # get filenames from remote directory - remote_files,remote_mtimes = gravtk.utilities.ftp_list( - remote_path, timeout=TIMEOUT, basename=True, pattern=R1, - sort=True) + remote_files, remote_mtimes = gravtk.utilities.ftp_list( + remote_path, timeout=TIMEOUT, basename=True, pattern=R1, sort=True + ) # download the file from the ftp server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # remote and local versions of the file remote_path.append(fi) local_file = local_dir.joinpath(fi) - ftp_mirror_file(ftp, remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, CHECKSUM=CHECKSUM, MODE=MODE) + ftp_mirror_file( + ftp, + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + CHECKSUM=CHECKSUM, + MODE=MODE, + ) # remove the file from the remote path list remote_path.remove(fi) # find local GRACE/GRACE-FO/Swarm files to create index - grace_files = sorted([f.name for f in local_dir.iterdir() - if R1.match(f.name)]) + grace_files = sorted( + [f.name for f in local_dir.iterdir() if R1.match(f.name)] + ) # write each file to an index index_file = local_dir.joinpath('index.txt') with index_file.open(mode='w', encoding='utf8') as fid: @@ -163,10 +183,20 @@ def gfz_icgem_costg_ftp(DIRECTORY, MISSION=[], RELEASE=None, TIMEOUT=None, if LOG: LOGFILE.chmod(mode=MODE) + # PURPOSE: pull file from a remote host checking if file exists locally # and if the remote file is newer than the local file -def ftp_mirror_file(ftp,remote_path,remote_mtime,local_file, - TIMEOUT=None,LIST=False,CLOBBER=False,CHECKSUM=False,MODE=0o775): +def ftp_mirror_file( + ftp, + remote_path, + remote_mtime, + local_file, + TIMEOUT=None, + LIST=False, + CLOBBER=False, + CHECKSUM=False, + MODE=0o775, +): # if file exists in file system: check if remote file is newer TEST = False OVERWRITE = ' (clobber)' @@ -177,20 +207,20 @@ def ftp_mirror_file(ftp,remote_path,remote_mtime,local_file, # open the local_file in binary read mode local_hash = gravtk.utilities.get_hash(local_file) # copy remote file contents to bytesIO object - remote_buffer = gravtk.utilities.from_ftp(remote_path, - timeout=TIMEOUT) + remote_buffer = gravtk.utilities.from_ftp(remote_path, timeout=TIMEOUT) # generate checksum hash for remote file remote_hash = hashlib.md5(remote_buffer.getvalue()).hexdigest() # compare checksums - if (local_hash != remote_hash): + if local_hash != remote_hash: TEST = True OVERWRITE = f' (checksums: {local_hash} {remote_hash})' elif local_file.exists(): # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -199,7 +229,7 @@ def ftp_mirror_file(ftp,remote_path,remote_mtime,local_file, # if file does not exist locally, is to be overwritten, or CLOBBER is set if TEST or CLOBBER: # Printing files transferred - remote_ftp_url = posixpath.join('ftp://',*remote_path) + remote_ftp_url = posixpath.join('ftp://', *remote_path) logging.info(f'{remote_ftp_url} -->') logging.info(f'\t{str(local_file)}{OVERWRITE}\n') # if executing copy command (not only printing the files) @@ -220,6 +250,7 @@ def ftp_mirror_file(ftp,remote_path,remote_mtime,local_file, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -229,63 +260,108 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # mission (GRACE, GRACE Follow-On or Swarm) - choices = ['Grace','Grace-FO','Swarm'] - parser.add_argument('--mission','-m', - type=str, nargs='+', - default=['Grace','Grace-FO','Swarm'], choices=choices, - help='Mission to sync between GRACE, GRACE-FO and Swarm') + choices = ['Grace', 'Grace-FO', 'Swarm'] + parser.add_argument( + '--mission', + '-m', + type=str, + nargs='+', + default=['Grace', 'Grace-FO', 'Swarm'], + choices=choices, + help='Mission to sync between GRACE, GRACE-FO and Swarm', + ) # data release - parser.add_argument('--release','-r', - type=str, default='RL01', choices=['RL01'], - help='Data release to sync') + parser.add_argument( + '--release', + '-r', + type=str, + default='RL01', + choices=['RL01'], + help='Data release to sync', + ) # connection timeout - parser.add_argument('--timeout','-t', - type=int, default=360, - help='Timeout in seconds for blocking operations') + parser.add_argument( + '--timeout', + '-t', + type=int, + default=360, + help='Timeout in seconds for blocking operations', + ) # Output log file in form # GFZ_ICGEM_COST-G_sync_2002-04-01.log - parser.add_argument('--log','-l', - default=False, action='store_true', - help='Output log file') + parser.add_argument( + '--log', + '-l', + default=False, + action='store_true', + help='Output log file', + ) # sync options - parser.add_argument('--list','-L', - default=False, action='store_true', - help='Only print files that could be transferred') - parser.add_argument('--checksum', - default=False, action='store_true', - help='Compare hashes to check for overwriting existing data') - parser.add_argument('--clobber','-C', - default=False, action='store_true', - help='Overwrite existing data in transfer') + parser.add_argument( + '--list', + '-L', + default=False, + action='store_true', + help='Only print files that could be transferred', + ) + parser.add_argument( + '--checksum', + default=False, + action='store_true', + help='Compare hashes to check for overwriting existing data', + ) + parser.add_argument( + '--clobber', + '-C', + default=False, + action='store_true', + help='Overwrite existing data in transfer', + ) # permissions mode of the directories and files synced (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permission mode of directories and files synced') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permission mode of directories and files synced', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # check internet connection before attempting to run program HOST = 'icgem.gfz-potsdam.de' if gravtk.utilities.check_ftp_connection(HOST): for m in args.mission: - gfz_icgem_costg_ftp(args.directory, MISSION=m, - RELEASE=args.release, TIMEOUT=args.timeout, - LIST=args.list, LOG=args.log, CLOBBER=args.clobber, - CHECKSUM=args.checksum, MODE=args.mode) + gfz_icgem_costg_ftp( + args.directory, + MISSION=m, + RELEASE=args.release, + TIMEOUT=args.timeout, + LIST=args.list, + LOG=args.log, + CLOBBER=args.clobber, + CHECKSUM=args.checksum, + MODE=args.mode, + ) else: raise RuntimeError('Check internet connection') + # run main program if __name__ == '__main__': main() diff --git a/access/gfz_isdc_dealiasing_sync.py b/access/gfz_isdc_dealiasing_sync.py index 32f23424..e84bb695 100644 --- a/access/gfz_isdc_dealiasing_sync.py +++ b/access/gfz_isdc_dealiasing_sync.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gfz_isdc_dealiasing_sync.py Written by Tyler Sutterley (10/2025) Syncs GRACE Level-1b dealiasing products from the GFZ Information @@ -48,6 +48,7 @@ Updated 03/2018: made tar file creation optional with --tar Written 03/2018 """ + from __future__ import print_function import sys @@ -63,20 +64,30 @@ import posixpath import gravity_toolkit as gravtk + # PURPOSE: syncs GRACE Level-1b dealiasing products from the GFZ data server # and optionally outputs as monthly tar files -def gfz_isdc_dealiasing_sync(base_dir, DREL, YEAR=None, MONTHS=None, TAR=False, - TIMEOUT=None, LOG=False, CLOBBER=False, MODE=None): +def gfz_isdc_dealiasing_sync( + base_dir, + DREL, + YEAR=None, + MONTHS=None, + TAR=False, + TIMEOUT=None, + LOG=False, + CLOBBER=False, + MODE=None, +): # check if directory exists and recursively create if not base_dir = pathlib.Path(base_dir).expanduser().absolute() - grace_dir = base_dir.joinpath('AOD1B',DREL) + grace_dir = base_dir.joinpath('AOD1B', DREL) grace_dir.mkdir(mode=MODE, parents=True, exist_ok=True) # create log file with list of synchronized files (or print to terminal) if LOG: # output to log file # format: GFZ_AOD1B_sync_2002-04-01.log - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) LOGFILE = base_dir.joinpath(f'GFZ_AOD1B_sync_{today}.log') logging.basicConfig(filename=LOGFILE, level=logging.INFO) logging.info(f'GFZ AOD1b Sync Log ({today})') @@ -98,14 +109,19 @@ def gfz_isdc_dealiasing_sync(base_dir, DREL, YEAR=None, MONTHS=None, TAR=False, SUFFIX = dict(RL04='tar.gz', RL05='tar.gz', RL06='tgz') # find remote yearly directories for DREL - YRS,_ = http_list([HOST,'grace','Level-1B', 'GFZ','AOD',DREL], - timeout=TIMEOUT, basename=True, pattern=R1, sort=True) + YRS, _ = http_list( + [HOST, 'grace', 'Level-1B', 'GFZ', 'AOD', DREL], + timeout=TIMEOUT, + basename=True, + pattern=R1, + sort=True, + ) # for each year for Y in YRS: # for each month of interest for M in MONTHS: # output tar file for year and month - args = (Y, M, DREL.replace('RL',''), SUFFIX[DREL]) + args = (Y, M, DREL.replace('RL', ''), SUFFIX[DREL]) FILE = 'AOD1B_{0}-{1:02d}_{2}.{3}'.format(*args) # check if output tar file exists (if TAR) local_tar_file = grace_dir.joinpath(FILE) @@ -113,22 +129,36 @@ def gfz_isdc_dealiasing_sync(base_dir, DREL, YEAR=None, MONTHS=None, TAR=False, # compile regular expressions operators for file dates # will extract year and month and calendar day from the ascii file regex_pattern = r'AOD1B_({0})-({1:02d})-(\d+)_X_\d+.asc.gz$' - R2 = re.compile(regex_pattern.format(Y,M), re.VERBOSE) - remote_files,remote_mtimes = http_list( - [HOST,'grace','Level-1B','GFZ','AOD',DREL,Y], - timeout=TIMEOUT, basename=True, pattern=R2, sort=True) + R2 = re.compile(regex_pattern.format(Y, M), re.VERBOSE) + remote_files, remote_mtimes = http_list( + [HOST, 'grace', 'Level-1B', 'GFZ', 'AOD', DREL, Y], + timeout=TIMEOUT, + basename=True, + pattern=R2, + sort=True, + ) file_count = len(remote_files) # if compressing into monthly tar files if TAR and (file_count > 0) and (TEST or CLOBBER): # copy each gzip file and store within monthly tar files tar = tarfile.open(name=local_tar_file, mode='w:gz') - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # remote version of each input file - remote = [HOST,'grace','Level-1B','GFZ','AOD',DREL,Y,fi] + remote = [ + HOST, + 'grace', + 'Level-1B', + 'GFZ', + 'AOD', + DREL, + Y, + fi, + ] logging.info(posixpath.join(*remote)) # retrieve bytes from remote file - remote_buffer = gravtk.utilities.from_sync(remote, - timeout=TIMEOUT) + remote_buffer = gravtk.utilities.from_sync( + remote, timeout=TIMEOUT + ) # add file to tar tar_info = tarfile.TarInfo(name=fi) tar_info.mtime = remote_mtime @@ -140,25 +170,40 @@ def gfz_isdc_dealiasing_sync(base_dir, DREL, YEAR=None, MONTHS=None, TAR=False, local_tar_file.chmod(mode=MODE) elif (file_count > 0) and not TAR: # copy each gzip file and keep as individual daily files - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # remote and local version of each input file - remote = [HOST,'grace','Level-1B','GFZ','AOD',DREL,Y,fi] + remote = [ + HOST, + 'grace', + 'Level-1B', + 'GFZ', + 'AOD', + DREL, + Y, + fi, + ] local_file = grace_dir.joinpath(fi) - http_pull_file(remote,remote_mtime,local_file, - CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote, + remote_mtime, + local_file, + CLOBBER=CLOBBER, + MODE=MODE, + ) # close log file and set permissions level to MODE if LOG: LOGFILE.chmod(mode=MODE) + # PURPOSE: list a directory on the GFZ https server def http_list( - HOST: str | list, - timeout: int | None = None, - context: ssl.SSLContext = gravtk.utilities._default_ssl_context, - pattern: str | re.Pattern = '', - sort: bool = False - ): + HOST: str | list, + timeout: int | None = None, + context: ssl.SSLContext = gravtk.utilities._default_ssl_context, + pattern: str | re.Pattern = '', + sort: bool = False, +): """ List a directory on the GFZ https Server @@ -192,8 +237,9 @@ def http_list( try: # Create and submit request. request = gravtk.utilities.urllib2.Request(posixpath.join(*HOST)) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=timeout, context=context) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=timeout, context=context + ) except Exception as exc: raise Exception('List error from {0}'.format(posixpath.join(*HOST))) # read the directory listing @@ -201,32 +247,41 @@ def http_list( # read and parse request for files (column names and modified times) lines = [l for l in contents if rx.search(l.decode('utf-8'))] # column names and last modified times - colnames = [None]*len(lines) - collastmod = [None]*len(lines) + colnames = [None] * len(lines) + collastmod = [None] * len(lines) for i, l in enumerate(lines): colnames[i], lastmod = rx.findall(l.decode('utf-8')).pop() # get the Unix timestamp value for a modification time - collastmod[i] = gravtk.utilities.get_unix_time(lastmod, - format='%Y-%m-%d %H:%M') + collastmod[i] = gravtk.utilities.get_unix_time( + lastmod, format='%Y-%m-%d %H:%M' + ) # reduce using regular expression pattern if pattern: - i = [i for i,f in enumerate(colnames) if re.search(pattern, f)] + i = [i for i, f in enumerate(colnames) if re.search(pattern, f)] # reduce list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # sort the list if sort: - i = [i for i,j in sorted(enumerate(colnames), key=lambda i: i[1])] + i = [i for i, j in sorted(enumerate(colnames), key=lambda i: i[1])] # sort list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # return the list of column names and last modified times return (colnames, collastmod) + # PURPOSE: pull file from a remote host checking if file exists locally # and if the remote file is newer than the local file -def http_pull_file(remote_path, remote_mtime, local_file, - TIMEOUT=0, LIST=False, CLOBBER=False, MODE=0o775): +def http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=0, + LIST=False, + CLOBBER=False, + MODE=0o775, +): # verify inputs for remote http host if isinstance(remote_path, str): remote_path = gravtk.utilities.url_split(remote_path) @@ -241,8 +296,9 @@ def http_pull_file(remote_path, remote_mtime, local_file, # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -258,8 +314,9 @@ def http_pull_file(remote_path, remote_mtime, local_file, # Create and submit request. There are a wide range of exceptions # that can be thrown here, including HTTPError and URLError. request = gravtk.utilities.urllib2.Request(remote_file) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=TIMEOUT + ) # chunked transfer encoding size CHUNK = 16 * 1024 # copy contents to local file using chunked transfer encoding @@ -270,6 +327,7 @@ def http_pull_file(remote_path, remote_mtime, local_file, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -279,65 +337,113 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, nargs='+', - default=['RL06'], choices=['RL04','RL05','RL06'], - help='GRACE/GRACE-FO data release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + nargs='+', + default=['RL06'], + choices=['RL04', 'RL05', 'RL06'], + help='GRACE/GRACE-FO data release', + ) # years to download - parser.add_argument('--year','-Y', - type=int, nargs='+', default=range(2000,2023), - help='Years of data to sync') + parser.add_argument( + '--year', + '-Y', + type=int, + nargs='+', + default=range(2000, 2023), + help='Years of data to sync', + ) # months to download - parser.add_argument('--month','-m', - type=int, nargs='+', default=range(1,13), - help='Months of data to sync') + parser.add_argument( + '--month', + '-m', + type=int, + nargs='+', + default=range(1, 13), + help='Months of data to sync', + ) # output dealiasing files as monthly tar files - parser.add_argument('--tar','-T', - default=False, action='store_true', - help='Output data as monthly tar files') + parser.add_argument( + '--tar', + '-T', + default=False, + action='store_true', + help='Output data as monthly tar files', + ) # connection timeout - parser.add_argument('--timeout','-t', - type=int, default=360, - help='Timeout in seconds for blocking operations') + parser.add_argument( + '--timeout', + '-t', + type=int, + default=360, + help='Timeout in seconds for blocking operations', + ) # Output log file in form # GFZ_AOD1B_sync_2002-04-01.log - parser.add_argument('--log','-l', - default=False, action='store_true', - help='Output log file') + parser.add_argument( + '--log', + '-l', + default=False, + action='store_true', + help='Output log file', + ) # sync options - parser.add_argument('--clobber','-C', - default=False, action='store_true', - help='Overwrite existing data in transfer') + parser.add_argument( + '--clobber', + '-C', + default=False, + action='store_true', + help='Overwrite existing data in transfer', + ) # permissions mode of the directories and files synced (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permission mode of directories and files synced') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permission mode of directories and files synced', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # GFZ ISDC https host HOST = 'https://isdc-data.gfz.de/' # check internet connection before attempting to run program if gravtk.utilities.check_connection(HOST): for DREL in args.release: - gfz_isdc_dealiasing_sync(args.directory, DREL=DREL, - YEAR=args.year, MONTHS=args.month, TAR=args.tar, - TIMEOUT=args.timeout, LOG=args.log, - CLOBBER=args.clobber, MODE=args.mode) + gfz_isdc_dealiasing_sync( + args.directory, + DREL=DREL, + YEAR=args.year, + MONTHS=args.month, + TAR=args.tar, + TIMEOUT=args.timeout, + LOG=args.log, + CLOBBER=args.clobber, + MODE=args.mode, + ) else: raise RuntimeError('Check internet connection') + # run main program if __name__ == '__main__': main() diff --git a/access/gfz_isdc_grace_sync.py b/access/gfz_isdc_grace_sync.py index f8f7e618..1106da52 100644 --- a/access/gfz_isdc_grace_sync.py +++ b/access/gfz_isdc_grace_sync.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gfz_isdc_grace_sync.py Written by Tyler Sutterley (10/2025) Syncs GRACE/GRACE-FO data from the GFZ Information System and Data Center (ISDC) @@ -59,6 +59,7 @@ added GRACE Follow-On data sync Written 08/2018 """ + from __future__ import print_function import sys @@ -74,11 +75,20 @@ import posixpath import gravity_toolkit as gravtk -# PURPOSE: sync local GRACE/GRACE-FO files with GFZ ISDC server -def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], - NEWSLETTERS=False, TIMEOUT=None, LOG=False, LIST=False, - CLOBBER=False, MODE=None): +# PURPOSE: sync local GRACE/GRACE-FO files with GFZ ISDC server +def gfz_isdc_grace_sync( + DIRECTORY, + PROC=[], + DREL=[], + VERSION=[], + NEWSLETTERS=False, + TIMEOUT=None, + LOG=False, + LIST=False, + CLOBBER=False, + MODE=None, +): # check if directory exists and recursively create if not DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) @@ -86,7 +96,7 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # GFZ ISDC https host HOST = 'https://isdc-data.gfz.de/' # mission shortnames - shortname = {'grace':'GRAC', 'grace-fo':'GRFO'} + shortname = {'grace': 'GRAC', 'grace-fo': 'GRFO'} # datasets for each processing center DSET = {} DSET['CSR'] = ['GAC', 'GAD', 'GSM'] @@ -97,7 +107,7 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], if LOG: # output to log file # format: GFZ_ISDC_sync_2002-04-01.log - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) LOGFILE = DIRECTORY.joinpath(f'GFZ_ISDC_sync_{today}.log') logging.basicConfig(filename=LOGFILE, level=logging.INFO) logging.info(f'GFZ ISDC Sync Log ({today})') @@ -116,51 +126,78 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # compile regular expression operator for remote files R1 = re.compile(r'TN-13_GEOC_(CSR|GFZ|JPL)_(.*?).txt$', re.VERBOSE) # get filenames from remote directory - remote_files,remote_mtimes = http_list( - [HOST,'grace-fo','DOCUMENTS','TECHNICAL_NOTES'], - timeout=TIMEOUT, pattern=R1, sort=True) + remote_files, remote_mtimes = http_list( + [HOST, 'grace-fo', 'DOCUMENTS', 'TECHNICAL_NOTES'], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) # for each file on the remote server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,'grace-fo','DOCUMENTS','TECHNICAL_NOTES',fi] + remote_path = [HOST, 'grace-fo', 'DOCUMENTS', 'TECHNICAL_NOTES', fi] local_file = local_dir.joinpath(fi) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # SLR C2,0 coefficients logging.info('C2,0 Coefficients:') # compile regular expression operator for remote files R1 = re.compile(r'TN-(05|07|11)_C20_SLR_RL(.*?).txt$', re.VERBOSE) # get filenames from remote directory - remote_files,remote_mtimes = http_list( - [HOST,'grace','DOCUMENTS','TECHNICAL_NOTES'], - timeout=TIMEOUT, pattern=R1, sort=True) + remote_files, remote_mtimes = http_list( + [HOST, 'grace', 'DOCUMENTS', 'TECHNICAL_NOTES'], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) # for each file on the remote server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,'grace','DOCUMENTS','TECHNICAL_NOTES',fi] - local_file = DIRECTORY.joinpath(re.sub(r'(_RL.*?).txt','.txt',fi)) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + remote_path = [HOST, 'grace', 'DOCUMENTS', 'TECHNICAL_NOTES', fi] + local_file = DIRECTORY.joinpath(re.sub(r'(_RL.*?).txt', '.txt', fi)) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # SLR C3,0 coefficients logging.info('C3,0 Coefficients:') # compile regular expression operator for remote files R1 = re.compile(r'TN-(14)_C30_C20_SLR_GSFC.txt$', re.VERBOSE) # get filenames from remote directory - remote_files,remote_mtimes = http_list( - [HOST,'grace-fo','DOCUMENTS','TECHNICAL_NOTES'], - timeout=TIMEOUT, pattern=R1, sort=True) + remote_files, remote_mtimes = http_list( + [HOST, 'grace-fo', 'DOCUMENTS', 'TECHNICAL_NOTES'], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) # for each file on the remote server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,'grace-fo','DOCUMENTS','TECHNICAL_NOTES',fi] - local_file = DIRECTORY.joinpath(re.sub(r'(SLR_GSFC)','GSFC_SLR',fi)) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + remote_path = [HOST, 'grace-fo', 'DOCUMENTS', 'TECHNICAL_NOTES', fi] + local_file = DIRECTORY.joinpath(re.sub(r'(SLR_GSFC)', 'GSFC_SLR', fi)) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # TN-08 GAE, TN-09 GAF and TN-10 GAG ECMWF atmosphere correction products logging.info('TN-08 GAE, TN-09 GAF and TN-10 GAG products:') @@ -171,17 +208,26 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # compile regular expression operator for remote files R1 = re.compile(r'({0}|{1}|{2})'.format(*ECMWF_files), re.VERBOSE) # get filenames from remote directory - remote_files,remote_mtimes = http_list( - [HOST,'grace','DOCUMENTS','TECHNICAL_NOTES'], - timeout=TIMEOUT, pattern=R1, sort=True) + remote_files, remote_mtimes = http_list( + [HOST, 'grace', 'DOCUMENTS', 'TECHNICAL_NOTES'], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) # for each file on the remote server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,'grace','DOCUMENTS','TECHNICAL_NOTES',fi] + remote_path = [HOST, 'grace', 'DOCUMENTS', 'TECHNICAL_NOTES', fi] local_file = DIRECTORY.joinpath(fi) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # GRACE and GRACE-FO newsletters if NEWSLETTERS: @@ -190,30 +236,41 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # check if newsletters directory exists and recursively create if not local_dir.mkdir(mode=MODE, parents=True, exist_ok=True) # for each satellite mission (grace, grace-fo) - for i,mi in enumerate(['grace','grace-fo']): + for i, mi in enumerate(['grace', 'grace-fo']): logging.info(f'{mi} Newsletters:') # compile regular expression operator for remote files - NAME = mi.upper().replace('-','_') + NAME = mi.upper().replace('-', '_') R1 = re.compile(rf'{NAME}_SDS_NL_(\d+).pdf', re.VERBOSE) # find years for GRACE/GRACE-FO newsletters - years,_ = http_list([HOST,mi,'DOCUMENTS','NEWSLETTER'], - timeout=TIMEOUT, pattern=r'\d+', - sort=True) + years, _ = http_list( + [HOST, mi, 'DOCUMENTS', 'NEWSLETTER'], + timeout=TIMEOUT, + pattern=r'\d+', + sort=True, + ) # for each year of GRACE/GRACE-FO newsletters for Y in years: # find GRACE/GRACE-FO newsletters - remote_files,remote_mtimes = http_list( - [HOST,mi,'DOCUMENTS','NEWSLETTER',Y], - timeout=TIMEOUT, pattern=R1, - sort=True) + remote_files, remote_mtimes = http_list( + [HOST, mi, 'DOCUMENTS', 'NEWSLETTER', Y], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) # for each file on the remote server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,mi,'DOCUMENTS','NEWSLETTER',Y,fi] + remote_path = [HOST, mi, 'DOCUMENTS', 'NEWSLETTER', Y, fi] local_file = local_dir.joinpath(fi) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # GRACE/GRACE-FO level-2 spherical harmonic products logging.info('GRACE/GRACE-FO L2 Global Spherical Harmonics:') @@ -230,9 +287,9 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # list of GRACE/GRACE-FO files for index grace_files = [] # for each satellite mission (grace, grace-fo) - for i,mi in enumerate(['grace','grace-fo']): + for i, mi in enumerate(['grace', 'grace-fo']): # modifiers for intermediate data releases - if (int(VERSION[i]) > 0): + if int(VERSION[i]) > 0: drel_str = f'{rl}.{VERSION[i]}' else: drel_str = copy.copy(rl) @@ -241,22 +298,37 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # compile the regular expression operator to find files R1 = re.compile(rf'({ds}-(.*?)(gz|txt|dif))') # get filenames from remote directory - remote_files,remote_mtimes = http_list( - [HOST,mi,'Level-2',pr,drel_str], timeout=TIMEOUT, - pattern=R1, sort=True) - for fi,remote_mtime in zip(remote_files,remote_mtimes): + remote_files, remote_mtimes = http_list( + [HOST, mi, 'Level-2', pr, drel_str], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,mi,'Level-2',pr,drel_str,fi] + remote_path = [HOST, mi, 'Level-2', pr, drel_str, fi] local_file = local_dir.joinpath(fi) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # regular expression operator for data product rx = gravtk.utilities.compile_regex_pattern( - pr, rl, ds, mission=shortname[mi]) + pr, rl, ds, mission=shortname[mi] + ) # find local GRACE/GRACE-FO files to create index - granules = sorted([f.name for f in local_dir.iterdir() - if rx.match(f.name)]) + granules = sorted( + [ + f.name + for f in local_dir.iterdir() + if rx.match(f.name) + ] + ) # reduce list of GRACE/GRACE-FO files to unique dates granules = gravtk.time.reduce_by_date(granules) # extend list of GRACE/GRACE-FO files with granules @@ -274,14 +346,15 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], if LOG: LOGFILE.chmod(mode=MODE) + # PURPOSE: list a directory on the GFZ https server def http_list( - HOST: str | list, - timeout: int | None = None, - context: ssl.SSLContext = gravtk.utilities._default_ssl_context, - pattern: str | re.Pattern = '', - sort: bool = False - ): + HOST: str | list, + timeout: int | None = None, + context: ssl.SSLContext = gravtk.utilities._default_ssl_context, + pattern: str | re.Pattern = '', + sort: bool = False, +): """ List a directory on the GFZ https Server @@ -315,8 +388,9 @@ def http_list( try: # Create and submit request. request = gravtk.utilities.urllib2.Request(posixpath.join(*HOST)) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=timeout, context=context) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=timeout, context=context + ) except Exception as exc: raise Exception('List error from {0}'.format(posixpath.join(*HOST))) # read the directory listing @@ -324,32 +398,41 @@ def http_list( # read and parse request for files (column names and modified times) lines = [l for l in contents if rx.search(l.decode('utf-8'))] # column names and last modified times - colnames = [None]*len(lines) - collastmod = [None]*len(lines) + colnames = [None] * len(lines) + collastmod = [None] * len(lines) for i, l in enumerate(lines): colnames[i], lastmod = rx.findall(l.decode('utf-8')).pop() # get the Unix timestamp value for a modification time - collastmod[i] = gravtk.utilities.get_unix_time(lastmod, - format='%Y-%m-%d %H:%M') + collastmod[i] = gravtk.utilities.get_unix_time( + lastmod, format='%Y-%m-%d %H:%M' + ) # reduce using regular expression pattern if pattern: - i = [i for i,f in enumerate(colnames) if re.search(pattern, f)] + i = [i for i, f in enumerate(colnames) if re.search(pattern, f)] # reduce list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # sort the list if sort: - i = [i for i,j in sorted(enumerate(colnames), key=lambda i: i[1])] + i = [i for i, j in sorted(enumerate(colnames), key=lambda i: i[1])] # sort list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # return the list of column names and last modified times return (colnames, collastmod) + # PURPOSE: pull file from a remote host checking if file exists locally # and if the remote file is newer than the local file -def http_pull_file(remote_path, remote_mtime, local_file, - TIMEOUT=0, LIST=False, CLOBBER=False, MODE=0o775): +def http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=0, + LIST=False, + CLOBBER=False, + MODE=0o775, +): # verify inputs for remote http host if isinstance(remote_path, str): remote_path = gravtk.utilities.url_split(remote_path) @@ -364,8 +447,9 @@ def http_pull_file(remote_path, remote_mtime, local_file, # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -381,8 +465,9 @@ def http_pull_file(remote_path, remote_mtime, local_file, # Create and submit request. There are a wide range of exceptions # that can be thrown here, including HTTPError and URLError. request = gravtk.utilities.urllib2.Request(remote_file) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=TIMEOUT + ) # chunked transfer encoding size CHUNK = 16 * 1024 # copy contents to local file using chunked transfer encoding @@ -393,6 +478,7 @@ def http_pull_file(remote_path, remote_mtime, local_file, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -402,70 +488,123 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, nargs='+', - default=['CSR','GFZ','JPL'], choices=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO processing center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + nargs='+', + default=['CSR', 'GFZ', 'JPL'], + choices=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO processing center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, nargs='+', - default=['RL06'], choices=['RL04','RL05','RL06'], - help='GRACE/GRACE-FO data release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + nargs='+', + default=['RL06'], + choices=['RL04', 'RL05', 'RL06'], + help='GRACE/GRACE-FO data release', + ) # GRACE/GRACE-FO data version - parser.add_argument('--version','-v', - metavar='VERSION', type=str, nargs=2, - default=['0','1'], - help='GRACE/GRACE-FO Level-2 data version') + parser.add_argument( + '--version', + '-v', + metavar='VERSION', + type=str, + nargs=2, + default=['0', '1'], + help='GRACE/GRACE-FO Level-2 data version', + ) # GRACE/GRACE-FO newsletters - parser.add_argument('--newsletters','-n', - default=False, action='store_true', - help='Sync GRACE/GRACE-FO Newsletters') + parser.add_argument( + '--newsletters', + '-n', + default=False, + action='store_true', + help='Sync GRACE/GRACE-FO Newsletters', + ) # connection timeout - parser.add_argument('--timeout','-t', - type=int, default=360, - help='Timeout in seconds for blocking operations') + parser.add_argument( + '--timeout', + '-t', + type=int, + default=360, + help='Timeout in seconds for blocking operations', + ) # Output log file in form # GFZ_ISDC_sync_2002-04-01.log - parser.add_argument('--log','-l', - default=False, action='store_true', - help='Output log file') + parser.add_argument( + '--log', + '-l', + default=False, + action='store_true', + help='Output log file', + ) # sync options - parser.add_argument('--list','-L', - default=False, action='store_true', - help='Only print files that could be transferred') - parser.add_argument('--clobber','-C', - default=False, action='store_true', - help='Overwrite existing data in transfer') + parser.add_argument( + '--list', + '-L', + default=False, + action='store_true', + help='Only print files that could be transferred', + ) + parser.add_argument( + '--clobber', + '-C', + default=False, + action='store_true', + help='Overwrite existing data in transfer', + ) # permissions mode of the directories and files synced (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permission mode of directories and files synced') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permission mode of directories and files synced', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # GFZ ISDC https host HOST = 'https://isdc-data.gfz.de/' # check internet connection before attempting to run program if gravtk.utilities.check_connection(HOST): - gfz_isdc_grace_sync(args.directory, PROC=args.center, - DREL=args.release, VERSION=args.version, - NEWSLETTERS=args.newsletters, TIMEOUT=args.timeout, - LIST=args.list, LOG=args.log, CLOBBER=args.clobber, - MODE=args.mode) + gfz_isdc_grace_sync( + args.directory, + PROC=args.center, + DREL=args.release, + VERSION=args.version, + NEWSLETTERS=args.newsletters, + TIMEOUT=args.timeout, + LIST=args.list, + LOG=args.log, + CLOBBER=args.clobber, + MODE=args.mode, + ) else: raise RuntimeError('Check internet connection') + # run main program if __name__ == '__main__': main() diff --git a/access/itsg_graz_grace_sync.py b/access/itsg_graz_grace_sync.py index 66dfa277..ec3a9423 100755 --- a/access/itsg_graz_grace_sync.py +++ b/access/itsg_graz_grace_sync.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" itsg_graz_grace_sync.py Written by Tyler Sutterley (07/2026) Syncs GRACE/GRACE-FO and auxiliary data from the ITSG GRAZ server @@ -47,6 +47,7 @@ Updated 10/2021: using python logging for handling verbose output Written 09/2021 """ + from __future__ import print_function import sys @@ -60,10 +61,18 @@ import posixpath import gravity_toolkit as gravtk -# PURPOSE: sync local GRACE/GRACE-FO files with ITSG GRAZ server -def itsg_graz_grace_sync(DIRECTORY, RELEASE=None, LMAX=None, TIMEOUT=0, - LOG=False, LIST=False, MODE=0o775, CLOBBER=False): +# PURPOSE: sync local GRACE/GRACE-FO files with ITSG GRAZ server +def itsg_graz_grace_sync( + DIRECTORY, + RELEASE=None, + LMAX=None, + TIMEOUT=0, + LOG=False, + LIST=False, + MODE=0o775, + CLOBBER=False, +): # check if directory exists and recursively create if not DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) @@ -72,7 +81,7 @@ def itsg_graz_grace_sync(DIRECTORY, RELEASE=None, LMAX=None, TIMEOUT=0, if LOG: # output to log file # format: ITSG_GRAZ_GRACE_sync_2002-04-01.log - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) LOGFILE = DIRECTORY.joinpath(f'ITSG_GRAZ_GRACE_sync_{today}.log') logging.basicConfig(filename=LOGFILE, level=logging.INFO) logging.info(f'ITSG GRAZ GRACE Sync Log ({today})') @@ -83,7 +92,7 @@ def itsg_graz_grace_sync(DIRECTORY, RELEASE=None, LMAX=None, TIMEOUT=0, logging.basicConfig(level=logging.INFO) # ITSG GRAZ server - HOST = ['http://ftp.tugraz.at','pub','ITSG','GRACE'] + HOST = ['http://ftp.tugraz.at', 'pub', 'ITSG', 'GRACE'] # open connection with ITSG GRAZ server at remote directory release_directory = f'ITSG-{RELEASE}' # regular expression operators for ITSG data and models @@ -96,8 +105,10 @@ def itsg_graz_grace_sync(DIRECTORY, RELEASE=None, LMAX=None, TIMEOUT=0, itsg_products.append(r'Grace2016') itsg_products.append(r'Grace2018') itsg_products.append(r'Grace_operational') - itsg_pattern = (r'(AOD1B_RL\d+|model|ITSG)[-_]({0})(_n\d+)?_' - r'(\d+)-(\d+)(\.gfc)').format(r'|'.join(itsg_products)) + itsg_pattern = ( + r'(AOD1B_RL\d+|model|ITSG)[-_]({0})(_n\d+)?_' + r'(\d+)-(\d+)(\.gfc)' + ).format(r'|'.join(itsg_products)) R1 = re.compile(itsg_pattern, re.VERBOSE | re.IGNORECASE) # local directory for release DREL = {} @@ -114,52 +125,69 @@ def itsg_graz_grace_sync(DIRECTORY, RELEASE=None, LMAX=None, TIMEOUT=0, # sync ITSG GRAZ dealiasing products subdir = 'background' if (RELEASE == 'Grace2014') else 'monthly_background' - REMOTE = [*HOST,release_directory,'monthly',subdir] - files,mtimes = gravtk.utilities.http_list(REMOTE, - timeout=TIMEOUT,pattern=R1,sort=True) + REMOTE = [*HOST, release_directory, 'monthly', subdir] + files, mtimes = gravtk.utilities.http_list( + REMOTE, timeout=TIMEOUT, pattern=R1, sort=True + ) # for each file on the remote directory - for colname,remote_mtime in zip(files,mtimes): + for colname, remote_mtime in zip(files, mtimes): # extract parameters from input filename - PFX,PRD,trunc,year,month,SFX = R1.findall(colname).pop() + PFX, PRD, trunc, year, month, SFX = R1.findall(colname).pop() # local directory for output GRAZ data - local_dir = DIRECTORY.joinpath('GRAZ',DREL[RELEASE],DEALIASING[PRD]) + local_dir = DIRECTORY.joinpath('GRAZ', DREL[RELEASE], DEALIASING[PRD]) # check if local directory exists and recursively create if not local_dir.mkdir(mode=MODE, parents=True, exist_ok=True) # local and remote versions of the file local_file = local_dir.joinpath(colname) - remote_file = posixpath.join(*REMOTE,colname) + remote_file = posixpath.join(*REMOTE, colname) # copy file from remote directory comparing modified dates - http_pull_file(remote_file, remote_mtime, local_file, - TIMEOUT=TIMEOUT, LIST=LIST, CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote_file, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # sync ITSG GRAZ data for truncation subdir = f'monthly_n{LMAX:d}' - REMOTE = [*HOST,release_directory,'monthly',subdir] - files,mtimes = gravtk.utilities.http_list(REMOTE, - timeout=TIMEOUT,pattern=R1,sort=True) + REMOTE = [*HOST, release_directory, 'monthly', subdir] + files, mtimes = gravtk.utilities.http_list( + REMOTE, timeout=TIMEOUT, pattern=R1, sort=True + ) # local directory for output GRAZ data - local_dir = DIRECTORY.joinpath('GRAZ',DREL[RELEASE],'GSM') + local_dir = DIRECTORY.joinpath('GRAZ', DREL[RELEASE], 'GSM') # check if local directory exists and recursively create if not local_dir.mkdir(mode=MODE, parents=True, exist_ok=True) # for each file on the remote directory - for colname,remote_mtime in zip(files,mtimes): + for colname, remote_mtime in zip(files, mtimes): # local and remote versions of the file local_file = local_dir.joinpath(colname) - remote_file = posixpath.join(*REMOTE,colname) + remote_file = posixpath.join(*REMOTE, colname) # copy file from remote directory comparing modified dates - http_pull_file(remote_file, remote_mtime, local_file, - TIMEOUT=TIMEOUT, LIST=LIST, CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote_file, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # create index file for GRACE/GRACE-FO L2 Spherical Harmonic Data # DATA PRODUCTS (GAC, GAD, GSM, GAA, GAB) - for ds in ['GAA','GAB','GAC','GAD','GSM']: + for ds in ['GAA', 'GAB', 'GAC', 'GAD', 'GSM']: # local directory for exact data product - local_dir = DIRECTORY.joinpath('GRAZ',DREL[RELEASE],ds) + local_dir = DIRECTORY.joinpath('GRAZ', DREL[RELEASE], ds) if not local_dir.exists(): continue # find local GRACE files to create index - grace_files = sorted([f.name for f in local_dir.iterdir() - if R1.match(f.name)]) + grace_files = sorted( + [f.name for f in local_dir.iterdir() if R1.match(f.name)] + ) # outputting GRACE filenames to index index_file = local_dir.joinpath('index.txt') with index_file.open(mode='w', encoding='utf8') as fid: @@ -172,10 +200,18 @@ def itsg_graz_grace_sync(DIRECTORY, RELEASE=None, LMAX=None, TIMEOUT=0, if LOG: LOGFILE.chmod(mode=MODE) + # PURPOSE: pull file from a remote host checking if file exists locally # and if the remote file is newer than the local file -def http_pull_file(remote_file,remote_mtime,local_file, - TIMEOUT=0,LIST=False,CLOBBER=False,MODE=0o775): +def http_pull_file( + remote_file, + remote_mtime, + local_file, + TIMEOUT=0, + LIST=False, + CLOBBER=False, + MODE=0o775, +): # if file exists in file system: check if remote file is newer TEST = False OVERWRITE = ' (clobber)' @@ -185,8 +221,9 @@ def http_pull_file(remote_file,remote_mtime,local_file, # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -202,8 +239,9 @@ def http_pull_file(remote_file,remote_mtime,local_file, # Create and submit request. There are a wide range of exceptions # that can be thrown here, including HTTPError and URLError. request = gravtk.utilities.urllib2.Request(remote_file) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=TIMEOUT + ) # chunked transfer encoding size CHUNK = 16 * 1024 # copy contents to local file using chunked transfer encoding @@ -214,6 +252,7 @@ def http_pull_file(remote_file,remote_mtime,local_file, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -223,56 +262,98 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # ITSG GRAZ releases - choices = ['Grace2014','Grace2016','Grace2018','Grace_operational'] - parser.add_argument('--release','-r', - type=str, nargs='+', metavar='DREL', - default=['Grace2018','Grace_operational'],choices=choices, - help='GRAZ Data Releases to sync') - parser.add_argument('--lmax', - type=int, default=60, choices=[60,96,120], - help='Maximum degree and order of GRAZ products') + choices = ['Grace2014', 'Grace2016', 'Grace2018', 'Grace_operational'] + parser.add_argument( + '--release', + '-r', + type=str, + nargs='+', + metavar='DREL', + default=['Grace2018', 'Grace_operational'], + choices=choices, + help='GRAZ Data Releases to sync', + ) + parser.add_argument( + '--lmax', + type=int, + default=60, + choices=[60, 96, 120], + help='Maximum degree and order of GRAZ products', + ) # connection timeout - parser.add_argument('--timeout','-t', - type=int, default=360, - help='Timeout in seconds for blocking operations') + parser.add_argument( + '--timeout', + '-t', + type=int, + default=360, + help='Timeout in seconds for blocking operations', + ) # Output log file in form # ITSG_GRAZ_GRACE_sync_2002-04-01.log - parser.add_argument('--log','-l', - default=False, action='store_true', - help='Output log file') + parser.add_argument( + '--log', + '-l', + default=False, + action='store_true', + help='Output log file', + ) # sync options - parser.add_argument('--list','-L', - default=False, action='store_true', - help='Only print files that could be transferred') - parser.add_argument('--clobber','-C', - default=False, action='store_true', - help='Overwrite existing data in transfer') + parser.add_argument( + '--list', + '-L', + default=False, + action='store_true', + help='Only print files that could be transferred', + ) + parser.add_argument( + '--clobber', + '-C', + default=False, + action='store_true', + help='Overwrite existing data in transfer', + ) # permissions mode of the directories and files synced (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permission mode of directories and files synced') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permission mode of directories and files synced', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # check internet connection before attempting to run program HOST = posixpath.join('http://ftp.tugraz.at') if gravtk.utilities.check_connection(HOST): # for each ITSG GRAZ release for RELEASE in args.release: - itsg_graz_grace_sync(args.directory, RELEASE=RELEASE, - LMAX=args.lmax, TIMEOUT=args.timeout, LOG=args.log, - LIST=args.list, CLOBBER=args.clobber, MODE=args.mode) + itsg_graz_grace_sync( + args.directory, + RELEASE=RELEASE, + LMAX=args.lmax, + TIMEOUT=args.timeout, + LOG=args.log, + LIST=args.list, + CLOBBER=args.clobber, + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/access/podaac_cumulus.py b/access/podaac_cumulus.py index d448850a..a49f445c 100644 --- a/access/podaac_cumulus.py +++ b/access/podaac_cumulus.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" podaac_cumulus.py Written by Tyler Sutterley (11/2024) @@ -70,6 +70,7 @@ use argparse descriptions within sphinx documentation Written 03/2022 with release of PO.DAAC Cumulus """ + from __future__ import print_function import sys @@ -83,17 +84,28 @@ import argparse import gravity_toolkit as gravtk -# PURPOSE: sync local GRACE/GRACE-FO files with JPL PO.DAAC AWS S3 bucket -def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], - AOD1B=False, ENDPOINT='s3', TIMEOUT=None, GZIP=False, LOG=False, - CLOBBER=False, MODE=None): +# PURPOSE: sync local GRACE/GRACE-FO files with JPL PO.DAAC AWS S3 bucket +def podaac_cumulus( + client, + DIRECTORY, + PROC=[], + DREL=[], + VERSION=[], + AOD1B=False, + ENDPOINT='s3', + TIMEOUT=None, + GZIP=False, + LOG=False, + CLOBBER=False, + MODE=None, +): # check if directory exists and recursively create if not DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) # mission shortnames - shortname = {'grace':'GRAC', 'grace-fo':'GRFO'} + shortname = {'grace': 'GRAC', 'grace-fo': 'GRFO'} # default bucket for GRACE/GRACE-FO bucket bucket = gravtk.utilities._s3_buckets['podaac'] # datasets for each processing center @@ -135,13 +147,17 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], for version in set(VERSION): # query CMR for product metadata urls = gravtk.utilities.cmr_metadata( - mission='grace-fo', center=pr, release=rl, - version=version, provider='POCLOUD', - endpoint='documentation') + mission='grace-fo', + center=pr, + release=rl, + version=version, + provider='POCLOUD', + endpoint='documentation', + ) # TN-13 JPL degree 1 files try: - url, = [url for url in urls if R1.search(url)] + (url,) = [url for url in urls if R1.search(url)] except ValueError as exc: logging.info('No TN-13 Files Available') url = None @@ -150,18 +166,25 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], local_file = local_dir.joinpath(granule) # access auxiliary data from endpoint if (ENDPOINT == 'data') and (url is not None): - http_pull_file(url, mtime, local_file, - TIMEOUT=TIMEOUT, CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + url, + mtime, + local_file, + TIMEOUT=TIMEOUT, + CLOBBER=CLOBBER, + MODE=MODE, + ) elif (ENDPOINT == 's3') and (url is not None): bucket = gravtk.utilities.s3_bucket(url) key = gravtk.utilities.s3_key(url) response = client.get_object(Bucket=bucket, Key=key) - s3_pull_file(response, mtime, local_file, - CLOBBER=CLOBBER, MODE=MODE) + s3_pull_file( + response, mtime, local_file, CLOBBER=CLOBBER, MODE=MODE + ) # TN-14 SLR C2,0 and C3,0 files try: - url, = [url for url in urls if R2.search(url)] + (url,) = [url for url in urls if R2.search(url)] except ValueError as exc: logging.info('No TN-14 Files Available') url = None @@ -170,14 +193,21 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], local_file = DIRECTORY.joinpath(granule) # access auxiliary data from endpoint if (ENDPOINT == 'data') and (url is not None): - http_pull_file(url, mtime, local_file, - TIMEOUT=TIMEOUT, CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + url, + mtime, + local_file, + TIMEOUT=TIMEOUT, + CLOBBER=CLOBBER, + MODE=MODE, + ) elif (ENDPOINT == 's3') and (url is not None): bucket = gravtk.utilities.s3_bucket(url) key = gravtk.utilities.s3_key(url) response = client.get_object(Bucket=bucket, Key=key) - s3_pull_file(response, mtime, local_file, - CLOBBER=CLOBBER, MODE=MODE) + s3_pull_file( + response, mtime, local_file, CLOBBER=CLOBBER, MODE=MODE + ) # GRACE/GRACE-FO AOD1B dealiasing products if AOD1B: @@ -187,41 +217,56 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], # print string of exact data product logging.info(f'GFZ/AOD1B/{rl}') # local directory for exact data product - local_dir = DIRECTORY.joinpath('AOD1B',rl) + local_dir = DIRECTORY.joinpath('AOD1B', rl) # check if directory exists and recursively create if not local_dir.mkdir(mode=MODE, parents=True, exist_ok=True) # test connection to s3 bucket - if (ENDPOINT == 's3'): + if ENDPOINT == 's3': # get shortname for CMR query - cmr_shortname, = gravtk.utilities.cmr_product_shortname( - mission='grace', center='GFZ', release=rl, level='L1B') + (cmr_shortname,) = gravtk.utilities.cmr_product_shortname( + mission='grace', center='GFZ', release=rl, level='L1B' + ) # attempt to list objects in s3 bucket try: - objects = client.list_objects(Bucket=bucket, - Prefix=cmr_shortname) + objects = client.list_objects( + Bucket=bucket, Prefix=cmr_shortname + ) except Exception as exc: message = f'Error accessing S3 bucket {bucket}' raise Exception(message) from exc # query CMR for dataset - ids,urls,mtimes = gravtk.utilities.cmr( - mission='grace', level='L1B', center='GFZ', release=rl, - product='AOD1B', start_date='2002-01-01T00:00:00', - provider='POCLOUD', endpoint=ENDPOINT) + ids, urls, mtimes = gravtk.utilities.cmr( + mission='grace', + level='L1B', + center='GFZ', + release=rl, + product='AOD1B', + start_date='2002-01-01T00:00:00', + provider='POCLOUD', + endpoint=ENDPOINT, + ) # for each model id and url - for id,url,mtime in zip(ids,urls,mtimes): + for id, url, mtime in zip(ids, urls, mtimes): # retrieve GRACE/GRACE-FO files granule = gravtk.utilities.url_split(url)[-1] local_file = local_dir.joinpath(granule) # access data from endpoint - if (ENDPOINT == 'data'): - http_pull_file(url, mtime, local_file, - TIMEOUT=TIMEOUT, CLOBBER=CLOBBER, MODE=MODE) - elif (ENDPOINT == 's3'): + if ENDPOINT == 'data': + http_pull_file( + url, + mtime, + local_file, + TIMEOUT=TIMEOUT, + CLOBBER=CLOBBER, + MODE=MODE, + ) + elif ENDPOINT == 's3': bucket = gravtk.utilities.s3_bucket(url) key = gravtk.utilities.s3_key(url) response = client.get_object(Bucket=bucket, Key=key) - s3_pull_file(response, mtime, local_file, - CLOBBER=CLOBBER, MODE=MODE) + s3_pull_file( + response, mtime, local_file, CLOBBER=CLOBBER, MODE=MODE + ) # GRACE/GRACE-FO level-2 spherical harmonic products logging.info('GRACE/GRACE-FO L2 Global Spherical Harmonics:') @@ -238,49 +283,76 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], # list of GRACE/GRACE-FO files for index grace_files = [] # for each satellite mission (grace, grace-fo) - for i,mi in enumerate(['grace','grace-fo']): + for i, mi in enumerate(['grace', 'grace-fo']): # print string of exact data product logging.info(f'{mi} {pr}/{rl}/{ds}') # test connection to s3 bucket - if (ENDPOINT == 's3'): + if ENDPOINT == 's3': # get shortname for CMR query - cmr_shortname, = gravtk.utilities.cmr_product_shortname( - mission=mi, center=pr, release=rl, product=ds) + (cmr_shortname,) = ( + gravtk.utilities.cmr_product_shortname( + mission=mi, center=pr, release=rl, product=ds + ) + ) # attempt to list objects in s3 bucket try: - objects = client.list_objects(Bucket=bucket, - Prefix=cmr_shortname) + objects = client.list_objects( + Bucket=bucket, Prefix=cmr_shortname + ) except Exception as exc: message = f'Error accessing S3 bucket {bucket}' raise Exception(message) from exc # query CMR for dataset - ids,urls,mtimes = gravtk.utilities.cmr( - mission=mi, center=pr, release=rl, product=ds, - version=VERSION[i], provider='POCLOUD', - endpoint=ENDPOINT) + ids, urls, mtimes = gravtk.utilities.cmr( + mission=mi, + center=pr, + release=rl, + product=ds, + version=VERSION[i], + provider='POCLOUD', + endpoint=ENDPOINT, + ) # regular expression operator for data product rx = gravtk.utilities.compile_regex_pattern( - pr, rl, ds, mission=shortname[mi]) + pr, rl, ds, mission=shortname[mi] + ) # for each model id and url - for id,url,mtime in zip(ids,urls,mtimes): + for id, url, mtime in zip(ids, urls, mtimes): # retrieve GRACE/GRACE-FO files granule = gravtk.utilities.url_split(url)[-1] suffix = '.gz' if GZIP else '' local_file = local_dir.joinpath(f'{granule}{suffix}') # access data from endpoint - if (ENDPOINT == 'data'): - http_pull_file(url, mtime, local_file, - GZIP=GZIP, TIMEOUT=TIMEOUT, - CLOBBER=CLOBBER, MODE=MODE) - elif (ENDPOINT == 's3'): + if ENDPOINT == 'data': + http_pull_file( + url, + mtime, + local_file, + GZIP=GZIP, + TIMEOUT=TIMEOUT, + CLOBBER=CLOBBER, + MODE=MODE, + ) + elif ENDPOINT == 's3': bucket = gravtk.utilities.s3_bucket(url) key = gravtk.utilities.s3_key(url) response = client.get_object(Bucket=bucket, Key=key) - s3_pull_file(response, mtime, local_file, - GZIP=GZIP, CLOBBER=CLOBBER, MODE=MODE) + s3_pull_file( + response, + mtime, + local_file, + GZIP=GZIP, + CLOBBER=CLOBBER, + MODE=MODE, + ) # find local GRACE/GRACE-FO files to create index - granules = sorted([f.name for f in local_dir.iterdir() - if rx.match(f.name)]) + granules = sorted( + [ + f.name + for f in local_dir.iterdir() + if rx.match(f.name) + ] + ) # reduce list of GRACE/GRACE-FO files to unique dates granules = gravtk.time.reduce_by_date(granules) # extend list of GRACE/GRACE-FO files with granules @@ -298,10 +370,18 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], if LOG: LOGFILE.chmod(mode=MODE) + # PURPOSE: pull file from a remote host checking if file exists locally # and if the remote file is newer than the local file -def http_pull_file(remote_file, remote_mtime, local_file, - GZIP=False, TIMEOUT=120, CLOBBER=False, MODE=0o775): +def http_pull_file( + remote_file, + remote_mtime, + local_file, + GZIP=False, + TIMEOUT=120, + CLOBBER=False, + MODE=0o775, +): # if file exists in file system: check if remote file is newer TEST = False OVERWRITE = ' (clobber)' @@ -311,8 +391,9 @@ def http_pull_file(remote_file, remote_mtime, local_file, # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -329,8 +410,7 @@ def http_pull_file(remote_file, remote_mtime, local_file, # There are a range of exceptions that can be thrown here # including HTTPError and URLError. request = gravtk.utilities.urllib2.Request(remote_file) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen(request, timeout=TIMEOUT) # copy remote file contents to local file if GZIP: with gzip.GzipFile(local_file, 'wb', 9, None, remote_mtime) as f: @@ -342,10 +422,12 @@ def http_pull_file(remote_file, remote_mtime, local_file, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: pull file from AWS s3 bucket checking if file exists locally # and if the remote file is newer than the local file -def s3_pull_file(response, remote_mtime, local_file, - GZIP=False, CLOBBER=False, MODE=0o775): +def s3_pull_file( + response, remote_mtime, local_file, GZIP=False, CLOBBER=False, MODE=0o775 +): # if file exists in file system: check if remote file is newer TEST = False OVERWRITE = ' (clobber)' @@ -355,8 +437,9 @@ def s3_pull_file(response, remote_mtime, local_file, # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -379,6 +462,7 @@ def s3_pull_file(response, remote_mtime, local_file, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -388,72 +472,133 @@ def arguments(): ) # command line parameters # NASA Earthdata credentials - parser.add_argument('--user','-U', - type=str, default=os.environ.get('EARTHDATA_USERNAME'), - help='Username for NASA Earthdata Login') - parser.add_argument('--password','-W', - type=str, default=os.environ.get('EARTHDATA_PASSWORD'), - help='Password for NASA Earthdata Login') - parser.add_argument('--netrc','-N', - type=pathlib.Path, default=pathlib.Path.home().joinpath('.netrc'), - help='Path to .netrc file for authentication') + parser.add_argument( + '--user', + '-U', + type=str, + default=os.environ.get('EARTHDATA_USERNAME'), + help='Username for NASA Earthdata Login', + ) + parser.add_argument( + '--password', + '-W', + type=str, + default=os.environ.get('EARTHDATA_PASSWORD'), + help='Password for NASA Earthdata Login', + ) + parser.add_argument( + '--netrc', + '-N', + type=pathlib.Path, + default=pathlib.Path.home().joinpath('.netrc'), + help='Path to .netrc file for authentication', + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, nargs='+', - default=['CSR','GFZ','JPL'], choices=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO processing center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + nargs='+', + default=['CSR', 'GFZ', 'JPL'], + choices=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO processing center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, nargs='+', + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + nargs='+', default=['RL06'], - help='GRACE/GRACE-FO data release') + help='GRACE/GRACE-FO data release', + ) # GRACE/GRACE-FO data version - parser.add_argument('--version','-v', - metavar='VERSION', type=str, nargs=2, - default=['0','3'], - help='GRACE/GRACE-FO Level-2 data version') + parser.add_argument( + '--version', + '-v', + metavar='VERSION', + type=str, + nargs=2, + default=['0', '3'], + help='GRACE/GRACE-FO Level-2 data version', + ) # GRACE/GRACE-FO dealiasing products - parser.add_argument('--aod1b','-a', - default=False, action='store_true', - help='Sync GRACE/GRACE-FO Level-1B dealiasing products') + parser.add_argument( + '--aod1b', + '-a', + default=False, + action='store_true', + help='Sync GRACE/GRACE-FO Level-1B dealiasing products', + ) # CMR endpoint type - parser.add_argument('--endpoint','-e', - type=str, default='data', choices=['s3','data'], - help='CMR url endpoint type') + parser.add_argument( + '--endpoint', + '-e', + type=str, + default='data', + choices=['s3', 'data'], + help='CMR url endpoint type', + ) # connection timeout - parser.add_argument('--timeout','-t', - type=int, default=360, - help='Timeout in seconds for blocking operations') + parser.add_argument( + '--timeout', + '-t', + type=int, + default=360, + help='Timeout in seconds for blocking operations', + ) # output compressed files - parser.add_argument('--gzip','-G', - default=False, action='store_true', - help='Compress output GRACE/GRACE-FO Level-2 granules') + parser.add_argument( + '--gzip', + '-G', + default=False, + action='store_true', + help='Compress output GRACE/GRACE-FO Level-2 granules', + ) # Output log file in form # PODAAC_sync_2002-04-01.log - parser.add_argument('--log','-l', - default=False, action='store_true', - help='Output log file') + parser.add_argument( + '--log', + '-l', + default=False, + action='store_true', + help='Output log file', + ) # sync options - parser.add_argument('--clobber','-C', - default=False, action='store_true', - help='Overwrite existing data in transfer') + parser.add_argument( + '--clobber', + '-C', + default=False, + action='store_true', + help='Overwrite existing data in transfer', + ) # permissions mode of the directories and files synced (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permission mode of directories and files synced') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permission mode of directories and files synced', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # NASA Earthdata hostname URS = 'urs.earthdata.nasa.gov' @@ -461,26 +606,40 @@ def main(): HOST = 'https://archive.podaac.earthdata.nasa.gov/s3credentials' # There are a range of exceptions that can be thrown here # including HTTPError and URLError. - if (args.endpoint == 's3'): + if args.endpoint == 's3': # build opener for s3 client access - opener = gravtk.utilities.attempt_login(URS, - username=args.user, password=args.password, - netrc=args.netrc) + opener = gravtk.utilities.attempt_login( + URS, username=args.user, password=args.password, netrc=args.netrc + ) # Create and submit request to create AWS session client = gravtk.utilities.s3_client(HOST, args.timeout) else: # build opener for data client access - opener = gravtk.utilities.attempt_login(URS, - username=args.user, password=args.password, - netrc=args.netrc, authorization_header=False) + opener = gravtk.utilities.attempt_login( + URS, + username=args.user, + password=args.password, + netrc=args.netrc, + authorization_header=False, + ) client = None # retrieve data objects from s3 client or data endpoints - podaac_cumulus(client, args.directory, PROC=args.center, - DREL=args.release, VERSION=args.version, AOD1B=args.aod1b, - ENDPOINT=args.endpoint, TIMEOUT=args.timeout, - GZIP=args.gzip, LOG=args.log, CLOBBER=args.clobber, - MODE=args.mode) + podaac_cumulus( + client, + args.directory, + PROC=args.center, + DREL=args.release, + VERSION=args.version, + AOD1B=args.aod1b, + ENDPOINT=args.endpoint, + TIMEOUT=args.timeout, + GZIP=args.gzip, + LOG=args.log, + CLOBBER=args.clobber, + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/dealiasing/aod1b_geocenter.py b/dealiasing/aod1b_geocenter.py index 0d3e6399..c96f1440 100644 --- a/dealiasing/aod1b_geocenter.py +++ b/dealiasing/aod1b_geocenter.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" aod1b_geocenter.py Written by Tyler Sutterley (05/2023) Contributions by Hugo Lecomte (03/2021) @@ -57,6 +57,7 @@ Updated 05-06/2016: oba=ocean bottom pressure, absolute import of shutil Written 05/2016 """ + from __future__ import print_function, division import sys @@ -69,12 +70,9 @@ import numpy as np import gravity_toolkit as gravtk + # program module to read the degree 1 coefficients of the AOD1b data -def aod1b_geocenter(base_dir, - DREL='', - DSET='', - CLOBBER=False, - MODE=0o775): +def aod1b_geocenter(base_dir, DREL='', DSET='', CLOBBER=False, MODE=0o775): """ Creates monthly files of geocenter variations at 6-hour or 3-hour intervals from GRACE/GRACE-FO level-1b dealiasing data files @@ -113,7 +111,7 @@ def aod1b_geocenter(base_dir, # set number of hours in a file # set the atmospheric and ocean model for a given release # set the maximum degree and order of a release - if DREL in ('RL01','RL02','RL03','RL04','RL05'): + if DREL in ('RL01', 'RL02', 'RL03', 'RL04', 'RL05'): # for 00, 06, 12 and 18 n_time = 4 ATMOSPHERE = 'ECMWF' @@ -128,7 +126,7 @@ def aod1b_geocenter(base_dir, else: raise ValueError('Invalid data release') # Calculating the number of cos and sin harmonics up to LMAX - n_harm = (LMAX**2 + 3*LMAX)//2 + 1 + n_harm = (LMAX**2 + 3 * LMAX) // 2 + 1 # AOD1B data products product = {} @@ -139,7 +137,7 @@ def aod1b_geocenter(base_dir, # AOD1B directory and output geocenter directory base_dir = pathlib.Path(base_dir).expanduser().absolute() - grace_dir = base_dir.joinpath('AOD1B',DREL) + grace_dir = base_dir.joinpath('AOD1B', DREL) output_dir = grace_dir.joinpath('geocenter') output_dir.mkdir(mode=MODE, parents=True, exist_ok=True) @@ -149,8 +147,8 @@ def aod1b_geocenter(base_dir, # for each tar file for input_file in sorted(input_tar_files): # extract the year and month from the file - YY,MM,SFX = tx.findall(input_file.name).pop() - YY,MM = np.array([YY, MM], dtype=np.int64) + YY, MM, SFX = tx.findall(input_file.name).pop() + YY, MM = np.array([YY, MM], dtype=np.int64) # output monthly geocenter file FILE = f'AOD1B_{DREL}_{DSET}_{YY:4d}_{MM:02d}.txt' output_file = output_dir.joinpath(FILE) @@ -163,7 +161,7 @@ def aod1b_geocenter(base_dir, input_mtime = input_file.stat().st_mtime output_mtime = output_file.stat().st_mtime # if input tar file is newer: overwrite the output file - if (input_mtime > output_mtime): + if input_mtime > output_mtime: TEST = True OVERWRITE = ' (overwrite)' else: @@ -179,7 +177,7 @@ def aod1b_geocenter(base_dir, args = ('Geocenter time series', DREL, DSET) print('# {0} from {1} AOD1b {2} Product'.format(*args), file=f) print('# {0}'.format(product[DSET]), file=f) - args = ('ISO-Time','X','Y','Z') + args = ('ISO-Time', 'X', 'Y', 'Z') print('# {0:^15} {1:^12} {2:^12} {3:^12}'.format(*args), file=f) # open the AOD1B monthly tar file @@ -190,10 +188,10 @@ def aod1b_geocenter(base_dir, # track tar file members logging.debug(member.name) # get calendar day from file - DD,SFX = fx.findall(member.name).pop() + DD, SFX = fx.findall(member.name).pop() DD = np.int64(DD) # open data file for day - if (SFX == '.gz'): + if SFX == '.gz': fid = gzip.GzipFile(fileobj=tar.extractfile(member)) else: fid = tar.extractfile(member) @@ -207,7 +205,7 @@ def aod1b_geocenter(base_dir, # create counter for hour in dataset c = 0 # while loop ends when dataset is read - while (c < n_time): + while c < n_time: # read line file_contents = fid.readline().decode('ISO-8859-1') # find file header for data product @@ -215,10 +213,10 @@ def aod1b_geocenter(base_dir, # track file header lines logging.debug(file_contents) # extract hour from header and convert to float - HH, = re.findall(r'(\d+):\d+:\d+',file_contents) + (HH,) = re.findall(r'(\d+):\d+:\d+', file_contents) hours[c] = np.int64(HH) # read each line of spherical harmonics - for k in range(0,n_harm): + for k in range(0, n_harm): file_contents = fid.readline().decode('ISO-8859-1') # find numerical instances in the data line line_contents = rx.findall(file_contents) @@ -237,8 +235,8 @@ def aod1b_geocenter(base_dir, # convert from spherical harmonics into geocenter DEG1.to_cartesian() # write to file for each hour (iterates each 6-hour block) - for h,X,Y,Z in zip(hours,DEG1.X,DEG1.Y,DEG1.Z): - print(fstr.format(YY,MM,DD,h,X,Y,Z), file=f) + for h, X, Y, Z in zip(hours, DEG1.X, DEG1.Y, DEG1.Z): + print(fstr.format(YY, MM, DD, h, X, Y, Z), file=f) # close the tar file tar.close() @@ -247,50 +245,77 @@ def aod1b_geocenter(base_dir, # set the permissions mode of the output file output_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates monthly files of geocenter variations at 3 or 6-hour intervals """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO level-1b dealiasing product - parser.add_argument('--product','-p', - metavar='DSET', type=str.lower, nargs='+', - choices=['atm','ocn','glo','oba'], - help='GRACE/GRACE-FO Level-1b data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str.lower, + nargs='+', + choices=['atm', 'ocn', 'glo', 'oba'], + help='GRACE/GRACE-FO Level-1b data product', + ) # clobber will overwrite the existing data - parser.add_argument('--clobber','-C', - default=False, action='store_true', - help='Overwrite existing data') + parser.add_argument( + '--clobber', + '-C', + default=False, + action='store_true', + help='Overwrite existing data', + ) # verbose will output information about each output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] logging.basicConfig(level=loglevels[args.verbose]) @@ -298,11 +323,14 @@ def main(): # for each entered AOD1B dataset for DSET in args.product: # run AOD1b geocenter program with parameters - aod1b_geocenter(args.directory, + aod1b_geocenter( + args.directory, DREL=args.release, DSET=DSET, CLOBBER=args.clobber, - MODE=args.mode) + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/dealiasing/aod1b_oblateness.py b/dealiasing/aod1b_oblateness.py index 9c9313db..a4fbff82 100644 --- a/dealiasing/aod1b_oblateness.py +++ b/dealiasing/aod1b_oblateness.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" aod1b_oblateness.py Written by Tyler Sutterley (05/2023) Contributions by Hugo Lecomte (03/2021) @@ -57,6 +57,7 @@ Updated 05-06/2016: oba=ocean bottom pressure, absolute import of shutil Written 05/2016 """ + from __future__ import print_function, division import sys @@ -69,12 +70,9 @@ import numpy as np import gravity_toolkit as gravtk + # program module to read the C20 coefficients of the AOD1b data -def aod1b_oblateness(base_dir, - DREL='', - DSET='', - CLOBBER=False, - MODE=0o775): +def aod1b_oblateness(base_dir, DREL='', DSET='', CLOBBER=False, MODE=0o775): """ Creates monthly files of oblateness (C20) variations at 6-hour intervals from GRACE/GRACE-FO level-1b dealiasing data files @@ -114,7 +112,7 @@ def aod1b_oblateness(base_dir, # set number of hours in a file # set the atmospheric and ocean model for a given release # set the maximum degree and order of a release - if DREL in ('RL01','RL02','RL03','RL04','RL05'): + if DREL in ('RL01', 'RL02', 'RL03', 'RL04', 'RL05'): # for 00, 06, 12 and 18 n_time = 4 ATMOSPHERE = 'ECMWF' @@ -129,7 +127,7 @@ def aod1b_oblateness(base_dir, else: raise ValueError('Invalid data release') # Calculating the number of cos and sin harmonics up to LMAX - n_harm = (LMAX**2 + 3*LMAX)//2 + 1 + n_harm = (LMAX**2 + 3 * LMAX) // 2 + 1 # AOD1B data products product = {} @@ -140,7 +138,7 @@ def aod1b_oblateness(base_dir, # AOD1B directory and output oblateness directory base_dir = pathlib.Path(base_dir).expanduser().absolute() - grace_dir = base_dir.joinpath('AOD1B',DREL) + grace_dir = base_dir.joinpath('AOD1B', DREL) output_dir = grace_dir.joinpath('oblateness') output_dir.mkdir(mode=MODE, parents=True, exist_ok=True) @@ -150,8 +148,8 @@ def aod1b_oblateness(base_dir, # for each tar file for input_file in sorted(input_tar_files): # extract the year and month from the file - YY,MM,SFX = tx.findall(input_file.name).pop() - YY,MM = np.array([YY, MM], dtype=np.int64) + YY, MM, SFX = tx.findall(input_file.name).pop() + YY, MM = np.array([YY, MM], dtype=np.int64) # output monthly oblateness file FILE = f'AOD1B_{DREL}_{DSET}_{YY:4d}_{MM:02d}.txt' output_file = output_dir.joinpath(FILE) @@ -164,7 +162,7 @@ def aod1b_oblateness(base_dir, input_mtime = input_file.stat().st_mtime output_mtime = output_file.stat().st_mtime # if input tar file is newer: overwrite the output file - if (input_mtime > output_mtime): + if input_mtime > output_mtime: TEST = True OVERWRITE = ' (overwrite)' else: @@ -177,10 +175,10 @@ def aod1b_oblateness(base_dir, logging.info(f'{str(output_file)}{OVERWRITE}') # open output monthly oblateness file f = output_file.open(mode='w', encoding='utf8') - args = ('Oblateness time series',DREL,DSET) + args = ('Oblateness time series', DREL, DSET) print('# {0} from {1} AOD1b {2} Product'.format(*args), file=f) print('# {0}'.format(product[DSET]), file=f) - print('# {0:^15} {1:^15}'.format('ISO-Time','C20'), file=f) + print('# {0:^15} {1:^15}'.format('ISO-Time', 'C20'), file=f) # open the AOD1B monthly tar file tar = tarfile.open(name=str(input_file), mode='r:gz') @@ -190,21 +188,21 @@ def aod1b_oblateness(base_dir, # track tar file members logging.debug(member.name) # get calendar day from file - DD,SFX = fx.findall(member.name).pop() + DD, SFX = fx.findall(member.name).pop() DD = np.int64(DD) # open datafile for day - if (SFX == '.gz'): + if SFX == '.gz': fid = gzip.GzipFile(fileobj=tar.extractfile(member)) else: fid = tar.extractfile(member) # C20 spherical harmonics for day and hours C20 = np.zeros((n_time)) - hours = np.zeros((n_time),dtype=np.int64) + hours = np.zeros((n_time), dtype=np.int64) # create counter for hour in dataset c = 0 # while loop ends when dataset is read - while (c < n_time): + while c < n_time: # read line file_contents = fid.readline().decode('ISO-8859-1') # find file header for data product @@ -212,10 +210,10 @@ def aod1b_oblateness(base_dir, # track file header lines logging.debug(file_contents) # extract hour from header and convert to float - HH, = re.findall(r'(\d+):\d+:\d+',file_contents) + (HH,) = re.findall(r'(\d+):\d+:\d+', file_contents) hours[c] = np.int64(HH) # read each line of spherical harmonics - for k in range(0,n_harm): + for k in range(0, n_harm): file_contents = fid.readline().decode('ISO-8859-1') # find numerical instances in the data line line_contents = rx.findall(file_contents) @@ -230,7 +228,7 @@ def aod1b_oblateness(base_dir, fid.close() # write to file for each hour for h in range(4): - print(fstr.format(YY,MM,DD,hours[h],C20[h]),file=f) + print(fstr.format(YY, MM, DD, hours[h], C20[h]), file=f) # close the tar file tar.close() @@ -239,50 +237,77 @@ def aod1b_oblateness(base_dir, # set the permissions mode of the output file output_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates monthly files of oblateness (C20) variations at 3 or 6-hour intervals """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO level-1b dealiasing product - parser.add_argument('--product','-p', - metavar='DSET', type=str.lower, nargs='+', - choices=['atm','ocn','glo','oba'], - help='GRACE/GRACE-FO Level-1b data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str.lower, + nargs='+', + choices=['atm', 'ocn', 'glo', 'oba'], + help='GRACE/GRACE-FO Level-1b data product', + ) # clobber will overwrite the existing data - parser.add_argument('--clobber','-C', - default=False, action='store_true', - help='Overwrite existing data') + parser.add_argument( + '--clobber', + '-C', + default=False, + action='store_true', + help='Overwrite existing data', + ) # verbose will output information about each output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -291,11 +316,14 @@ def main(): # for each entered AOD1B dataset for DSET in args.product: # run AOD1b oblateness program with parameters - aod1b_oblateness(args.directory, + aod1b_oblateness( + args.directory, DREL=args.release, DSET=DSET, CLOBBER=args.clobber, - MODE=args.mode) + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/dealiasing/dealiasing_global_uplift.py b/dealiasing/dealiasing_global_uplift.py index 3fc366fd..ca5f4597 100644 --- a/dealiasing/dealiasing_global_uplift.py +++ b/dealiasing/dealiasing_global_uplift.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" dealiasing_global_uplift.py Written by Tyler Sutterley (05/2023) @@ -71,6 +71,7 @@ Updated 03/2023: attributes from units class for output netCDF4/HDF5 files Written 03/2023 """ + from __future__ import print_function, division import sys @@ -86,6 +87,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -95,9 +97,11 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: estimates global elastic uplift due to changes in atmospheric # and oceanic loading -def dealiasing_global_uplift(base_dir, +def dealiasing_global_uplift( + base_dir, DREL=None, DSET=None, YEAR=None, @@ -108,8 +112,8 @@ def dealiasing_global_uplift(base_dir, BOUNDS=None, DATAFORM=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # input directory setup base_dir = pathlib.Path(base_dir).expanduser().absolute() grace_dir = base_dir.joinpath('AOD1B', DREL) @@ -124,7 +128,7 @@ def dealiasing_global_uplift(base_dir, # set number of hours in a file for a release # set the atmospheric and ocean model for a given release # set the maximum degree and order of a release - if DREL in ('RL01','RL02','RL03','RL04','RL05'): + if DREL in ('RL01', 'RL02', 'RL03', 'RL04', 'RL05'): # for 00, 06, 12 and 18 nt = 4 ATMOSPHERE = 'ECMWF' @@ -139,7 +143,7 @@ def dealiasing_global_uplift(base_dir, else: raise ValueError('Invalid data release') # Calculating the number of cos and sin harmonics up to LMAX - n_harm = (LMAX**2 + 3*LMAX)//2 + 1 + n_harm = (LMAX**2 + 3 * LMAX) // 2 + 1 # AOD1B data products product = {} @@ -156,10 +160,11 @@ def dealiasing_global_uplift(base_dir, attributes['ROOT']['project_version'] = DREL attributes['ROOT']['product_name'] = DSET attributes['ROOT']['product_type'] = 'gravity_field' - attributes['ROOT']['reference'] = \ + attributes['ROOT']['reference'] = ( f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # output suffix for data formats - suffix = dict(ascii='txt',netCDF4='nc',HDF5='H5') + suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # compile regular expressions operators for file dates # will extract the year and month from the tar file (.tar.gz) @@ -179,33 +184,34 @@ def dealiasing_global_uplift(base_dir, input_tar_files = [tf for tf in grace_dir.iterdir() if tx.match(tf.name)] # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output spatial data grid = gravtk.spatial() # Output Degree Interval - if (INTERVAL == 1): + if INTERVAL == 1: # (0:360,90:-90) - n_lon = np.int64((360.0/dlon)+1.0) - n_lat = np.int64((180.0/dlat)+1.0) - grid.lon = dlon*np.arange(0,n_lon) - grid.lat = 90.0 - dlat*np.arange(0,n_lat) - elif (INTERVAL == 2): + n_lon = np.int64((360.0 / dlon) + 1.0) + n_lat = np.int64((180.0 / dlat) + 1.0) + grid.lon = 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(dlon/2.0,360+dlon/2.0,dlon) - grid.lat = np.arange(90.0-dlat/2.0,-90.0-dlat/2.0,-dlat) + 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) n_lon = len(grid.lon) n_lat = len(grid.lat) - elif (INTERVAL == 3): + 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) + 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) n_lon = len(grid.lon) n_lat = len(grid.lat) # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth parameters attributes['ROOT']['earth_model'] = LOVE.model attributes['ROOT']['earth_love_numbers'] = LOVE.citation @@ -245,7 +251,7 @@ def dealiasing_global_uplift(base_dir, # for each tar file for input_file in sorted(input_tar_files): # extract the year and month from the file - YY,MM,SFX = tx.findall(input_file.name).pop() + YY, MM, SFX = tx.findall(input_file.name).pop() # number of days per month dpm = gravtk.time.calendar_days(int(YY)) # output monthly spatial file @@ -259,7 +265,7 @@ def dealiasing_global_uplift(base_dir, input_mtime = input_file.stat().st_mtime output_mtime = output_file.stat().st_mtime # if input tar file is newer: overwrite the output file - if (input_mtime > output_mtime): + if input_mtime > output_mtime: TEST = True else: TEST = True @@ -272,10 +278,9 @@ def dealiasing_global_uplift(base_dir, # open the AOD1B monthly tar file tar = tarfile.open(name=str(input_file), mode='r:gz') # number of time points - n_time = int(nt*dpm[int(MM)-1]) + n_time = int(nt * dpm[int(MM) - 1]) # flattened harmonics object - YLMS = gravtk.harmonics(lmax=LMAX, mmax=LMAX, - flattened=True) + YLMS = gravtk.harmonics(lmax=LMAX, mmax=LMAX, flattened=True) YLMS.l = np.zeros((n_harm), dtype=int) YLMS.m = np.zeros((n_harm), dtype=int) YLMS.clm = np.zeros((n_harm, n_time)) @@ -290,16 +295,16 @@ def dealiasing_global_uplift(base_dir, # track tar file members logging.debug(member.name) # get calendar day from file - DD,SFX = fx.findall(member.name).pop() + DD, SFX = fx.findall(member.name).pop() # open data file for day - if (SFX == '.gz'): + if SFX == '.gz': fid = gzip.GzipFile(fileobj=tar.extractfile(member)) else: fid = tar.extractfile(member) # create counter for hour in dataset c = 0 # while loop ends when dataset is read - while (c < nt): + while c < nt: # read line file_contents = fid.readline().decode('ISO-8859-1') # find file header for data product @@ -307,15 +312,15 @@ def dealiasing_global_uplift(base_dir, # track file header lines logging.debug(file_contents) # extract hour from header - HH, = re.findall(r'(\d+):\d+:\d+',file_contents) + (HH,) = re.findall(r'(\d+):\d+:\d+', file_contents) # convert dates to int and save to arrays - i = (int(DD)-1)*nt + c + i = (int(DD) - 1) * nt + c years[i] = np.int64(YY) months[i] = np.int64(MM) days[i] = np.int64(DD) hours[i] = np.int64(HH) # read each line of spherical harmonics - for k in range(0,n_harm): + for k in range(0, n_harm): file_contents = fid.readline().decode('ISO-8859-1') # find numerical instances in the data line line_contents = rx.findall(file_contents) @@ -323,35 +328,34 @@ def dealiasing_global_uplift(base_dir, YLMS.l[k] = np.int64(line_contents[0]) YLMS.m[k] = np.int64(line_contents[1]) # extract spherical harmonics - YLMS.clm[k,i] = np.float64(line_contents[2]) - YLMS.slm[k,i] = np.float64(line_contents[3]) + YLMS.clm[k, i] = np.float64(line_contents[2]) + YLMS.slm[k, i] = np.float64(line_contents[3]) # add 1 to hour counter c += 1 # close the input file for day fid.close() # calculate times for flattened harmonics YLMS.time = gravtk.time.convert_calendar_decimal( - years, months, day=days, hour=hours) + years, months, day=days, hour=hours + ) YLMS.month = gravtk.time.calendar_to_grace(YLMS.time) # convert to expanded form in output units Ylms = YLMS.expand(date=True).convolve(dfactor) # convert harmonics to spatial domain - grid.data = np.zeros((n_lat,n_lon,n_time)) - grid.mask = np.zeros((n_lat,n_lon,n_time), dtype=bool) + grid.data = np.zeros((n_lat, n_lon, n_time)) + grid.mask = np.zeros((n_lat, n_lon, n_time), dtype=bool) # calculate delta times for output spatial grids - grid.time = np.array(hours + 24*(days-1), dtype=int) + grid.time = np.array(hours + 24 * (days - 1), dtype=int) # for each date in the harmonics object - for i,iYlm in enumerate(Ylms): + for i, iYlm in enumerate(Ylms): # convert to spatial domain - grid.data[:,:,i] = gravtk.harmonic_summation( - iYlm.clm, iYlm.slm, grid.lon, grid.lat, - LMAX=LMAX, PLM=PLM).T + grid.data[:, :, i] = gravtk.harmonic_summation( + iYlm.clm, iYlm.slm, grid.lon, grid.lat, LMAX=LMAX, PLM=PLM + ).T # update attributes for time - attributes['time']['units'] = \ - f'hours since {YY}-{MM}-01T00:00:00' + attributes['time']['units'] = f'hours since {YY}-{MM}-01T00:00:00' # output spatial data to file - grid.to_file(output_file, format=DATAFORM, - attributes=attributes) + grid.to_file(output_file, format=DATAFORM, attributes=attributes) # set the permissions mode of the output file output_file.chmod(mode=MODE) # append output file to list @@ -362,10 +366,11 @@ def dealiasing_global_uplift(base_dir, # return the list of output files return output_files + # PURPOSE: print a file log for the AOD1b spatial analysis def output_log_file(input_arguments, output_files): # format: aod1b_spatial_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'aod1b_spatial_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -382,10 +387,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the AOD1b spatial analysis def output_error_log_file(input_arguments): # format: aod1b_spatial_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'aod1b_spatial_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -401,6 +407,7 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -408,83 +415,148 @@ def arguments(): for global atmospheric and oceanic loading and estimates anomalies in elastic crustal uplift """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') + help='Output directory for spatial files', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO level-1b dealiasing product - parser.add_argument('--product','-p', - metavar='DSET', type=str.lower, default='glo', - choices=['atm','ocn','glo','oba'], - help='GRACE/GRACE-FO Level-1b data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str.lower, + default='glo', + choices=['atm', 'ocn', 'glo', 'oba'], + help='GRACE/GRACE-FO Level-1b data product', + ) # years to run - parser.add_argument('--year','-Y', - type=int, nargs='+', default=range(2000,2024), - help='Years of data to run') + parser.add_argument( + '--year', + '-Y', + type=int, + nargs='+', + default=range(2000, 2024), + help='Years of data to run', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # output grid parameters - parser.add_argument('--spacing','-S', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval','-I', - type=int, default=2, choices=[1,2,3], - help=('Output grid interval ' - '(1: global, 2: centered global, 3: non-global)')) - parser.add_argument('--bounds','-B', - type=float, nargs=4, metavar=('lon_min','lon_max','lat_min','lat_max'), - help='Bounding box for non-global grid') + parser.add_argument( + '--spacing', + '-S', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + '-I', + type=int, + default=2, + choices=[1, 2, 3], + help=( + 'Output grid interval ' + '(1: global, 2: centered global, 3: non-global)' + ), + ) + parser.add_argument( + '--bounds', + '-B', + type=float, + nargs=4, + metavar=('lon_min', 'lon_max', 'lat_min', 'lat_max'), + help='Bounding box for non-global grid', + ) # input and output data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input and output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input and output data format', + ) # Output log file for each job in forms # aod1b_spatial_run_2002-04-01_PID-00000.log # aod1b_spatial_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the output files (octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] logging.basicConfig(level=loglevels[args.verbose]) @@ -493,7 +565,8 @@ def main(): try: info(args) # run AOD1b uplift program with parameters - output_files = dealiasing_global_uplift(args.directory, + output_files = dealiasing_global_uplift( + args.directory, DREL=args.release, DSET=args.product, YEAR=args.year, @@ -504,18 +577,20 @@ def main(): BOUNDS=args.bounds, DATAFORM=args.format, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/dealiasing/dealiasing_monthly_mean.py b/dealiasing/dealiasing_monthly_mean.py index e07b6a17..0860272f 100755 --- a/dealiasing/dealiasing_monthly_mean.py +++ b/dealiasing/dealiasing_monthly_mean.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" dealiasing_monthly_mean.py Written by Tyler Sutterley (05/2023) @@ -74,6 +74,7 @@ Updated 03/2018: copy date file from input GSM directory to output directory Written 03/2018 """ + from __future__ import print_function, division import sys @@ -88,25 +89,39 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: calculate the Julian day from the year and the day of the year # http://scienceworld.wolfram.com/astronomy/JulianDate.html def calc_julian_day(YEAR, DAY_OF_YEAR): - JD = 367.0*YEAR - np.floor(7.0*(YEAR + np.floor(10.0/12.0))/4.0) - \ - np.floor(3.0*(np.floor((YEAR + 8.0/7.0)/100.0) + 1.0)/4.0) + \ - np.floor(275.0/9.0) + np.float64(DAY_OF_YEAR) + 1721028.5 + JD = ( + 367.0 * YEAR + - np.floor(7.0 * (YEAR + np.floor(10.0 / 12.0)) / 4.0) + - np.floor(3.0 * (np.floor((YEAR + 8.0 / 7.0) / 100.0) + 1.0) / 4.0) + + np.floor(275.0 / 9.0) + + np.float64(DAY_OF_YEAR) + + 1721028.5 + ) return JD -# PURPOSE: reads the AOD1B data and outputs a monthly mean -def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, - LMAX=None, DATAFORM=None, CLOBBER=False, MODE=0o775): +# PURPOSE: reads the AOD1B data and outputs a monthly mean +def dealiasing_monthly_mean( + base_dir, + PROC=None, + DREL=None, + DSET=None, + LMAX=None, + DATAFORM=None, + CLOBBER=False, + MODE=0o775, +): # output data suffix suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # aod1b data products - aod1b_products = dict(GAA='atm',GAB='ocn',GAC='glo',GAD='oba') + aod1b_products = dict(GAA='atm', GAB='ocn', GAC='glo', GAD='oba') # compile regular expressions operator for the clm/slm headers # for the specific AOD1b product - hx = re.compile(fr'^DATA.*SET.*{aod1b_products[DSET]}',re.VERBOSE) + hx = re.compile(rf'^DATA.*SET.*{aod1b_products[DSET]}', re.VERBOSE) # compile regular expression operator to find numerical instances # will extract the data from the file regex_pattern = r'[-+]?(?:(?:\d*\.\d+)|(?:\d+\.?))(?:[Ee][+-]?\d+)?' @@ -114,7 +129,7 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # set number of hours in a file # set the ocean model for a given release - if DREL in ('RL01','RL02','RL03','RL04','RL05'): + if DREL in ('RL01', 'RL02', 'RL03', 'RL04', 'RL05'): # for 00, 06, 12 and 18 nt = 4 ATMOSPHERE = 'ECMWF' @@ -133,7 +148,7 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # Maximum spherical harmonic degree (LMAX) LMAX = default_lmax if not LMAX else LMAX # Calculating the number of cos and sin harmonics up to d/o of file - n_harm = (default_lmax**2 + 3*default_lmax)//2 + 1 + n_harm = (default_lmax**2 + 3 * default_lmax) // 2 + 1 # AOD1B data products product = {} @@ -155,11 +170,11 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # file formatting string if outputting to SHM format shm = '{0}-2_{1:4.0f}{2:03.0f}-{3:4.0f}{4:03.0f}_{5}_{6}_{7}_{8}00.gz' # center name if outputting to SHM format - if (PROC == 'CSR'): + if PROC == 'CSR': CENTER = 'UTCSR' - elif (PROC == 'GFZ'): + elif PROC == 'GFZ': CENTER = default_center - elif (PROC == 'JPL'): + elif PROC == 'JPL': CENTER = 'JPLEM' else: CENTER = default_center @@ -167,9 +182,9 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # read input DATE file from GSM data product grace_date_file = f'{PROC}_{DREL}_DATES.txt' # names and formats of GRACE/GRACE-FO date ascii file - names = ('t','mon','styr','stday','endyr','endday','total') - formats = ('f','i','i','i','i','i','i') - dtype = np.dtype({'names':names, 'formats':formats}) + names = ('t', 'mon', 'styr', 'stday', 'endyr', 'endday', 'total') + formats = ('f', 'i', 'i', 'i', 'i', 'i', 'i') + dtype = np.dtype({'names': names, 'formats': formats}) input_date_file = grace_dir.joinpath('GSM', grace_date_file) date_input = np.loadtxt(input_date_file, skiprows=1, dtype=dtype) tdec = date_input['t'] @@ -183,49 +198,73 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, output_date_file = grace_dir.joinpath(DSET, grace_date_file) f_out = output_date_file.open(mode='w', encoding='utf8') # date file header information - args = ('Mid-date','Month','Start_Day','End_Day','Total_Days') + args = ('Mid-date', 'Month', 'Start_Day', 'End_Day', 'Total_Days') print('{0} {1:>10} {2:>11} {3:>10} {4:>13}'.format(*args), file=f_out) # for each GRACE/GRACE-FO month - for t,gm in enumerate(grace_month): + for t, gm in enumerate(grace_month): # check if GRACE/GRACE-FO month crosses years - if (start_yr[t] != end_yr[t]): + if start_yr[t] != end_yr[t]: # check if start_yr is a Leap Year or Standard Year dpy = gravtk.time.calendar_days(start_yr[t]).sum() # list of Julian Days to read from both start and end year julian_days_to_read = [] # add days to read from start and end years - julian_days_to_read.extend([calc_julian_day(start_yr[t],D) - for D in range(start_day[t],dpy+1)]) - julian_days_to_read.extend([calc_julian_day(end_yr[t],D) - for D in range(1,end_day[t]+1)]) + julian_days_to_read.extend( + [ + calc_julian_day(start_yr[t], D) + for D in range(start_day[t], dpy + 1) + ] + ) + julian_days_to_read.extend( + [ + calc_julian_day(end_yr[t], D) + for D in range(1, end_day[t] + 1) + ] + ) else: # Julian Days to read going from start_day to end_day - julian_days_to_read = [calc_julian_day(start_yr[t],D) - for D in range(start_day[t],end_day[t]+1)] + julian_days_to_read = [ + calc_julian_day(start_yr[t], D) + for D in range(start_day[t], end_day[t] + 1) + ] # output filename for GRACE/GRACE-FO month - if (DATAFORM == 'SHM'): + if DATAFORM == 'SHM': MISSION = 'GRAC' if (gm <= 186) else 'GRFO' - FILE = shm.format(DSET.upper(),start_yr[t],start_day[t], - end_yr[t],end_day[t],MISSION,CENTER,'BC01',DREL[2:]) + FILE = shm.format( + DSET.upper(), + start_yr[t], + start_day[t], + end_yr[t], + end_day[t], + MISSION, + CENTER, + 'BC01', + DREL[2:], + ) else: - args = (PROC,DREL,DSET.upper(),LMAX,gm,suffix[DATAFORM]) + args = (PROC, DREL, DSET.upper(), LMAX, gm, suffix[DATAFORM]) FILE = '{0}_{1}_{2}_CLM_L{3:d}_{4:03d}.{5}'.format(*args) # complete path to output filename OUTPUT_FILE = grace_dir.joinpath(DSET, FILE) # calendar dates to read JD = np.array(julian_days_to_read) - Y,M,D,h,m,s = gravtk.time.convert_julian(JD, - astype='i', format='tuple') + Y, M, D, h, m, s = gravtk.time.convert_julian( + JD, astype='i', format='tuple' + ) # find unique year and month pairs to read - rx1='|'.join(['{0:d}-{1:02d}'.format(*p) for p in set(zip(Y,M))]) - rx2='|'.join(['{0:0d}-{1:02d}-{2:02d}'.format(*p) for p in set(zip(Y,M,D))]) + rx1 = '|'.join(['{0:d}-{1:02d}'.format(*p) for p in set(zip(Y, M))]) + rx2 = '|'.join( + ['{0:0d}-{1:02d}-{2:02d}'.format(*p) for p in set(zip(Y, M, D))] + ) # compile regular expressions operators for finding tar files tx = re.compile(rf'AOD1B_({rx1})_\d+.(tar.gz|tgz)$', re.VERBOSE) # finding all of the tar files in the AOD1b directory - input_tar_files = [tf for tf in aod1b_dir.iterdir() if tx.match(tf.name)] + input_tar_files = [ + tf for tf in aod1b_dir.iterdir() if tx.match(tf.name) + ] # compile regular expressions operators for file dates # will extract year and month and calendar day from the ascii file fx = re.compile(rf'AOD1B_({rx2})_X_\d+.asc(.gz)?$', re.VERBOSE) @@ -265,10 +304,15 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # print GRACE/GRACE-FO dates if there is a complete month of AOD if COMPLETE: # print GRACE/GRACE-FO dates to file - print((f'{tdec[t]:13.8f} {gm:03d} ' - f'{start_yr[t]:8.0f} {start_day[t]:03d} ' - f'{end_yr[t]:8.0f} {end_day[t]:03d} ' - f'{total_days[t]:8.0f}'), file=f_out) + print( + ( + f'{tdec[t]:13.8f} {gm:03d} ' + f'{start_yr[t]:8.0f} {start_day[t]:03d} ' + f'{end_yr[t]:8.0f} {end_day[t]:03d} ' + f'{total_days[t]:8.0f}' + ), + file=f_out, + ) # if there are new files, files to be rewritten or clobbered if COMPLETE and (TEST or CLOBBER): @@ -277,10 +321,11 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # allocate for the mean output harmonics Ylms = gravtk.harmonics(lmax=LMAX, mmax=LMAX) # number of time points - n_time = len(julian_days_to_read)*nt + n_time = len(julian_days_to_read) * nt # flattened harmonics object - YLMS = gravtk.harmonics(lmax=default_lmax, mmax=default_lmax, - flattened=True) + YLMS = gravtk.harmonics( + lmax=default_lmax, mmax=default_lmax, flattened=True + ) YLMS.l = np.zeros((n_harm), dtype=int) YLMS.m = np.zeros((n_harm), dtype=int) YLMS.clm = np.zeros((n_harm, n_time)) @@ -296,7 +341,9 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # open the AOD1B monthly tar file tar = tarfile.open(name=str(input_file), mode='r:gz') # for each ascii file within the tar file that matches fx - monthly_members=[m for m in tar.getmembers() if fx.match(m.name)] + monthly_members = [ + m for m in tar.getmembers() if fx.match(m.name) + ] for member in monthly_members: # track tar file members logging.debug(member.name) @@ -304,38 +351,44 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, YMD, SFX = fx.findall(member.name).pop() YY, MM, DD = re.findall(r'\d+', YMD) # open datafile for day - if (SFX == '.gz'): + if SFX == '.gz': fid = gzip.GzipFile(fileobj=tar.extractfile(member)) else: fid = tar.extractfile(member) # create counters for hour in dataset c = 0 # while loop ends when dataset is read - while (c < nt): + while c < nt: # read line - file_contents=fid.readline().decode('ISO-8859-1') + file_contents = fid.readline().decode('ISO-8859-1') # find file header for data product if bool(hx.search(file_contents)): # track file header lines logging.debug(file_contents) # extract hour from header and convert to float - HH, = re.findall(r'(\d+):\d+:\d+',file_contents) + (HH,) = re.findall(r'(\d+):\d+:\d+', file_contents) # convert dates to int and save to arrays years[count] = np.int64(YY) months[count] = np.int64(MM) days[count] = np.int64(DD) hours[count] = np.int64(HH) # read each line of spherical harmonics - for k in range(0,n_harm): - file_contents=fid.readline().decode('ISO-8859-1') + for k in range(0, n_harm): + file_contents = fid.readline().decode( + 'ISO-8859-1' + ) # find numerical instances in the data line line_contents = rx.findall(file_contents) # spherical harmonic degree and order YLMS.l[k] = np.int64(line_contents[0]) YLMS.m[k] = np.int64(line_contents[1]) # extract spherical harmonics - YLMS.clm[k,count] = np.float64(line_contents[2]) - YLMS.slm[k,count] = np.float64(line_contents[3]) + YLMS.clm[k, count] = np.float64( + line_contents[2] + ) + YLMS.slm[k, count] = np.float64( + line_contents[3] + ) # add 1 to hour counter c += 1 count += 1 @@ -344,7 +397,8 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # calculate times for flattened harmonics YLMS.time = gravtk.time.convert_calendar_decimal( - years, months, day=days, hour=hours) + years, months, day=days, hour=hours + ) YLMS.month = gravtk.time.calendar_to_grace(YLMS.time) # convert to expanded form and truncate to LMAX Ylms = YLMS.expand(date=True).truncate(LMAX) @@ -359,25 +413,29 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, mean_Ylms.product = DSET # start and end time for month start_time = gravtk.time.convert_julian(np.min(JD)) - mean_Ylms.start_time = [f"{start_time['year']:4.0f}", - f"{start_time['month']:02.0f}", - f"{start_time['day']:02.0f}"] + mean_Ylms.start_time = [ + f'{start_time["year"]:4.0f}', + f'{start_time["month"]:02.0f}', + f'{start_time["day"]:02.0f}', + ] end_time = gravtk.time.convert_julian(np.max(JD)) - mean_Ylms.end_time = [f"{end_time['year']:4.0f}", - f"{end_time['month']:02.0f}", - f"{end_time['day']:02.0f}"] + mean_Ylms.end_time = [ + f'{end_time["year"]:4.0f}', + f'{end_time["month"]:02.0f}', + f'{end_time["day"]:02.0f}', + ] # output mean Ylms to file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) mean_Ylms.to_ascii(OUTPUT_FILE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netcdf (.nc) mean_Ylms.to_netCDF4(OUTPUT_FILE, **attributes) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 (.H5) mean_Ylms.to_HDF5(OUTPUT_FILE, **attributes) - elif (DATAFORM == 'SHM'): + elif DATAFORM == 'SHM': mean_Ylms.to_SHM(OUTPUT_FILE, gzip=True) # set the permissions mode of the output file OUTPUT_FILE.chmod(mode=MODE) @@ -386,10 +444,13 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, logging.info(f'File {FILE} not output (incomplete)') # if outputting as spherical harmonic model files - if (DATAFORM == 'SHM'): + if DATAFORM == 'SHM': # Create an index file for the output GRACE product - grace_files = [f.name for f in grace_dir.joinpath(DSET).iterdir() if - re.match(rf'{DSET}-2(.*?)\.gz', f.name)] + grace_files = [ + f.name + for f in grace_dir.joinpath(DSET).iterdir() + if re.match(rf'{DSET}-2(.*?)\.gz', f.name) + ] # outputting GRACE filenames to index grace_index_file = grace_dir.joinpath(DSET, 'index.txt') with grace_index_file.open(mode='w', encoding='utf8') as fid: @@ -403,16 +464,17 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # close the output date file f_out.close() + # PURPOSE: additional routines for the harmonics module class dealiasing(gravtk.harmonics): def __init__(self, **kwargs): super().__init__(**kwargs) - self.center=None - self.release='RLxx' - self.product=None - self.start_time=[None]*3 - self.end_time=[None]*3 - self.gzip=True + self.center = None + self.release = 'RLxx' + self.product = None + self.start_time = [None] * 3 + self.end_time = [None] * 3 + self.gzip = True def from_harmonics(self, temp): """ @@ -420,8 +482,18 @@ def from_harmonics(self, temp): """ self = dealiasing(lmax=temp.lmax, mmax=temp.mmax) # try to assign variables to self - for key in ['clm','slm','time','month','filename', - 'center','release','product','start_time','end_time']: + for key in [ + 'clm', + 'slm', + 'time', + 'month', + 'filename', + 'center', + 'release', + 'product', + 'start_time', + 'end_time', + ]: try: val = getattr(temp, key) setattr(self, key, np.copy(val)) @@ -441,7 +513,7 @@ def to_SHM(self, filename, **kwargs): """ self.filename = pathlib.Path(filename).expanduser().absolute() # set default verbosity - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) logging.info(str(self.filename)) # open the output file if self.gzip: @@ -452,18 +524,30 @@ def to_SHM(self, filename, **kwargs): self.print_header(fid) self.print_harmonic(fid) self.print_global(fid) - self.print_variables(fid,'double precision') + self.print_variables(fid, 'double precision') # output file format - file_format = ('{0:6} {1:4d} {2:4d} {3:+18.12E} {4:+18.12E} ' - '{5:10.4E} {6:10.4E} {7} {8} {9}') + file_format = ( + '{0:6} {1:4d} {2:4d} {3:+18.12E} {4:+18.12E} ' + '{5:10.4E} {6:10.4E} {7} {8} {9}' + ) # start and end time in line format start_date = '{0}{1}{2}.0000'.format(*self.start_time) end_date = '{0}{1}{2}.0000'.format(*self.end_time) # write to file for each spherical harmonic degree and order - for m in range(0, self.mmax+1): - for l in range(m, self.lmax+1): - args = ('GRCOF2', l, m, self.clm[l,m], self.slm[l,m], - 0, 0, start_date, end_date, 'nnnn') + for m in range(0, self.mmax + 1): + for l in range(m, self.lmax + 1): + args = ( + 'GRCOF2', + l, + m, + self.clm[l, m], + self.slm[l, m], + 0, + 0, + start_date, + end_date, + 'nnnn', + ) print(file_format.format(*args), file=fid) # close the output file fid.close() @@ -474,8 +558,8 @@ def print_header(self, fid): fid.write('{0}:\n'.format('header')) # data dimensions fid.write(' {0}:\n'.format('dimensions')) - fid.write(' {0:22}: {1:d}\n'.format('degree',self.lmax)) - fid.write(' {0:22}: {1:d}\n'.format('order',self.lmax)) + fid.write(' {0:22}: {1:d}\n'.format('degree', self.lmax)) + fid.write(' {0:22}: {1:d}\n'.format('order', self.lmax)) fid.write('\n') # PURPOSE: print spherical harmonic attributes to YAML header @@ -484,84 +568,96 @@ def print_harmonic(self, fid): fid.write(' {0}:\n'.format('non-standard_attributes')) # product id product_id = '{0}-2'.format(self.product) - fid.write(' {0:22}: {1}\n'.format('product_id',product_id)) + fid.write(' {0:22}: {1}\n'.format('product_id', product_id)) # format id fid.write(' {0:22}:\n'.format('format_id')) short_name = 'SHM' - fid.write(' {0:20}: {1}\n'.format('short_name',short_name)) + fid.write(' {0:20}: {1}\n'.format('short_name', short_name)) long_name = 'Earth Gravity Spherical Harmonic Model Format' - fid.write(' {0:20}: {1}\n'.format('long_name',long_name)) + fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) # harmonic normalization normalization = 'fully normalized' - fid.write(' {0:22}: {1}\n'.format('normalization', - normalization)) + fid.write(' {0:22}: {1}\n'.format('normalization', normalization)) # earth parameters # gravitational constant fid.write(' {0:22}:\n'.format('earth_gravity_param')) long_name = 'gravitational constant times mass of Earth' - fid.write(' {0:20}: {1}\n'.format('long_name',long_name)) + fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) units = 'm3/s2' - fid.write(' {0:20}: {1}\n'.format('units',units)) + fid.write(' {0:20}: {1}\n'.format('units', units)) value = '3.9860044180E+14' - fid.write(' {0:20}: {1}\n'.format('value',value)) + fid.write(' {0:20}: {1}\n'.format('value', value)) # equatorial radius fid.write(' {0:22}:\n'.format('mean_equator_radius')) long_name = 'mean equator radius' - fid.write(' {0:20}: {1}\n'.format('long_name',long_name)) + fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) units = 'meters' - fid.write(' {0:20}: {1}\n'.format('units',units)) + fid.write(' {0:20}: {1}\n'.format('units', units)) value = '6.3781366000E+06' - fid.write(' {0:20}: {1}\n'.format('value',value)) + fid.write(' {0:20}: {1}\n'.format('value', value)) fid.write('\n') # PURPOSE: print global attributes to YAML header def print_global(self, fid): fid.write(' {0}:\n'.format('global_attributes')) # product title - if (self.month <= 186): + if self.month <= 186: MISSION = 'GRACE' PROJECT = 'NASA Gravity Recovery And Climate Experiment (GRACE)' - ACKNOWLEDGEMENT = ('GRACE is a joint mission of NASA (USA) and ' - 'DLR (Germany).') + ACKNOWLEDGEMENT = ( + 'GRACE is a joint mission of NASA (USA) and DLR (Germany).' + ) else: MISSION = 'GRACE-FO' - PROJECT = ('NASA Gravity Recovery And Climate Experiment ' - 'Follow-On (GRACE-FO)') - ACKNOWLEDGEMENT = ('GRACE-FO is a joint mission of the US National ' + PROJECT = ( + 'NASA Gravity Recovery And Climate Experiment ' + 'Follow-On (GRACE-FO)' + ) + ACKNOWLEDGEMENT = ( + 'GRACE-FO is a joint mission of the US National ' 'Aeronautics and Space Administration and the German Research ' - 'Center for Geosciences.') - args = (MISSION,self.product,self.center,self.release) + 'Center for Geosciences.' + ) + args = (MISSION, self.product, self.center, self.release) title = '{0} Geopotential {1} Coefficients {2} {3}'.format(*args) - fid.write(' {0:22}: {1}\n'.format('title',title)) + fid.write(' {0:22}: {1}\n'.format('title', title)) # product summaries summaries = {} - summaries['GAA'] = ("Spherical harmonic coefficients that represent " + summaries['GAA'] = ( + 'Spherical harmonic coefficients that represent ' "anomalous contributions of the non-tidal atmosphere to the Earth's " - "mean gravity field during the specified timespan. This includes the " - "contribution of atmospheric surface pressure over the continents, " - "the static contribution of atmospheric pressure to ocean bottom " - "pressure elsewhere, and the contribution of upper-air density " - "anomalies above both the continents and the oceans.") - summaries['GAB'] = ("Spherical harmonic coefficients that represent " - "anomalous contributions of the non-tidal dynamic ocean to ocean " - "bottom pressure during the specified timespan.") - summaries['GAC'] = ("Spherical harmonic coefficients that represent " - "the sum of the ATM (or GAA) and OCN (or GAB) coefficients during " - "the specified timespan. These coefficients represent anomalous " - "contributions of the non-tidal dynamic ocean to ocean bottom " - "pressure, the non-tidal atmospheric surface pressure over the " - "continents, the static contribution of atmospheric pressure to " - "ocean bottom pressure, and the upper-air density anomalies above " - "both the continents and the oceans.") - summaries['GAD'] = ("Spherical harmonic coefficients that are zero " - "over the continents, and provide the anomalous simulated ocean " - "bottom pressure that includes non-tidal air and water " - "contributions elsewhere during the specified timespan. These " - "coefficients differ from GLO (or GAC) coefficients over the " - "ocean domain by disregarding upper air density anomalies.") + 'mean gravity field during the specified timespan. This includes the ' + 'contribution of atmospheric surface pressure over the continents, ' + 'the static contribution of atmospheric pressure to ocean bottom ' + 'pressure elsewhere, and the contribution of upper-air density ' + 'anomalies above both the continents and the oceans.' + ) + summaries['GAB'] = ( + 'Spherical harmonic coefficients that represent ' + 'anomalous contributions of the non-tidal dynamic ocean to ocean ' + 'bottom pressure during the specified timespan.' + ) + summaries['GAC'] = ( + 'Spherical harmonic coefficients that represent ' + 'the sum of the ATM (or GAA) and OCN (or GAB) coefficients during ' + 'the specified timespan. These coefficients represent anomalous ' + 'contributions of the non-tidal dynamic ocean to ocean bottom ' + 'pressure, the non-tidal atmospheric surface pressure over the ' + 'continents, the static contribution of atmospheric pressure to ' + 'ocean bottom pressure, and the upper-air density anomalies above ' + 'both the continents and the oceans.' + ) + summaries['GAD'] = ( + 'Spherical harmonic coefficients that are zero ' + 'over the continents, and provide the anomalous simulated ocean ' + 'bottom pressure that includes non-tidal air and water ' + 'contributions elsewhere during the specified timespan. These ' + 'coefficients differ from GLO (or GAC) coefficients over the ' + 'ocean domain by disregarding upper air density anomalies.' + ) summary = summaries[self.product] - fid.write(' {0:22}: {1}\n'.format('summary',''.join(summary))) - fid.write(' {0:22}: {1}\n'.format('project',PROJECT)) + fid.write(' {0:22}: {1}\n'.format('summary', ''.join(summary))) + fid.write(' {0:22}: {1}\n'.format('project', PROJECT)) keywords = [] keywords.append('GRACE') keywords.append('GRACE-FO') if (self.month > 186) else None @@ -585,32 +681,38 @@ def print_global(self, fid): keywords.append('Atmosphere') keywords.append('Non-tidal Atmosphere') keywords.append('Dealiasing Product') - fid.write(' {0:22}: {1}\n'.format('keywords',', '.join(keywords))) - vocabulary = 'NASA Global Change Master Directory (GCMD) Science Keywords' - fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary',vocabulary)) - if (self.center == 'CSR'): + fid.write(' {0:22}: {1}\n'.format('keywords', ', '.join(keywords))) + vocabulary = ( + 'NASA Global Change Master Directory (GCMD) Science Keywords' + ) + fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary', vocabulary)) + if self.center == 'CSR': institution = 'UT-AUSTIN/CSR' - elif (self.center == 'GFZ'): + elif self.center == 'GFZ': institution = 'GFZ German Research Centre for Geosciences' - elif (self.center == 'JPL'): + elif self.center == 'JPL': institution = 'NASA/JPL' else: # default to GFZ institution = 'GFZ German Research Centre for Geosciences' - fid.write(' {0:22}: {1}\n'.format('institution',institution)) + fid.write(' {0:22}: {1}\n'.format('institution', institution)) src = 'All data from AOD1B {0}'.format(self.release) - fid.write(' {0:22}: {1}\n'.format('source',src)) - fid.write(' {0:22}: {1:d}\n'.format('processing_level',2)) - fid.write(' {0:22}: {1}\n'.format('acknowledgement',ACKNOWLEDGEMENT)) + fid.write(' {0:22}: {1}\n'.format('source', src)) + fid.write(' {0:22}: {1:d}\n'.format('processing_level', 2)) + fid.write( + ' {0:22}: {1}\n'.format('acknowledgement', ACKNOWLEDGEMENT) + ) PRODUCT_VERSION = 'Release-{0}'.format(self.release[2:]) - fid.write(' {0:22}: {1}\n'.format('product_version',PRODUCT_VERSION)) + fid.write( + ' {0:22}: {1}\n'.format('product_version', PRODUCT_VERSION) + ) fid.write(' {0:22}:\n'.format('references')) # date range and date created start_date = '{0}-{1}-{2}'.format(*self.start_time) - fid.write(' {0:22}: {1}\n'.format('time_coverage_start',start_date)) + fid.write(' {0:22}: {1}\n'.format('time_coverage_start', start_date)) end_date = '{0}-{1}-{2}'.format(*self.end_time) - fid.write(' {0:22}: {1}\n'.format('time_coverage_end',end_date)) - today = time.strftime('%Y-%m-%d',time.localtime()) + fid.write(' {0:22}: {1}\n'.format('time_coverage_end', end_date)) + today = time.strftime('%Y-%m-%d', time.localtime()) fid.write(' {0:22}: {1}\n'.format('date_created', today)) fid.write('\n') @@ -638,7 +740,9 @@ def print_variables(self, fid, data_precision): fid.write(' {0:20}: {1}\n'.format('comment', '3rd column')) # clm fid.write(' {0:22}:\n'.format('clm')) - long_name = 'Clm coefficient; cosine coefficient for degree l and order m' + long_name = ( + 'Clm coefficient; cosine coefficient for degree l and order m' + ) fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) fid.write(' {0:20}: {1}\n'.format('data_type', data_precision)) fid.write(' {0:20}: {1}\n'.format('comment', '4th column')) @@ -678,8 +782,11 @@ def print_variables(self, fid, data_precision): fid.write(' {0:22}:\n'.format('solution_flags')) long_name = 'Coefficient adjustment and a priori flags' fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) - fid.write(' {0:20}: {1}\n'.format('coverage_content_type', - 'auxiliaryInformation')) + fid.write( + ' {0:20}: {1}\n'.format( + 'coverage_content_type', 'auxiliaryInformation' + ) + ) fid.write(' {0:20}: {1}\n'.format('data_type', 'byte')) fid.write(' {0:20}:\n'.format('flag_meanings')) # solution flag meanings @@ -688,12 +795,13 @@ def print_variables(self, fid, data_precision): m.append('Slm adjusted, y for yes and n for no') m.append('stochastic a priori info for Clm, y for yes and n for no') m.append('stochastic a priori info for Slm, y for yes and n for no') - for i,meaning in enumerate(m): + for i, meaning in enumerate(m): fid.write(' - char {0:d} = {1}\n'.format(i, meaning)) fid.write(' {0:20}: {1}\n'.format('comment', '10th column')) # end of header fid.write('\n\n# End of YAML header\n') + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -701,57 +809,96 @@ def arguments(): specific product and outputs monthly mean for a specific GRACE/GRACE-FO processing center and data release """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Data processing center or satellite mission - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO dealiasing product - parser.add_argument('--product','-p', - metavar='DSET', type=str.upper, nargs='+', - choices=['GAA','GAB','GAC','GAD'], - help='GRACE/GRACE-FO dealiasing product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str.upper, + nargs='+', + choices=['GAA', 'GAB', 'GAC', 'GAD'], + help='GRACE/GRACE-FO dealiasing product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=180, - help='Maximum spherical harmonic degree') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=180, + help='Maximum spherical harmonic degree', + ) # input and output data format (ascii, netCDF4, HDF5, SHM) - parser.add_argument('--format','-F', - type=str, default='netCDF4', - choices=['ascii','netCDF4','HDF5','SHM'], - help='Output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5', 'SHM'], + help='Output data format', + ) # clobber will overwrite the existing data - parser.add_argument('--clobber','-C', - default=False, action='store_true', - help='Overwrite existing data') + parser.add_argument( + '--clobber', + '-C', + default=False, + action='store_true', + help='Overwrite existing data', + ) # verbose will output information about each output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger for verbosity level loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -759,14 +906,17 @@ def main(): for DSET in args.product: # run monthly mean AOD1b program with parameters - dealiasing_monthly_mean(args.directory, + dealiasing_monthly_mean( + args.directory, PROC=args.center, DREL=args.release, DSET=DSET, LMAX=args.lmax, DATAFORM=args.format, CLOBBER=args.clobber, - MODE=args.mode) + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/doc/source/background/sphharm.py b/doc/source/background/sphharm.py index 85852172..d340da92 100644 --- a/doc/source/background/sphharm.py +++ b/doc/source/background/sphharm.py @@ -6,8 +6,8 @@ # latitude and longitude dlon, dlat = 0.625, 0.5 -lat = np.arange(-90 + dlat/2.0, 90 + dlat/2.0, dlat) -lon = np.arange(0 + dlon/2.0, 360 + dlon/2.0, dlon) +lat = np.arange(-90 + dlat / 2.0, 90 + dlat / 2.0, dlat) +lon = np.arange(0 + dlon / 2.0, 360 + dlon / 2.0, dlon) gridlon, gridlat = np.meshgrid(lon, lat) nlat, nlon = gridlat.shape # colatitude and longitude in radians @@ -18,33 +18,39 @@ lmin, lmax = (1, 4) # number of rows and columns for subplots nrows = (lmax - lmin) + 1 -ncols = 2*lmax + 1 -# compute associated Legendre functions +ncols = 2 * lmax + 1 +# compute associated Legendre functions Plm, dPlm = gravtk.associated_legendre(lmax, np.cos(theta)) # reshape to [l,m,lat,lon] -Plm = Plm.reshape((lmax+1, lmax+1, nlat, nlon)) +Plm = Plm.reshape((lmax + 1, lmax + 1, nlat, nlon)) # projection for the plots projection = ccrs.Orthographic(central_longitude=0.0, central_latitude=0.0) # plot spherical harmonics -fig = plt.figure(num=1, figsize=(12,7), facecolor="#fcfcfc") -patch = mpatches.Rectangle((0, 0), 0.445, 1, color='0.95', - zorder=0, transform=fig.transFigure) +fig = plt.figure(num=1, figsize=(12, 7), facecolor='#fcfcfc') +patch = mpatches.Rectangle( + (0, 0), 0.445, 1, color='0.95', zorder=0, transform=fig.transFigure +) fig.add_artist(patch) -for n, l in enumerate(range(lmin, lmax+1)): - for m in range(-l, l+1): +for n, l in enumerate(range(lmin, lmax + 1)): + for m in range(-l, l + 1): # setup subplot - i = n*ncols + l + (lmax-l) + m + 1 + i = n * ncols + l + (lmax - l) + m + 1 ax = fig.add_subplot(nrows, ncols, i, projection=projection) # spherical harmonics of degree l and order m - Ylms = Plm[l,np.abs(m),:,:]*np.exp(1j*m*phi) + Ylms = Plm[l, np.abs(m), :, :] * np.exp(1j * m * phi) Ylm = Ylms.imag if (m < 0) else Ylms.real # plot the surface - ax.pcolormesh(lon, lat, Ylm, + ax.pcolormesh( + lon, + lat, + Ylm, transform=ccrs.PlateCarree(), - cmap='viridis', rasterized=True) + cmap='viridis', + rasterized=True, + ) # set the title ax.set_title(f'$l={l}, m={m}$') # add coastlines and set global @@ -54,11 +60,25 @@ ax.set_axis_off() # add labels for cosine and sine terms -t1 = fig.text(0.05, 0.925, '$S_{lm}$', size=20, - ha="center", va="center", transform=fig.transFigure) -t2 = fig.text(0.95, 0.925, '$C_{lm}$', size=20, - ha="center", va="center", transform=fig.transFigure) +t1 = fig.text( + 0.05, + 0.925, + '$S_{lm}$', + size=20, + ha='center', + va='center', + transform=fig.transFigure, +) +t2 = fig.text( + 0.95, + 0.925, + '$C_{lm}$', + size=20, + ha='center', + va='center', + transform=fig.transFigure, +) # adjust spacing and show plt.tight_layout() -plt.show() \ No newline at end of file +plt.show() diff --git a/doc/source/conf.py b/doc/source/conf.py index 9a69ee99..5c47d145 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -11,6 +11,7 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import os + # import sys import logging import datetime @@ -21,37 +22,39 @@ # -- Project information ----------------------------------------------------- -on_rtd = os.environ.get("READTHEDOCS") == "True" -on_github = os.environ.get("GITHUB_ACTIONS") == "true" +on_rtd = os.environ.get('READTHEDOCS') == 'True' +on_github = os.environ.get('GITHUB_ACTIONS') == 'true' # package metadata -metadata = importlib.metadata.metadata("gravity-toolkit") -project = metadata["Name"] +metadata = importlib.metadata.metadata('gravity-toolkit') +project = metadata['Name'] year = datetime.date.today().year -copyright = f"2019\u2013{year}, Tyler C. Sutterley" +copyright = f'2019\u2013{year}, Tyler C. Sutterley' author = 'Tyler C. Sutterley' # The full version, including alpha/beta/rc tags -version = metadata["version"] +version = metadata['version'] # append "v" before the version -release = f"v{version}" +release = f'v{version}' + # filter out numfig warnings when building documentation, see # https://github.com/sphinx-doc/sphinx/issues/10316 # https://github.com/sphinx-doc/sphinx/pull/14446 class numfig_filter(logging.Filter): def filter(self, record): - warning_type = getattr(record, "type", "") - warning_subtype = getattr(record, "subtype", "") + warning_type = getattr(record, 'type', '') + warning_subtype = getattr(record, 'subtype', '') suppress_warning = ( - f"{warning_type}.{warning_subtype}" == "html.numfig_format" - or record.getMessage().startswith("numfig_format") + f'{warning_type}.{warning_subtype}' == 'html.numfig_format' + or record.getMessage().startswith('numfig_format') ) return not suppress_warning + # suppress warnings in examples and documentation if on_rtd: - warnings.filterwarnings("ignore") + warnings.filterwarnings('ignore') # -- General configuration --------------------------------------------------- @@ -59,37 +62,37 @@ def filter(self, record): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - "matplotlib.sphinxext.plot_directive", - "myst_nb", - "numpydoc", + 'matplotlib.sphinxext.plot_directive', + 'myst_nb', + 'numpydoc', 'sphinxcontrib.bibtex', - "sphinx.ext.autodoc", - "sphinx.ext.graphviz", - "sphinx.ext.viewcode", - "sphinx_design", - "sphinxarg.ext" + 'sphinx.ext.autodoc', + 'sphinx.ext.graphviz', + 'sphinx.ext.viewcode', + 'sphinx_design', + 'sphinxarg.ext', ] # use myst for notebooks source_suffix = { - ".rst": "restructuredtext", - ".ipynb": "myst-nb", + '.rst': 'restructuredtext', + '.ipynb': 'myst-nb', } # execute notebooks on build if on_rtd: - nb_execution_mode = "auto" + nb_execution_mode = 'auto' nb_execution_excludepatterns = [ - "notebooks/*.ipynb", + 'notebooks/*.ipynb', ] - nb_output_stderr = "remove-warn" + nb_output_stderr = 'remove-warn' elif on_github: - nb_execution_mode = "off" + nb_execution_mode = 'off' else: - nb_execution_mode = "auto" + nb_execution_mode = 'auto' nb_execution_excludepatterns = [ - "notebooks/*.ipynb", + 'notebooks/*.ipynb', ] - nb_output_stderr = "remove-warn" + nb_output_stderr = 'remove-warn' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -116,61 +119,62 @@ def filter(self, record): # -- Options for HTML output ------------------------------------------------- # html_title = "gravity-toolkit" -html_short_title = "gravity-toolkit" +html_short_title = 'gravity-toolkit' html_show_sourcelink = False html_show_sphinx = True html_show_copyright = True numfig_format = { - "code-block": None, - "figure": "Figure %s:", - "table": "Table %s:", + 'code-block': None, + 'figure': 'Figure %s:', + 'table': 'Table %s:', } # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "sphinx_rtd_theme" +html_theme = 'sphinx_rtd_theme' html_theme_options = { - "logo_only": True, + 'logo_only': True, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_logo = "_assets/gravity_logo.png" -html_static_path = ["_static"] +html_logo = '_assets/gravity_logo.png' +html_static_path = ['_static'] # fetch the project urls project_urls = {} -for project_url in metadata.get_all("Project-URL"): - name, _, url = project_url.partition(", ") +for project_url in metadata.get_all('Project-URL'): + name, _, url = project_url.partition(', ') project_urls[name.lower()] = url # fetch the repository url -github_url = project_urls.get("repository") -*_, github_user, github_repo = github_url.split("/") +github_url = project_urls.get('repository') +*_, github_user, github_repo = github_url.split('/') # add html context html_context = { - "display_github": True, - "github_user": github_user, - "github_repo": github_repo, - "github_version": "main", - "conf_py_path": "/doc/source/", - "menu_links": [ + 'display_github': True, + 'github_user': github_user, + 'github_repo': github_repo, + 'github_version': 'main', + 'conf_py_path': '/doc/source/', + 'menu_links': [ ( ' Source Code', github_url, ), ( ' License', - f"{github_url}/blob/main/LICENSE", + f'{github_url}/blob/main/LICENSE', ), ( ' Discussions', - f"{github_url}/discussions", + f'{github_url}/discussions', ), ], } + # Load the custom CSS files (needs sphinx >= 1.6 for this to work) def setup(app): - app.add_css_file("style.css") + app.add_css_file('style.css') diff --git a/doc/source/getting_started/Install.ipynb b/doc/source/getting_started/Install.ipynb index 74fd99ed..ff800b0b 100644 --- a/doc/source/getting_started/Install.ipynb +++ b/doc/source/getting_started/Install.ipynb @@ -129,7 +129,7 @@ "source": [ "import gravity_toolkit as gravtk\n", "\n", - "print(f\"gravity-toolkit version: {gravtk.__version__}\")" + "print(f'gravity-toolkit version: {gravtk.__version__}')" ] }, { diff --git a/doc/source/notebooks/GRACE-Geostrophic-Maps.ipynb b/doc/source/notebooks/GRACE-Geostrophic-Maps.ipynb index ca4ce4c2..0221b80e 100644 --- a/doc/source/notebooks/GRACE-Geostrophic-Maps.ipynb +++ b/doc/source/notebooks/GRACE-Geostrophic-Maps.ipynb @@ -26,9 +26,10 @@ "source": [ "import numpy as np\n", "import matplotlib\n", + "\n", "matplotlib.rcParams['mathtext.default'] = 'regular'\n", - "matplotlib.rcParams[\"animation.html\"] = \"jshtml\"\n", - "matplotlib.rcParams[\"animation.embed_limit\"] = 50\n", + "matplotlib.rcParams['animation.html'] = 'jshtml'\n", + "matplotlib.rcParams['animation.embed_limit'] = 50\n", "import matplotlib.pyplot as plt\n", "import matplotlib.animation as animation\n", "import matplotlib.offsetbox as offsetbox\n", @@ -59,11 +60,7 @@ "# set the directory with GRACE/GRACE-FO data\n", "# update local data with PO.DAAC https servers\n", "widgets = gravtk.tools.widgets()\n", - "ipywidgets.VBox([\n", - " widgets.directory,\n", - " widgets.update,\n", - " widgets.endpoint\n", - "])" + "ipywidgets.VBox([widgets.directory, widgets.update, widgets.endpoint])" ] }, { @@ -121,12 +118,9 @@ "# update widgets\n", "widgets.select_product()\n", "# display widgets for setting GRACE/GRACE-FO parameters\n", - "ipywidgets.VBox([\n", - " widgets.center,\n", - " widgets.release,\n", - " widgets.product,\n", - " widgets.months\n", - "])" + "ipywidgets.VBox(\n", + " [widgets.center, widgets.release, widgets.product, widgets.months]\n", + ")" ] }, { @@ -156,19 +150,21 @@ "# update widgets\n", "widgets.select_options()\n", "# display widgets for setting GRACE/GRACE-FO read parameters\n", - "ipywidgets.VBox([\n", - " widgets.lmax,\n", - " widgets.mmax,\n", - " widgets.geocenter,\n", - " widgets.C20,\n", - " widgets.CS21,\n", - " widgets.CS22,\n", - " widgets.C30,\n", - " widgets.C40,\n", - " widgets.C50,\n", - " widgets.pole_tide,\n", - " widgets.atm\n", - "])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.lmax,\n", + " widgets.mmax,\n", + " widgets.geocenter,\n", + " widgets.C20,\n", + " widgets.CS21,\n", + " widgets.CS22,\n", + " widgets.C30,\n", + " widgets.C40,\n", + " widgets.C50,\n", + " widgets.pole_tide,\n", + " widgets.atm,\n", + " ]\n", + ")" ] }, { @@ -206,11 +202,27 @@ "# read GRACE/GRACE-FO data for parameters\n", "start_mon = np.min(months)\n", "end_mon = np.max(months)\n", - "missing = sorted(set(np.arange(start_mon,end_mon+1)) - set(months))\n", - "Ylms = gravtk.grace_input_months(widgets.base_directory, PROC, DREL, DSET,\n", - " LMAX, start_mon, end_mon, missing, SLR_C20, DEG1, MMAX=MMAX,\n", - " SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40,\n", - " SLR_C50=SLR_C50, POLE_TIDE=POLE_TIDE, ATM=ATM)\n", + "missing = sorted(set(np.arange(start_mon, end_mon + 1)) - set(months))\n", + "Ylms = gravtk.grace_input_months(\n", + " widgets.base_directory,\n", + " PROC,\n", + " DREL,\n", + " DSET,\n", + " LMAX,\n", + " start_mon,\n", + " end_mon,\n", + " missing,\n", + " SLR_C20,\n", + " DEG1,\n", + " MMAX=MMAX,\n", + " SLR_21=SLR_21,\n", + " SLR_22=SLR_22,\n", + " SLR_C30=SLR_C30,\n", + " SLR_C40=SLR_C40,\n", + " SLR_C50=SLR_C50,\n", + " POLE_TIDE=POLE_TIDE,\n", + " ATM=ATM,\n", + ")\n", "# create harmonics object and remove mean\n", "GRACE_Ylms = gravtk.harmonics().from_dict(Ylms)\n", "GRACE_Ylms.mean(apply=True)\n", @@ -311,15 +323,18 @@ "widgets.select_output()\n", "# display widgets for setting GRACE/GRACE-FO corrections parameters\n", "widgets.gaussian.value = 600.0\n", - "ipywidgets.VBox([\n", - " widgets.GIA_file,\n", - " widgets.GIA,\n", - " widgets.remove_file,\n", - " widgets.remove_format,\n", - " widgets.redistribute_removed,\n", - " widgets.mask,\n", - " widgets.gaussian,\n", - " widgets.destripe])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.GIA_file,\n", + " widgets.GIA,\n", + " widgets.remove_file,\n", + " widgets.remove_format,\n", + " widgets.redistribute_removed,\n", + " widgets.mask,\n", + " widgets.gaussian,\n", + " widgets.destripe,\n", + " ]\n", + ")" ] }, { @@ -351,21 +366,21 @@ "\n", "# Read Smoothed Ocean and Land Functions\n", "# will mask out land regions in the final current maps\n", - "LANDMASK = gravtk.utilities.get_data_path(['data','land_fcn_300km.nc'])\n", - "landsea = gravtk.spatial().from_netCDF4(LANDMASK,\n", - " date=False, varname='LSMASK')\n", + "LANDMASK = gravtk.utilities.get_data_path(['data', 'land_fcn_300km.nc'])\n", + "landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, varname='LSMASK')\n", "# degree spacing and grid dimensions\n", "# will create GRACE spatial fields with same dimensions\n", - "dlon,dlat = landsea.spacing\n", + "dlon, dlat = landsea.spacing\n", "nlat, nlon = landsea.shape\n", "# shift landsea mask to have longitudes -180:180\n", - "landsea.mask, landsea.lon = gravtk.tools.shift_grid(180.0 + dlon,\n", - " landsea.mask, landsea.lon, CYCLIC=360)\n", + "landsea.mask, landsea.lon = gravtk.tools.shift_grid(\n", + " 180.0 + dlon, landsea.mask, landsea.lon, CYCLIC=360\n", + ")\n", "# grid latitude and longitude\n", "grid.lon = np.copy(landsea.lon)\n", "grid.lat = np.copy(landsea.lat)\n", "# mask equatorial regions due to hydrostrophic inaccuracies\n", - "valid, = np.nonzero((np.abs(grid.lat) > 10))\n", + "(valid,) = np.nonzero((np.abs(grid.lat) > 10))\n", "\n", "# Computing plms for converting to spatial domain\n", "theta = np.radians(90.0 - grid.lat)\n", @@ -375,21 +390,28 @@ "# read load love numbers file\n", "# PREM outputs from Han and Wahr (1995)\n", "# https://doi.org/10.1111/j.1365-246X.1995.tb01819.x\n", - "love_numbers_file = gravtk.utilities.get_data_path(['data','love_numbers'])\n", + "love_numbers_file = gravtk.utilities.get_data_path(['data', 'love_numbers'])\n", "header = 2\n", - "columns = ['l','hl','kl','ll']\n", + "columns = ['l', 'hl', 'kl', 'll']\n", "# LMAX of load love numbers from Han and Wahr (1995) is 696.\n", "# from Wahr (2007) linearly interpolating kl works\n", "# however, as we are linearly extrapolating out, do not make\n", "# LMAX too much larger than 696\n", "# read arrays of kl, hl, and ll Love Numbers\n", - "LOVE = gravtk.read_love_numbers(love_numbers_file, LMAX=LMAX,\n", - " HEADER=header, COLUMNS=columns, REFERENCE='CF', FORMAT='class')\n", + "LOVE = gravtk.read_love_numbers(\n", + " love_numbers_file,\n", + " LMAX=LMAX,\n", + " HEADER=header,\n", + " COLUMNS=columns,\n", + " REFERENCE='CF',\n", + " FORMAT='class',\n", + ")\n", "\n", "# read GIA data\n", "GIA = widgets.GIA.value\n", - "GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA(widgets.GIA_model,\n", - " GIA=GIA, mmax=MMAX)\n", + "GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA(\n", + " widgets.GIA_model, GIA=GIA, mmax=MMAX\n", + ")\n", "gia_str = '' if (GIA == '[None]') else f'_{GIA_Ylms_rate.title}'\n", "# calculate the monthly mass change from GIA\n", "# monthly GIA calculated by gia_rate*time elapsed\n", @@ -400,8 +422,9 @@ "# if redistributing removed mass over the ocean\n", "if widgets.redistribute_removed.value:\n", " # read Land-Sea Mask and convert to spherical harmonics\n", - " ocean_Ylms = gravtk.ocean_stokes(widgets.landmask, LMAX,\n", - " MMAX=MMAX, LOVE=LOVE)\n", + " ocean_Ylms = gravtk.ocean_stokes(\n", + " widgets.landmask, LMAX, MMAX=MMAX, LOVE=LOVE\n", + " )\n", "\n", "# read data to be removed from GRACE/GRACE-FO monthly harmonics\n", "remove_Ylms = GRACE_Ylms.zeros_like()\n", @@ -410,48 +433,48 @@ "# If there are files to be removed from the GRACE/GRACE-FO data\n", "# for each file separated by commas\n", "for f in widgets.remove_files:\n", - " if (widgets.remove_format.value == 'netCDF4'):\n", + " if widgets.remove_format.value == 'netCDF4':\n", " # read netCDF4 file\n", " Ylms = gravtk.harmonics().from_netCDF4(f)\n", - " elif (widgets.remove_format.value == 'HDF5'):\n", + " elif widgets.remove_format.value == 'HDF5':\n", " # read HDF5 file\n", " Ylms = gravtk.harmonics().from_HDF5(f)\n", - " elif (widgets.remove_format.value == 'index (ascii)'):\n", + " elif widgets.remove_format.value == 'index (ascii)':\n", " # read index of ascii files\n", - " Ylms = gravtk.harmonics().from_index(f,format='ascii')\n", - " elif (widgets.remove_format.value == 'index (netCDF4)'):\n", + " Ylms = gravtk.harmonics().from_index(f, format='ascii')\n", + " elif widgets.remove_format.value == 'index (netCDF4)':\n", " # read index of netCDF4 files\n", - " Ylms = gravtk.harmonics().from_index(f,format='netCDF4')\n", - " elif (widgets.remove_format.value == 'index (HDF5)'):\n", + " Ylms = gravtk.harmonics().from_index(f, format='netCDF4')\n", + " elif widgets.remove_format.value == 'index (HDF5)':\n", " # read index of HDF5 files\n", - " Ylms = gravtk.harmonics().from_index(f,format='HDF5')\n", + " Ylms = gravtk.harmonics().from_index(f, format='HDF5')\n", " # reduce to months of interest and truncate to range\n", - " Ylms = Ylms.subset(months).truncate(LMAX,mmax=MMAX)\n", + " Ylms = Ylms.subset(months).truncate(LMAX, mmax=MMAX)\n", " # redistribute removed mass over the ocean\n", " if widgets.redistribute_removed.value:\n", " # calculate ratio between total removed mass and\n", " # a uniformly distributed cm of water over the ocean\n", - " ratio = Ylms.clm[0,0,:]/ocean_Ylms.clm[0,0]\n", + " ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0]\n", " # for each spherical harmonic\n", - " for m in range(0,MMAX+1):\n", - " for l in range(m,LMAX+1):\n", + " for m in range(0, MMAX + 1):\n", + " for l in range(m, LMAX + 1):\n", " # remove the ratio*ocean Ylms from Ylms\n", - " Ylms.clm[l,m,:]-=ratio*ocean_Ylms.clm[l,m]\n", - " Ylms.slm[l,m,:]-=ratio*ocean_Ylms.slm[l,m]\n", + " Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m]\n", + " Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m]\n", " # add the harmonics to be removed to the total\n", " remove_Ylms.add(Ylms)\n", "\n", "# converting harmonics to truncated, smoothed coefficients in units\n", "# combining harmonics to calculate output spatial fields\n", "# output geostrophic current grid\n", - "grid.data = np.zeros((nlat, nlon, 2,nt))\n", - "grid.mask = np.ones((nlat, nlon, 2,nt), dtype=bool)\n", - "grid.mask[valid,:,:,:] = False\n", + "grid.data = np.zeros((nlat, nlon, 2, nt))\n", + "grid.mask = np.ones((nlat, nlon, 2, nt), dtype=bool)\n", + "grid.mask[valid, :, :, :] = False\n", "# set land values from land-sea mask to invalid\n", - "indy,indx = np.nonzero(np.logical_not(landsea.mask))\n", - "grid.mask[indy,indx,:,:] = True\n", + "indy, indx = np.nonzero(np.logical_not(landsea.mask))\n", + "grid.mask[indy, indx, :, :] = True\n", "# for each GRACE/GRACE-FO month\n", - "for i,grace_month in enumerate(GRACE_Ylms.month):\n", + "for i, grace_month in enumerate(GRACE_Ylms.month):\n", " # GRACE/GRACE-FO harmonics for time t\n", " # and monthly files to be removed\n", " if widgets.destripe.value:\n", @@ -463,11 +486,19 @@ " # Remove GIA rate for time\n", " Ylms.subtract(GIA_Ylms.index(i))\n", " # convert spherical harmonics to output spatial grid\n", - " currents = gravtk.geostrophic_currents(Ylms.clm, Ylms.slm,\n", - " grid.lon, grid.lat[valid], LMAX=LMAX, MMAX=MMAX,\n", - " RAD=RAD, LOVE=LOVE, PLM=PLM)\n", + " currents = gravtk.geostrophic_currents(\n", + " Ylms.clm,\n", + " Ylms.slm,\n", + " grid.lon,\n", + " grid.lat[valid],\n", + " LMAX=LMAX,\n", + " MMAX=MMAX,\n", + " RAD=RAD,\n", + " LOVE=LOVE,\n", + " PLM=PLM,\n", + " )\n", " # transpose to outputs to latxlon\n", - " grid.data[valid,:,:,i] = currents.transpose(1,0,2)\n", + " grid.data[valid, :, :, i] = currents.transpose(1, 0, 2)\n", "# update the mask and replace fill values\n", "grid.update_mask();" ] @@ -491,7 +522,7 @@ "vmax = np.ceil(np.nanmax(grid.data)).astype(np.int64)\n", "cmap1 = gravtk.tools.colormap(vmin=vmin, vmax=vmax)\n", "# display widgets for setting GRACE/GRACE-FO regression plot parameters\n", - "ipywidgets.VBox([cmap1.range,cmap1.step,cmap1.name,cmap1.reverse])" + "ipywidgets.VBox([cmap1.range, cmap1.step, cmap1.name, cmap1.reverse])" ] }, { @@ -501,50 +532,94 @@ "outputs": [], "source": [ "%matplotlib inline\n", - "fig, (ax1,ax2) = plt.subplots(num=1, nrows=2, ncols=1, figsize=(10.375,11.625),\n", - " sharex=True, sharey=True, subplot_kw=dict(projection=ccrs.PlateCarree()))\n", + "fig, (ax1, ax2) = plt.subplots(\n", + " num=1,\n", + " nrows=2,\n", + " ncols=1,\n", + " figsize=(10.375, 11.625),\n", + " sharex=True,\n", + " sharey=True,\n", + " subplot_kw=dict(projection=ccrs.PlateCarree()),\n", + ")\n", "\n", "# levels and normalization for plot range\n", - "im1 = ax1.imshow(np.zeros((nlat, nlon)), interpolation='nearest',\n", - " norm=cmap1.norm, cmap=cmap1.value, transform=ccrs.PlateCarree(),\n", - " extent=grid.extent, origin='upper', animated=True)\n", - "im2 = ax2.imshow(np.zeros((nlat, nlon)), interpolation='nearest',\n", - " norm=cmap1.norm, cmap=cmap1.value, transform=ccrs.PlateCarree(),\n", - " extent=grid.extent, origin='upper', animated=True)\n", + "im1 = ax1.imshow(\n", + " np.zeros((nlat, nlon)),\n", + " interpolation='nearest',\n", + " norm=cmap1.norm,\n", + " cmap=cmap1.value,\n", + " transform=ccrs.PlateCarree(),\n", + " extent=grid.extent,\n", + " origin='upper',\n", + " animated=True,\n", + ")\n", + "im2 = ax2.imshow(\n", + " np.zeros((nlat, nlon)),\n", + " interpolation='nearest',\n", + " norm=cmap1.norm,\n", + " cmap=cmap1.value,\n", + " transform=ccrs.PlateCarree(),\n", + " extent=grid.extent,\n", + " origin='upper',\n", + " animated=True,\n", + ")\n", "\n", "# add date label (year-calendar month e.g. 2002-01)\n", - "time_text = ax1.text(0.025, 0.015, '', transform=fig.transFigure,\n", - " color='k', size=24, weight='bold', ha='left', va='baseline')\n", + "time_text = ax1.text(\n", + " 0.025,\n", + " 0.015,\n", + " '',\n", + " transform=fig.transFigure,\n", + " color='k',\n", + " size=24,\n", + " weight='bold',\n", + " ha='left',\n", + " va='baseline',\n", + ")\n", "\n", "# Add colorbar\n", "# Add an axes at position rect [left, bottom, width, height]\n", "cbar_ax = fig.add_axes([0.095, 0.075, 0.81, 0.03])\n", "# extend = add extension triangles to upper and lower bounds\n", "# options: neither, both, min, max\n", - "cbar = fig.colorbar(im1, cax=cbar_ax, extend='both',\n", - " extendfrac=0.0375, drawedges=False, orientation='horizontal')\n", + "cbar = fig.colorbar(\n", + " im1,\n", + " cax=cbar_ax,\n", + " extend='both',\n", + " extendfrac=0.0375,\n", + " drawedges=False,\n", + " orientation='horizontal',\n", + ")\n", "# rasterized colorbar to remove lines\n", "cbar.solids.set_rasterized(True)\n", "# Add label to the colorbar\n", - "cbar.ax.set_title('Geostrophic Current', fontsize=18, rotation=0, y=-1.65, va='top')\n", + "cbar.ax.set_title(\n", + " 'Geostrophic Current', fontsize=18, rotation=0, y=-1.65, va='top'\n", + ")\n", "cbar.ax.set_xlabel('cm/s', fontsize=18, rotation=0, va='center')\n", "cbar.ax.xaxis.set_label_coords(1.085, 0.5)\n", "# Set the tick levels for the colorbar\n", "cbar.set_ticks(cmap1.levels)\n", "cbar.set_ticklabels(cmap1.label)\n", "# ticks lines all the way across\n", - "cbar.ax.tick_params(which='both', width=1, length=25, labelsize=18,\n", - " direction='in')\n", + "cbar.ax.tick_params(\n", + " which='both', width=1, length=25, labelsize=18, direction='in'\n", + ")\n", "\n", "# add labels, coastlines and adjust frames\n", "labels = ['Zonal', 'Meridional']\n", "for i, ax in enumerate([ax1, ax2]):\n", " # add current label\n", - " at = offsetbox.AnchoredText(labels[i],\n", - " loc=3, pad=0, borderpad=0.25, frameon=True,\n", - " prop=dict(size=24, weight='bold', color='k'))\n", - " at.patch.set_boxstyle(\"Square,pad=0.2\")\n", - " at.patch.set_edgecolor(\"white\")\n", + " at = offsetbox.AnchoredText(\n", + " labels[i],\n", + " loc=3,\n", + " pad=0,\n", + " borderpad=0.25,\n", + " frameon=True,\n", + " prop=dict(size=24, weight='bold', color='k'),\n", + " )\n", + " at.patch.set_boxstyle('Square,pad=0.2')\n", + " at.patch.set_edgecolor('white')\n", " ax.axes.add_artist(at)\n", " # add coastlines\n", " ax.coastlines('50m')\n", @@ -552,20 +627,23 @@ " ax.spines['geo'].set_linewidth(2.0)\n", " ax.spines['geo'].set_zorder(10)\n", " ax.spines['geo'].set_capstyle('projecting')\n", - " \n", + "\n", "# adjust subplot within figure\n", "fig.patch.set_facecolor('white')\n", - "fig.subplots_adjust(left=0.01, right=0.99, bottom=0.12, top=0.97,\n", - " hspace=0.05, wspace=0.05)\n", - " \n", + "fig.subplots_adjust(\n", + " left=0.01, right=0.99, bottom=0.12, top=0.97, hspace=0.05, wspace=0.05\n", + ")\n", + "\n", + "\n", "# animate frames\n", "def animate_frames(i):\n", " # set image\n", - " im1.set_data(grid.data[:,:,0,i])\n", - " im2.set_data(grid.data[:,:,1,i])\n", + " im1.set_data(grid.data[:, :, 0, i])\n", + " im2.set_data(grid.data[:, :, 1, i])\n", " # add date label (year-calendar month e.g. 2002-01)\n", - " year,month = gravtk.time.grace_to_calendar(grid.month[i])\n", - " time_text.set_text(u'{0:4d}\\u2013{1:02d}'.format(year,month))\n", + " year, month = gravtk.time.grace_to_calendar(grid.month[i])\n", + " time_text.set_text('{0:4d}\\u2013{1:02d}'.format(year, month))\n", + "\n", "\n", "# set animation\n", "anim = animation.FuncAnimation(fig, animate_frames, frames=nt)\n", diff --git a/doc/source/notebooks/GRACE-Harmonic-Plots.ipynb b/doc/source/notebooks/GRACE-Harmonic-Plots.ipynb index 8b3bbba6..6f83b711 100644 --- a/doc/source/notebooks/GRACE-Harmonic-Plots.ipynb +++ b/doc/source/notebooks/GRACE-Harmonic-Plots.ipynb @@ -26,9 +26,10 @@ "source": [ "import numpy as np\n", "import matplotlib\n", + "\n", "matplotlib.rcParams['mathtext.default'] = 'regular'\n", - "matplotlib.rcParams[\"animation.html\"] = \"jshtml\"\n", - "matplotlib.rcParams[\"animation.embed_limit\"] = 50\n", + "matplotlib.rcParams['animation.html'] = 'jshtml'\n", + "matplotlib.rcParams['animation.embed_limit'] = 50\n", "import matplotlib.pyplot as plt\n", "import matplotlib.animation as animation\n", "import ipywidgets\n", @@ -57,11 +58,7 @@ "# set the directory with GRACE/GRACE-FO data\n", "# update local data with PO.DAAC https servers\n", "widgets = gravtk.tools.widgets()\n", - "ipywidgets.VBox([\n", - " widgets.directory,\n", - " widgets.update,\n", - " widgets.endpoint\n", - "])" + "ipywidgets.VBox([widgets.directory, widgets.update, widgets.endpoint])" ] }, { @@ -122,12 +119,9 @@ "# update widgets\n", "widgets.select_product()\n", "# display widgets for setting GRACE/GRACE-FO parameters\n", - "ipywidgets.VBox([\n", - " widgets.center,\n", - " widgets.release,\n", - " widgets.product,\n", - " widgets.months\n", - "])" + "ipywidgets.VBox(\n", + " [widgets.center, widgets.release, widgets.product, widgets.months]\n", + ")" ] }, { @@ -157,19 +151,21 @@ "# update widgets\n", "widgets.select_options()\n", "# display widgets for setting GRACE/GRACE-FO read parameters\n", - "ipywidgets.VBox([\n", - " widgets.lmax,\n", - " widgets.mmax,\n", - " widgets.geocenter,\n", - " widgets.C20,\n", - " widgets.CS21,\n", - " widgets.CS22,\n", - " widgets.C30,\n", - " widgets.C40,\n", - " widgets.C50,\n", - " widgets.pole_tide,\n", - " widgets.atm\n", - "])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.lmax,\n", + " widgets.mmax,\n", + " widgets.geocenter,\n", + " widgets.C20,\n", + " widgets.CS21,\n", + " widgets.CS22,\n", + " widgets.C30,\n", + " widgets.C40,\n", + " widgets.C50,\n", + " widgets.pole_tide,\n", + " widgets.atm,\n", + " ]\n", + ")" ] }, { @@ -207,11 +203,27 @@ "# read GRACE/GRACE-FO data for parameters\n", "start_mon = np.min(months)\n", "end_mon = np.max(months)\n", - "missing = sorted(set(np.arange(start_mon,end_mon+1)) - set(months))\n", - "Ylms = gravtk.grace_input_months(widgets.base_directory, PROC, DREL, DSET,\n", - " LMAX, start_mon, end_mon, missing, SLR_C20, DEG1, MMAX=MMAX,\n", - " SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40,\n", - " SLR_C50=SLR_C50, POLE_TIDE=POLE_TIDE, ATM=ATM)\n", + "missing = sorted(set(np.arange(start_mon, end_mon + 1)) - set(months))\n", + "Ylms = gravtk.grace_input_months(\n", + " widgets.base_directory,\n", + " PROC,\n", + " DREL,\n", + " DSET,\n", + " LMAX,\n", + " start_mon,\n", + " end_mon,\n", + " missing,\n", + " SLR_C20,\n", + " DEG1,\n", + " MMAX=MMAX,\n", + " SLR_21=SLR_21,\n", + " SLR_22=SLR_22,\n", + " SLR_C30=SLR_C30,\n", + " SLR_C40=SLR_C40,\n", + " SLR_C50=SLR_C50,\n", + " POLE_TIDE=POLE_TIDE,\n", + " ATM=ATM,\n", + ")\n", "# create harmonics object and remove mean\n", "GRACE_Ylms = gravtk.harmonics().from_dict(Ylms)\n", "GRACE_Ylms.mean(apply=True)\n", @@ -247,16 +259,19 @@ "widgets.select_corrections(units=['cmwe', 'mmGH'])\n", "widgets.select_output()\n", "# display widgets for setting GRACE/GRACE-FO corrections parameters\n", - "ipywidgets.VBox([\n", - " widgets.GIA_file,\n", - " widgets.GIA,\n", - " widgets.remove_file,\n", - " widgets.remove_format,\n", - " widgets.redistribute_removed,\n", - " widgets.mask,\n", - " widgets.gaussian,\n", - " widgets.destripe,\n", - " widgets.units])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.GIA_file,\n", + " widgets.GIA,\n", + " widgets.remove_file,\n", + " widgets.remove_format,\n", + " widgets.redistribute_removed,\n", + " widgets.mask,\n", + " widgets.gaussian,\n", + " widgets.destripe,\n", + " widgets.units,\n", + " ]\n", + ")" ] }, { @@ -281,22 +296,28 @@ "# read load love numbers file\n", "# PREM outputs from Han and Wahr (1995)\n", "# https://doi.org/10.1111/j.1365-246X.1995.tb01819.x\n", - "love_numbers_file = gravtk.utilities.get_data_path(['data','love_numbers'])\n", + "love_numbers_file = gravtk.utilities.get_data_path(['data', 'love_numbers'])\n", "header = 2\n", - "columns = ['l','hl','kl','ll']\n", + "columns = ['l', 'hl', 'kl', 'll']\n", "# LMAX of load love numbers from Han and Wahr (1995) is 696.\n", "# from Wahr (2007) linearly interpolating kl works\n", "# however, as we are linearly extrapolating out, do not make\n", "# LMAX too much larger than 696\n", "# read arrays of kl, hl, and ll Love Numbers\n", - "hl,kl,ll = gravtk.read_love_numbers(love_numbers_file,\n", - " LMAX=LMAX, HEADER=header, COLUMNS=columns,\n", - " REFERENCE='CF', FORMAT='tuple')\n", + "hl, kl, ll = gravtk.read_love_numbers(\n", + " love_numbers_file,\n", + " LMAX=LMAX,\n", + " HEADER=header,\n", + " COLUMNS=columns,\n", + " REFERENCE='CF',\n", + " FORMAT='tuple',\n", + ")\n", "\n", "# read GIA data\n", "GIA = widgets.GIA.value\n", - "GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA(widgets.GIA_model,\n", - " GIA=GIA, mmax=MMAX)\n", + "GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA(\n", + " widgets.GIA_model, GIA=GIA, mmax=MMAX\n", + ")\n", "gia_str = '' if (GIA == '[None]') else f'_{GIA_Ylms_rate.title}'\n", "# calculate the monthly mass change from GIA\n", "# monthly GIA calculated by gia_rate*time elapsed\n", @@ -307,9 +328,10 @@ "# if redistributing removed mass over the ocean\n", "if widgets.redistribute_removed.value:\n", " # read Land-Sea Mask and convert to spherical harmonics\n", - " ocean_Ylms = gravtk.ocean_stokes(widgets.landmask, LMAX,\n", - " MMAX=MMAX, LOVE=(hl,kl,ll))\n", - " \n", + " ocean_Ylms = gravtk.ocean_stokes(\n", + " widgets.landmask, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll)\n", + " )\n", + "\n", "# read data to be removed from GRACE/GRACE-FO monthly harmonics\n", "remove_Ylms = GRACE_Ylms.zeros_like()\n", "remove_Ylms.time[:] = np.copy(GRACE_Ylms.time)\n", @@ -317,45 +339,45 @@ "# If there are files to be removed from the GRACE/GRACE-FO data\n", "# for each file separated by commas\n", "for f in widgets.remove_files:\n", - " if (widgets.remove_format.value == 'netCDF4'):\n", + " if widgets.remove_format.value == 'netCDF4':\n", " # read netCDF4 file\n", " Ylms = gravtk.harmonics().from_netCDF4(f)\n", - " elif (widgets.remove_format.value == 'HDF5'):\n", + " elif widgets.remove_format.value == 'HDF5':\n", " # read HDF5 file\n", " Ylms = gravtk.harmonics().from_HDF5(f)\n", - " elif (widgets.remove_format.value == 'index (ascii)'):\n", + " elif widgets.remove_format.value == 'index (ascii)':\n", " # read index of ascii files\n", - " Ylms = gravtk.harmonics().from_index(f,format='ascii')\n", - " elif (widgets.remove_format.value == 'index (netCDF4)'):\n", + " Ylms = gravtk.harmonics().from_index(f, format='ascii')\n", + " elif widgets.remove_format.value == 'index (netCDF4)':\n", " # read index of netCDF4 files\n", - " Ylms = gravtk.harmonics().from_index(f,format='netCDF4')\n", - " elif (widgets.remove_format.value == 'index (HDF5)'):\n", + " Ylms = gravtk.harmonics().from_index(f, format='netCDF4')\n", + " elif widgets.remove_format.value == 'index (HDF5)':\n", " # read index of HDF5 files\n", - " Ylms = gravtk.harmonics().from_index(f,format='HDF5')\n", + " Ylms = gravtk.harmonics().from_index(f, format='HDF5')\n", " # reduce to months of interest and truncate to range\n", - " Ylms = Ylms.subset(months).truncate(LMAX,mmax=MMAX)\n", + " Ylms = Ylms.subset(months).truncate(LMAX, mmax=MMAX)\n", " # redistribute removed mass over the ocean\n", " if widgets.redistribute_removed.value:\n", " # calculate ratio between total removed mass and\n", " # a uniformly distributed cm of water over the ocean\n", - " ratio = Ylms.clm[0,0,:]/ocean_Ylms.clm[0,0]\n", + " ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0]\n", " # for each spherical harmonic\n", - " for m in range(0,MMAX+1):\n", - " for l in range(m,LMAX+1):\n", + " for m in range(0, MMAX + 1):\n", + " for l in range(m, LMAX + 1):\n", " # remove the ratio*ocean Ylms from Ylms\n", - " Ylms.clm[l,m,:]-=ratio*ocean_Ylms.clm[l,m]\n", - " Ylms.slm[l,m,:]-=ratio*ocean_Ylms.slm[l,m]\n", + " Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m]\n", + " Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m]\n", " # add the harmonics to be removed to the total\n", " remove_Ylms.add(Ylms)\n", "\n", "# gaussian smoothing radius in km (Jekeli, 1981)\n", "RAD = widgets.gaussian.value\n", - "if (RAD != 0):\n", - " wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX)\n", + "if RAD != 0:\n", + " wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX)\n", " gw_str = f'_r{RAD:0.0f}km'\n", "else:\n", " # else = 1\n", - " wt = np.ones((LMAX+1))\n", + " wt = np.ones((LMAX + 1))\n", " gw_str = ''\n", "\n", "# destriping the GRACE/GRACE-FO harmonics\n", @@ -365,13 +387,13 @@ "UNITS = widgets.unit_index\n", "# dfactor is the degree dependent coefficients\n", "# for specific spherical harmonic output units\n", - "factors = gravtk.units(lmax=LMAX).harmonic(hl,kl,ll)\n", + "factors = gravtk.units(lmax=LMAX).harmonic(hl, kl, ll)\n", "# 1: cmwe, centimeters water equivalent\n", "# 2: mmGH, millimeters geoid height\n", "dfactor = factors.get(gravtk.units.bycode(UNITS))\n", "# units strings for output files and plots\n", "unit_label = ['cm', 'mm']\n", - "unit_name = ['Equivalent Water Thickness','Geoid Height']\n", + "unit_name = ['Equivalent Water Thickness', 'Geoid Height']\n", "\n", "# converting harmonics to truncated, smoothed coefficients in units\n", "if widgets.destripe.value:\n", @@ -383,7 +405,7 @@ "# Remove GIA estimate for month\n", "Ylms.subtract(GIA_Ylms)\n", "# smooth harmonics and convert to output units\n", - "Ylms.convolve(dfactor*wt)\n", + "Ylms.convolve(dfactor * wt)\n", "# create merged masked array\n", "triangle = Ylms.to_masked_array()" ] @@ -405,7 +427,7 @@ "source": [ "# display widgets for setting GRACE/GRACE-FO regression plot parameters\n", "cmap = gravtk.tools.colormap(vmin=-1, vmax=1)\n", - "ipywidgets.VBox([cmap.name,cmap.reverse])" + "ipywidgets.VBox([cmap.name, cmap.reverse])" ] }, { @@ -424,30 +446,59 @@ "source": [ "%matplotlib inline\n", "# plot spherical harmonics for each month\n", - "fig, ax1 = plt.subplots(num=1, figsize=(8,4))\n", + "fig, ax1 = plt.subplots(num=1, figsize=(8, 4))\n", "\n", "# levels and normalization for plot range\n", - "cmap.value.set_bad('lightgrey',1.)\n", + "cmap.value.set_bad('lightgrey', 1.0)\n", "# imshow = show image (interpolation nearest for blocks)\n", - "im = ax1.imshow(np.ma.zeros((LMAX+1,LMAX+1)), interpolation='nearest',\n", - " cmap=cmap.value, extent=(-LMAX,LMAX,LMAX,0), animated=True)\n", + "im = ax1.imshow(\n", + " np.ma.zeros((LMAX + 1, LMAX + 1)),\n", + " interpolation='nearest',\n", + " cmap=cmap.value,\n", + " extent=(-LMAX, LMAX, LMAX, 0),\n", + " animated=True,\n", + ")\n", "# Z color limit between -1 and 1\n", - "im.set_clim(-1.0,1.0)\n", + "im.set_clim(-1.0, 1.0)\n", "\n", "# add date label (year-calendar month e.g. 2002-01)\n", - "time_text = ax1.text(0.025, 0.025, '', transform=fig.transFigure,\n", - " color='k', size=24, weight='bold', ha='left', va='baseline')\n", + "time_text = ax1.text(\n", + " 0.025,\n", + " 0.025,\n", + " '',\n", + " transform=fig.transFigure,\n", + " color='k',\n", + " size=24,\n", + " weight='bold',\n", + " ha='left',\n", + " va='baseline',\n", + ")\n", "\n", "# add text to label Slm side and Clm side\n", - "t1 = ax1.text(0.39, 0.92, '$S_{lm}$', size=24, weight='bold', \n", - " transform=ax1.transAxes, ha=\"center\", va=\"center\")\n", - "t2 = ax1.text(0.61, 0.92, '$C_{lm}$', size=24, weight='bold', \n", - " transform=ax1.transAxes, ha=\"center\", va=\"center\")\n", + "t1 = ax1.text(\n", + " 0.39,\n", + " 0.92,\n", + " '$S_{lm}$',\n", + " size=24,\n", + " weight='bold',\n", + " transform=ax1.transAxes,\n", + " ha='center',\n", + " va='center',\n", + ")\n", + "t2 = ax1.text(\n", + " 0.61,\n", + " 0.92,\n", + " '$C_{lm}$',\n", + " size=24,\n", + " weight='bold',\n", + " transform=ax1.transAxes,\n", + " ha='center',\n", + " va='center',\n", + ")\n", "# add x and y labels\n", "ax1.set_ylabel('Degree [l]', fontsize=13)\n", "ax1.set_xlabel('Order [m]', fontsize=13)\n", - "ax1.tick_params(axis='both', which='both',\n", - " labelsize=13, direction='in')\n", + "ax1.tick_params(axis='both', which='both', labelsize=13, direction='in')\n", "\n", "# Add horizontal colorbar and adjust size\n", "# extend = add extension triangles to upper and lower bounds\n", @@ -455,32 +506,43 @@ "# pad = distance from main plot axis\n", "# shrink = percent size of colorbar\n", "# aspect = lengthXwidth aspect of colorbar\n", - "cbar = plt.colorbar(im, ax=ax1, extend='both', extendfrac=0.0375,\n", - " orientation='vertical', pad=0.025, shrink=0.85,\n", - " aspect=15, drawedges=False)\n", + "cbar = plt.colorbar(\n", + " im,\n", + " ax=ax1,\n", + " extend='both',\n", + " extendfrac=0.0375,\n", + " orientation='vertical',\n", + " pad=0.025,\n", + " shrink=0.85,\n", + " aspect=15,\n", + " drawedges=False,\n", + ")\n", "# rasterized colorbar to remove lines\n", "cbar.solids.set_rasterized(True)\n", "# Add label to the colorbar\n", - "cbar.ax.set_ylabel(unit_name[UNITS-1], labelpad=5, fontsize=13)\n", - "cbar.ax.set_xlabel(unit_label[UNITS-1], fontsize=13, rotation=0)\n", - "cbar.ax.xaxis.set_label_coords(0.5,1.065)\n", + "cbar.ax.set_ylabel(unit_name[UNITS - 1], labelpad=5, fontsize=13)\n", + "cbar.ax.set_xlabel(unit_label[UNITS - 1], fontsize=13, rotation=0)\n", + "cbar.ax.xaxis.set_label_coords(0.5, 1.065)\n", "# ticks lines all the way across\n", - "cbar.ax.tick_params(which='both', width=1, length=15, labelsize=13,\n", - " direction='in')\n", - " \n", + "cbar.ax.tick_params(\n", + " which='both', width=1, length=15, labelsize=13, direction='in'\n", + ")\n", + "\n", "# stronger linewidth on frame\n", "[i.set_linewidth(2.0) for i in ax1.spines.values()]\n", "# adjust subplot within figure\n", "fig.patch.set_facecolor('white')\n", - "fig.subplots_adjust(left=0.075,right=0.99,bottom=0.07,top=0.99)\n", + "fig.subplots_adjust(left=0.075, right=0.99, bottom=0.07, top=0.99)\n", + "\n", "\n", "# animate frames\n", "def animate_frames(i):\n", " # set image\n", - " im.set_data(triangle[:,:,i])\n", + " im.set_data(triangle[:, :, i])\n", " # add date label (year-calendar month e.g. 2002-01)\n", - " year,month = gravtk.time.grace_to_calendar(Ylms.month[i])\n", - " time_text.set_text(u'{0:4d}\\u2013{1:02d}'.format(year,month))\n", + " year, month = gravtk.time.grace_to_calendar(Ylms.month[i])\n", + " time_text.set_text('{0:4d}\\u2013{1:02d}'.format(year, month))\n", + "\n", "\n", "# set animation\n", "anim = animation.FuncAnimation(fig, animate_frames, frames=nt)\n", diff --git a/doc/source/notebooks/GRACE-Spatial-Error.ipynb b/doc/source/notebooks/GRACE-Spatial-Error.ipynb index b2cf733e..b66aafe7 100644 --- a/doc/source/notebooks/GRACE-Spatial-Error.ipynb +++ b/doc/source/notebooks/GRACE-Spatial-Error.ipynb @@ -26,6 +26,7 @@ "source": [ "import numpy as np\n", "import matplotlib\n", + "\n", "matplotlib.rcParams['mathtext.default'] = 'regular'\n", "import matplotlib.pyplot as plt\n", "import cartopy.crs as ccrs\n", @@ -54,11 +55,7 @@ "# set the directory with GRACE/GRACE-FO data\n", "# update local data with PO.DAAC https servers\n", "widgets = gravtk.tools.widgets()\n", - "ipywidgets.VBox([\n", - " widgets.directory,\n", - " widgets.update,\n", - " widgets.endpoint\n", - "])" + "ipywidgets.VBox([widgets.directory, widgets.update, widgets.endpoint])" ] }, { @@ -119,12 +116,9 @@ "# update widgets\n", "widgets.select_product()\n", "# display widgets for setting GRACE/GRACE-FO parameters\n", - "ipywidgets.VBox([\n", - " widgets.center,\n", - " widgets.release,\n", - " widgets.product,\n", - " widgets.months\n", - "])" + "ipywidgets.VBox(\n", + " [widgets.center, widgets.release, widgets.product, widgets.months]\n", + ")" ] }, { @@ -154,19 +148,21 @@ "# update widgets\n", "widgets.select_options()\n", "# display widgets for setting GRACE/GRACE-FO read parameters\n", - "ipywidgets.VBox([\n", - " widgets.lmax,\n", - " widgets.mmax,\n", - " widgets.geocenter,\n", - " widgets.C20,\n", - " widgets.CS21,\n", - " widgets.CS22,\n", - " widgets.C30,\n", - " widgets.C40,\n", - " widgets.C50,\n", - " widgets.pole_tide,\n", - " widgets.atm,\n", - "])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.lmax,\n", + " widgets.mmax,\n", + " widgets.geocenter,\n", + " widgets.C20,\n", + " widgets.CS21,\n", + " widgets.CS22,\n", + " widgets.C30,\n", + " widgets.C40,\n", + " widgets.C50,\n", + " widgets.pole_tide,\n", + " widgets.atm,\n", + " ]\n", + ")" ] }, { @@ -204,11 +200,27 @@ "# read GRACE/GRACE-FO data for parameters\n", "start_mon = np.min(months)\n", "end_mon = np.max(months)\n", - "missing = sorted(set(np.arange(start_mon,end_mon+1)) - set(months))\n", - "Ylms = gravtk.grace_input_months(widgets.base_directory, PROC, DREL, DSET,\n", - " LMAX, start_mon, end_mon, missing, SLR_C20, DEG1, MMAX=MMAX,\n", - " SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40,\n", - " SLR_C50=SLR_C50, POLE_TIDE=POLE_TIDE, ATM=ATM)\n", + "missing = sorted(set(np.arange(start_mon, end_mon + 1)) - set(months))\n", + "Ylms = gravtk.grace_input_months(\n", + " widgets.base_directory,\n", + " PROC,\n", + " DREL,\n", + " DSET,\n", + " LMAX,\n", + " start_mon,\n", + " end_mon,\n", + " missing,\n", + " SLR_C20,\n", + " DEG1,\n", + " MMAX=MMAX,\n", + " SLR_21=SLR_21,\n", + " SLR_22=SLR_22,\n", + " SLR_C30=SLR_C30,\n", + " SLR_C40=SLR_C40,\n", + " SLR_C50=SLR_C50,\n", + " POLE_TIDE=POLE_TIDE,\n", + " ATM=ATM,\n", + ")\n", "# create harmonics object and remove mean\n", "GRACE_Ylms = gravtk.harmonics().from_dict(Ylms)\n", "GRACE_Ylms.mean(apply=True)\n", @@ -241,11 +253,9 @@ "# update widgets\n", "widgets.select_corrections()\n", "# display widgets for setting GRACE/GRACE-FO corrections parameters\n", - "ipywidgets.VBox([\n", - " widgets.gaussian,\n", - " widgets.destripe,\n", - " widgets.spacing,\n", - " widgets.interval])" + "ipywidgets.VBox(\n", + " [widgets.gaussian, widgets.destripe, widgets.spacing, widgets.interval]\n", + ")" ] }, { @@ -274,16 +284,16 @@ "dlat = widgets.spacing.value\n", "# Output Degree Interval\n", "INTERVAL = widgets.interval.index + 1\n", - "if (INTERVAL == 1):\n", + "if INTERVAL == 1:\n", " # (-180:180,90:-90)\n", - " nlon = np.int64((360.0/dlon)+1.0)\n", - " nlat = np.int64((180.0/dlat)+1.0)\n", - " grid.lon = -180 + dlon*np.arange(0,nlon)\n", - " grid.lat = 90.0 - dlat*np.arange(0,nlat)\n", - "elif (INTERVAL == 2):\n", + " nlon = np.int64((360.0 / dlon) + 1.0)\n", + " nlat = np.int64((180.0 / dlat) + 1.0)\n", + " grid.lon = -180 + dlon * np.arange(0, nlon)\n", + " grid.lat = 90.0 - dlat * np.arange(0, nlat)\n", + "elif INTERVAL == 2:\n", " # (Degree spacing)/2\n", - " grid.lon = np.arange(-180+dlon/2.0,180+dlon/2.0,dlon)\n", - " grid.lat = np.arange(90.0-dlat/2.0,-90.0-dlat/2.0,-dlat)\n", + " grid.lon = np.arange(-180 + dlon / 2.0, 180 + dlon / 2.0, dlon)\n", + " grid.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat)\n", " nlon = len(grid.lon)\n", " nlat = len(grid.lat)\n", "\n", @@ -291,34 +301,40 @@ "theta = np.radians(90.0 - grid.lat)\n", "PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(theta))\n", "# square of legendre polynomials truncated to order MMAX\n", - "mm = np.arange(0,MMAX+1)\n", - "PLM2 = PLM[:,mm,:]**2\n", + "mm = np.arange(0, MMAX + 1)\n", + "PLM2 = PLM[:, mm, :] ** 2\n", "# Calculating cos(m*phi)^2 and sin(m*phi)^2\n", - "phi = np.radians(grid.lon[np.newaxis,:])\n", - "ccos = np.cos(np.dot(mm[:,np.newaxis],phi))**2\n", - "ssin = np.sin(np.dot(mm[:,np.newaxis],phi))**2\n", - " \n", + "phi = np.radians(grid.lon[np.newaxis, :])\n", + "ccos = np.cos(np.dot(mm[:, np.newaxis], phi)) ** 2\n", + "ssin = np.sin(np.dot(mm[:, np.newaxis], phi)) ** 2\n", + "\n", "# read load love numbers file\n", "# PREM outputs from Han and Wahr (1995)\n", "# https://doi.org/10.1111/j.1365-246X.1995.tb01819.x\n", - "love_numbers_file = gravtk.utilities.get_data_path(['data','love_numbers'])\n", + "love_numbers_file = gravtk.utilities.get_data_path(['data', 'love_numbers'])\n", "header = 2\n", - "columns = ['l','hl','kl','ll']\n", + "columns = ['l', 'hl', 'kl', 'll']\n", "# LMAX of load love numbers from Han and Wahr (1995) is 696.\n", "# from Wahr (2007) linearly interpolating kl works\n", "# however, as we are linearly extrapolating out, do not make\n", "# LMAX too much larger than 696\n", "# read arrays of kl, hl, and ll Love Numbers\n", - "hl,kl,ll = gravtk.read_love_numbers(love_numbers_file, LMAX=LMAX,\n", - " HEADER=header, COLUMNS=columns, REFERENCE='CF', FORMAT='tuple')\n", + "hl, kl, ll = gravtk.read_love_numbers(\n", + " love_numbers_file,\n", + " LMAX=LMAX,\n", + " HEADER=header,\n", + " COLUMNS=columns,\n", + " REFERENCE='CF',\n", + " FORMAT='tuple',\n", + ")\n", "\n", "# gaussian smoothing radius in km (Jekeli, 1981)\n", "RAD = widgets.gaussian.value\n", - "if (RAD != 0):\n", - " wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX)\n", + "if RAD != 0:\n", + " wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX)\n", "else:\n", " # else = 1\n", - " wt = np.ones((LMAX+1))\n", + " wt = np.ones((LMAX + 1))\n", "\n", "# destriping the GRACE/GRACE-FO harmonics\n", "if widgets.destripe.value:\n", @@ -328,7 +344,7 @@ "\n", "# dfactor is the degree dependent coefficients\n", "# for converting to spherical harmonic output units\n", - "factors = gravtk.units(lmax=LMAX).harmonic(hl,kl,ll).mmwe\n", + "factors = gravtk.units(lmax=LMAX).harmonic(hl, kl, ll).mmwe\n", "# mmwe, millimeters water equivalent\n", "dfactor = factors.get('mmwe')\n", "# units strings for output plots\n", @@ -336,50 +352,51 @@ "unit_name = 'Equivalent Water Thickness'\n", "\n", "# Delta coefficients of GRACE time series (Error components)\n", - "delta_Ylms = gravtk.harmonics(lmax=LMAX,mmax=MMAX)\n", - "delta_Ylms.clm = np.zeros((LMAX+1, MMAX+1))\n", - "delta_Ylms.slm = np.zeros((LMAX+1, MMAX+1))\n", + "delta_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX)\n", + "delta_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1))\n", + "delta_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1))\n", "# Smoothing Half-Width (CNES is a 10-day solution)\n", "# All other solutions are monthly solutions (HFWTH for annual = 6)\n", - "if ((PROC == 'CNES') and (DREL in ('RL01','RL02'))):\n", + "if (PROC == 'CNES') and (DREL in ('RL01', 'RL02')):\n", " HFWTH = 19\n", "else:\n", " HFWTH = 6\n", "# Equal to the noise of the smoothed time-series\n", "# for each spherical harmonic order\n", - "for m in range(0,MMAX+1):# MMAX+1 to include MMAX\n", + "for m in range(0, MMAX + 1): # MMAX+1 to include MMAX\n", " # for each spherical harmonic degree\n", - " for l in range(m,LMAX+1):# LMAX+1 to include LMAX\n", + " for l in range(m, LMAX + 1): # LMAX+1 to include LMAX\n", " # Delta coefficients of GRACE time series\n", - " for cs,csharm in enumerate(['clm','slm']):\n", + " for cs, csharm in enumerate(['clm', 'slm']):\n", " # calculate GRACE Error (Noise of smoothed time-series)\n", " # With Annual and Semi-Annual Terms\n", " val1 = getattr(Ylms, csharm)\n", - " smth = gravtk.time_series.smooth(Ylms.time, val1[l,m,:],\n", - " HFWTH=HFWTH)\n", + " smth = gravtk.time_series.smooth(\n", + " Ylms.time, val1[l, m, :], HFWTH=HFWTH\n", + " )\n", " # number of smoothed points\n", " nsmth = len(smth['data'])\n", " tsmth = np.mean(smth['time'])\n", " # GRACE delta Ylms\n", " # variance of data-(smoothed+annual+semi)\n", " val2 = getattr(delta_Ylms, csharm)\n", - " val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth)\n", - " \n", + " val2[l, m] = np.sqrt(np.sum(smth['noise'] ** 2) / nsmth)\n", + "\n", "# convolve delta harmonics with degree dependent factors\n", - "delta_Ylms = delta_Ylms.convolve(dfactor*wt)\n", + "delta_Ylms = delta_Ylms.convolve(dfactor * wt)\n", "# smooth harmonics and convert to output units\n", - "YLM2 = delta_Ylms.power(2.0).scale(1.0/nsmth)\n", + "YLM2 = delta_Ylms.power(2.0).scale(1.0 / nsmth)\n", "# Calculate fourier coefficients\n", - "d_cos = np.zeros((MMAX+1,nlat))# [m,th]\n", - "d_sin = np.zeros((MMAX+1,nlat))# [m,th]\n", + "d_cos = np.zeros((MMAX + 1, nlat)) # [m,th]\n", + "d_sin = np.zeros((MMAX + 1, nlat)) # [m,th]\n", "# Calculating delta spatial values\n", - "for k in range(0,nlat):\n", + "for k in range(0, nlat):\n", " # summation over all spherical harmonic degrees\n", - " d_cos[:,k] = np.sum(PLM2[:,:,k]*YLM2.clm, axis=0)\n", - " d_sin[:,k] = np.sum(PLM2[:,:,k]*YLM2.slm, axis=0)\n", + " d_cos[:, k] = np.sum(PLM2[:, :, k] * YLM2.clm, axis=0)\n", + " d_sin[:, k] = np.sum(PLM2[:, :, k] * YLM2.slm, axis=0)\n", "\n", "# Multiplying by c/s(phi#m) to get spatial maps (lon,lat)\n", - "grid.data = np.sqrt(np.dot(ccos.T,d_cos) + np.dot(ssin.T,d_sin)).T\n", + "grid.data = np.sqrt(np.dot(ccos.T, d_cos) + np.dot(ssin.T, d_sin)).T\n", "grid.mask = np.zeros_like(grid.data, dtype=bool)" ] }, @@ -426,7 +443,7 @@ "vmax = np.ceil(np.max(grid.data)).astype(np.int64)\n", "cmap = gravtk.tools.colormap(vmin=0, vmax=vmax)\n", "# display widgets for setting GRACE/GRACE-FO plot parameters\n", - "ipywidgets.VBox([cmap.range,cmap.step,cmap.name,cmap.reverse])" + "ipywidgets.VBox([cmap.range, cmap.step, cmap.name, cmap.reverse])" ] }, { @@ -435,13 +452,24 @@ "metadata": {}, "outputs": [], "source": [ - "fig, ax2 = plt.subplots(num=2, nrows=1, ncols=1, figsize=(10.375,6.625),\n", - " subplot_kw=dict(projection=ccrs.PlateCarree()))\n", + "fig, ax2 = plt.subplots(\n", + " num=2,\n", + " nrows=1,\n", + " ncols=1,\n", + " figsize=(10.375, 6.625),\n", + " subplot_kw=dict(projection=ccrs.PlateCarree()),\n", + ")\n", "\n", "# levels and normalization for plot range\n", - "im = ax2.imshow(grid.data, interpolation='nearest',\n", - " norm=cmap.norm, cmap=cmap.value, transform=ccrs.PlateCarree(),\n", - " extent=grid.extent, origin='upper')\n", + "im = ax2.imshow(\n", + " grid.data,\n", + " interpolation='nearest',\n", + " norm=cmap.norm,\n", + " cmap=cmap.value,\n", + " transform=ccrs.PlateCarree(),\n", + " extent=grid.extent,\n", + " origin='upper',\n", + ")\n", "ax2.coastlines('50m')\n", "\n", "# Add horizontal colorbar and adjust size\n", @@ -450,27 +478,35 @@ "# pad = distance from main plot axis\n", "# shrink = percent size of colorbar\n", "# aspect = lengthXwidth aspect of colorbar\n", - "cbar = plt.colorbar(im, ax=ax2, extend='both', extendfrac=0.0375,\n", - " orientation='horizontal', pad=0.025, shrink=0.85,\n", - " aspect=22, drawedges=False)\n", + "cbar = plt.colorbar(\n", + " im,\n", + " ax=ax2,\n", + " extend='both',\n", + " extendfrac=0.0375,\n", + " orientation='horizontal',\n", + " pad=0.025,\n", + " shrink=0.85,\n", + " aspect=22,\n", + " drawedges=False,\n", + ")\n", "# rasterized colorbar to remove lines\n", "cbar.solids.set_rasterized(True)\n", "# Add label to the colorbar\n", - "cbar.ax.set_xlabel(f'{unit_name} [{unit_label}]',\n", - " labelpad=10, fontsize=24)\n", + "cbar.ax.set_xlabel(f'{unit_name} [{unit_label}]', labelpad=10, fontsize=24)\n", "# Set the tick levels for the colorbar\n", "cbar.set_ticks(cmap.levels)\n", "cbar.set_ticklabels(cmap.label)\n", "# ticks lines all the way across\n", - "cbar.ax.tick_params(which='both', width=1, length=26, labelsize=24,\n", - " direction='in')\n", - " \n", + "cbar.ax.tick_params(\n", + " which='both', width=1, length=26, labelsize=24, direction='in'\n", + ")\n", + "\n", "# stronger linewidth on frame\n", "ax2.spines['geo'].set_linewidth(2.0)\n", "ax2.spines['geo'].set_capstyle('projecting')\n", "# adjust subplot within figure\n", "fig.patch.set_facecolor('white')\n", - "fig.subplots_adjust(left=0.02,right=0.98,bottom=0.05,top=0.98)\n", + "fig.subplots_adjust(left=0.02, right=0.98, bottom=0.05, top=0.98)\n", "plt.show()" ] } diff --git a/doc/source/notebooks/GRACE-Spatial-Maps.ipynb b/doc/source/notebooks/GRACE-Spatial-Maps.ipynb index c8802c90..a4361aee 100644 --- a/doc/source/notebooks/GRACE-Spatial-Maps.ipynb +++ b/doc/source/notebooks/GRACE-Spatial-Maps.ipynb @@ -37,9 +37,10 @@ "source": [ "import numpy as np\n", "import matplotlib\n", + "\n", "matplotlib.rcParams['mathtext.default'] = 'regular'\n", - "matplotlib.rcParams[\"animation.html\"] = \"jshtml\"\n", - "matplotlib.rcParams[\"animation.embed_limit\"] = 50\n", + "matplotlib.rcParams['animation.html'] = 'jshtml'\n", + "matplotlib.rcParams['animation.embed_limit'] = 50\n", "import matplotlib.pyplot as plt\n", "import matplotlib.animation as animation\n", "import cartopy.crs as ccrs\n", @@ -69,11 +70,7 @@ "# set the directory with GRACE/GRACE-FO data\n", "# update local data with PO.DAAC https servers\n", "widgets = gravtk.tools.widgets()\n", - "ipywidgets.VBox([\n", - " widgets.directory,\n", - " widgets.update,\n", - " widgets.endpoint\n", - "])" + "ipywidgets.VBox([widgets.directory, widgets.update, widgets.endpoint])" ] }, { @@ -134,12 +131,9 @@ "# update widgets\n", "widgets.select_product()\n", "# display widgets for setting GRACE/GRACE-FO parameters\n", - "ipywidgets.VBox([\n", - " widgets.center,\n", - " widgets.release,\n", - " widgets.product,\n", - " widgets.months\n", - "])" + "ipywidgets.VBox(\n", + " [widgets.center, widgets.release, widgets.product, widgets.months]\n", + ")" ] }, { @@ -193,19 +187,21 @@ "# update widgets\n", "widgets.select_options()\n", "# display widgets for setting GRACE/GRACE-FO read parameters\n", - "ipywidgets.VBox([\n", - " widgets.lmax,\n", - " widgets.mmax,\n", - " widgets.geocenter,\n", - " widgets.C20,\n", - " widgets.CS21,\n", - " widgets.CS22,\n", - " widgets.C30,\n", - " widgets.C40,\n", - " widgets.C50,\n", - " widgets.pole_tide,\n", - " widgets.atm\n", - "])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.lmax,\n", + " widgets.mmax,\n", + " widgets.geocenter,\n", + " widgets.C20,\n", + " widgets.CS21,\n", + " widgets.CS22,\n", + " widgets.C30,\n", + " widgets.C40,\n", + " widgets.C50,\n", + " widgets.pole_tide,\n", + " widgets.atm,\n", + " ]\n", + ")" ] }, { @@ -243,11 +239,27 @@ "# read GRACE/GRACE-FO data for parameters\n", "start_mon = np.min(months)\n", "end_mon = np.max(months)\n", - "missing = sorted(set(np.arange(start_mon,end_mon+1)) - set(months))\n", - "Ylms = gravtk.grace_input_months(widgets.base_directory, PROC, DREL, DSET,\n", - " LMAX, start_mon, end_mon, missing, SLR_C20, DEG1, MMAX=MMAX,\n", - " SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40,\n", - " SLR_C50=SLR_C50, POLE_TIDE=POLE_TIDE, ATM=ATM)\n", + "missing = sorted(set(np.arange(start_mon, end_mon + 1)) - set(months))\n", + "Ylms = gravtk.grace_input_months(\n", + " widgets.base_directory,\n", + " PROC,\n", + " DREL,\n", + " DSET,\n", + " LMAX,\n", + " start_mon,\n", + " end_mon,\n", + " missing,\n", + " SLR_C20,\n", + " DEG1,\n", + " MMAX=MMAX,\n", + " SLR_21=SLR_21,\n", + " SLR_22=SLR_22,\n", + " SLR_C30=SLR_C30,\n", + " SLR_C40=SLR_C40,\n", + " SLR_C50=SLR_C50,\n", + " POLE_TIDE=POLE_TIDE,\n", + " ATM=ATM,\n", + ")\n", "# create harmonics object and remove mean\n", "GRACE_Ylms = gravtk.harmonics().from_dict(Ylms)\n", "GRACE_Ylms.mean(apply=True)\n", @@ -343,19 +355,22 @@ "widgets.select_corrections()\n", "widgets.select_output()\n", "# display widgets for setting GRACE/GRACE-FO corrections parameters\n", - "ipywidgets.VBox([\n", - " widgets.GIA_file,\n", - " widgets.GIA,\n", - " widgets.remove_file,\n", - " widgets.remove_format,\n", - " widgets.redistribute_removed,\n", - " widgets.mask,\n", - " widgets.gaussian,\n", - " widgets.destripe,\n", - " widgets.spacing,\n", - " widgets.interval,\n", - " widgets.units,\n", - " widgets.output_format])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.GIA_file,\n", + " widgets.GIA,\n", + " widgets.remove_file,\n", + " widgets.remove_format,\n", + " widgets.redistribute_removed,\n", + " widgets.mask,\n", + " widgets.gaussian,\n", + " widgets.destripe,\n", + " widgets.spacing,\n", + " widgets.interval,\n", + " widgets.units,\n", + " widgets.output_format,\n", + " ]\n", + ")" ] }, { @@ -391,41 +406,48 @@ "dlat = widgets.spacing.value\n", "# Output Degree Interval\n", "INTERVAL = widgets.interval.index + 1\n", - "if (INTERVAL == 1):\n", + "if INTERVAL == 1:\n", " # (-180:180,90:-90)\n", - " nlon = np.int64((360.0/dlon)+1.0)\n", - " nlat = np.int64((180.0/dlat)+1.0)\n", - " grid.lon = -180 + dlon*np.arange(0,nlon)\n", - " grid.lat = 90.0 - dlat*np.arange(0,nlat)\n", - "elif (INTERVAL == 2):\n", + " nlon = np.int64((360.0 / dlon) + 1.0)\n", + " nlat = np.int64((180.0 / dlat) + 1.0)\n", + " grid.lon = -180 + dlon * np.arange(0, nlon)\n", + " grid.lat = 90.0 - dlat * np.arange(0, nlat)\n", + "elif INTERVAL == 2:\n", " # (Degree spacing)/2\n", - " grid.lon = np.arange(-180+dlon/2.0,180+dlon/2.0,dlon)\n", - " grid.lat = np.arange(90.0-dlat/2.0,-90.0-dlat/2.0,-dlat)\n", + " grid.lon = np.arange(-180 + dlon / 2.0, 180 + dlon / 2.0, dlon)\n", + " grid.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat)\n", " nlon = len(grid.lon)\n", " nlat = len(grid.lat)\n", "\n", "# Computing plms for converting to spatial domain\n", - "theta = np.radians(90.0-grid.lat)\n", + "theta = np.radians(90.0 - grid.lat)\n", "PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(theta))\n", "\n", "# read load love numbers file\n", "# PREM outputs from Han and Wahr (1995)\n", "# https://doi.org/10.1111/j.1365-246X.1995.tb01819.x\n", - "love_numbers_file = gravtk.utilities.get_data_path(['data','love_numbers'])\n", + "love_numbers_file = gravtk.utilities.get_data_path(['data', 'love_numbers'])\n", "header = 2\n", - "columns = ['l','hl','kl','ll']\n", + "columns = ['l', 'hl', 'kl', 'll']\n", "# LMAX of load love numbers from Han and Wahr (1995) is 696.\n", "# from Wahr (2007) linearly interpolating kl works\n", "# however, as we are linearly extrapolating out, do not make\n", "# LMAX too much larger than 696\n", "# read arrays of kl, hl, and ll Love Numbers\n", - "hl,kl,ll = gravtk.read_love_numbers(love_numbers_file, LMAX=LMAX,\n", - " HEADER=header, COLUMNS=columns, REFERENCE='CF', FORMAT='tuple')\n", + "hl, kl, ll = gravtk.read_love_numbers(\n", + " love_numbers_file,\n", + " LMAX=LMAX,\n", + " HEADER=header,\n", + " COLUMNS=columns,\n", + " REFERENCE='CF',\n", + " FORMAT='tuple',\n", + ")\n", "\n", "# read GIA data\n", "GIA = widgets.GIA.value\n", - "GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA(widgets.GIA_model,\n", - " GIA=GIA, mmax=MMAX)\n", + "GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA(\n", + " widgets.GIA_model, GIA=GIA, mmax=MMAX\n", + ")\n", "gia_str = '' if (GIA == '[None]') else f'_{GIA_Ylms_rate.title}'\n", "# calculate the monthly mass change from GIA\n", "# monthly GIA calculated by gia_rate*time elapsed\n", @@ -436,9 +458,10 @@ "# if redistributing removed mass over the ocean\n", "if widgets.redistribute_removed.value:\n", " # read Land-Sea Mask and convert to spherical harmonics\n", - " ocean_Ylms = gravtk.ocean_stokes(widgets.landmask, LMAX,\n", - " MMAX=MMAX, LOVE=(hl,kl,ll))\n", - " \n", + " ocean_Ylms = gravtk.ocean_stokes(\n", + " widgets.landmask, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll)\n", + " )\n", + "\n", "# read data to be removed from GRACE/GRACE-FO monthly harmonics\n", "remove_Ylms = GRACE_Ylms.zeros_like()\n", "remove_Ylms.time[:] = np.copy(GRACE_Ylms.time)\n", @@ -446,45 +469,45 @@ "# If there are files to be removed from the GRACE/GRACE-FO data\n", "# for each file separated by commas\n", "for f in widgets.remove_files:\n", - " if (widgets.remove_format.value == 'netCDF4'):\n", + " if widgets.remove_format.value == 'netCDF4':\n", " # read netCDF4 file\n", " Ylms = gravtk.harmonics().from_netCDF4(f)\n", - " elif (widgets.remove_format.value == 'HDF5'):\n", + " elif widgets.remove_format.value == 'HDF5':\n", " # read HDF5 file\n", " Ylms = gravtk.harmonics().from_HDF5(f)\n", - " elif (widgets.remove_format.value == 'index (ascii)'):\n", + " elif widgets.remove_format.value == 'index (ascii)':\n", " # read index of ascii files\n", - " Ylms = gravtk.harmonics().from_index(f,format='ascii')\n", - " elif (widgets.remove_format.value == 'index (netCDF4)'):\n", + " Ylms = gravtk.harmonics().from_index(f, format='ascii')\n", + " elif widgets.remove_format.value == 'index (netCDF4)':\n", " # read index of netCDF4 files\n", - " Ylms = gravtk.harmonics().from_index(f,format='netCDF4')\n", - " elif (widgets.remove_format.value == 'index (HDF5)'):\n", + " Ylms = gravtk.harmonics().from_index(f, format='netCDF4')\n", + " elif widgets.remove_format.value == 'index (HDF5)':\n", " # read index of HDF5 files\n", - " Ylms = gravtk.harmonics().from_index(f,format='HDF5')\n", + " Ylms = gravtk.harmonics().from_index(f, format='HDF5')\n", " # reduce to months of interest and truncate to range\n", - " Ylms = Ylms.subset(months).truncate(LMAX,mmax=MMAX)\n", + " Ylms = Ylms.subset(months).truncate(LMAX, mmax=MMAX)\n", " # redistribute removed mass over the ocean\n", " if widgets.redistribute_removed.value:\n", " # calculate ratio between total removed mass and\n", " # a uniformly distributed cm of water over the ocean\n", - " ratio = Ylms.clm[0,0,:]/ocean_Ylms.clm[0,0]\n", + " ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0]\n", " # for each spherical harmonic\n", - " for m in range(0,MMAX+1):\n", - " for l in range(m,LMAX+1):\n", + " for m in range(0, MMAX + 1):\n", + " for l in range(m, LMAX + 1):\n", " # remove the ratio*ocean Ylms from Ylms\n", - " Ylms.clm[l,m,:]-=ratio*ocean_Ylms.clm[l,m]\n", - " Ylms.slm[l,m,:]-=ratio*ocean_Ylms.slm[l,m]\n", + " Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m]\n", + " Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m]\n", " # add the harmonics to be removed to the total\n", " remove_Ylms.add(Ylms)\n", "\n", "# gaussian smoothing radius in km (Jekeli, 1981)\n", "RAD = widgets.gaussian.value\n", - "if (RAD != 0):\n", - " wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX)\n", + "if RAD != 0:\n", + " wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX)\n", " gw_str = f'_r{RAD:0.0f}km'\n", "else:\n", " # else = 1\n", - " wt = np.ones((LMAX+1))\n", + " wt = np.ones((LMAX + 1))\n", " gw_str = ''\n", "\n", "# destriping the GRACE/GRACE-FO harmonics\n", @@ -494,7 +517,7 @@ "UNITS = widgets.unit_index\n", "# dfactor is the degree dependent coefficients\n", "# for specific spherical harmonic output units\n", - "factors = gravtk.units(lmax=LMAX).harmonic(hl,kl,ll)\n", + "factors = gravtk.units(lmax=LMAX).harmonic(hl, kl, ll)\n", "# 1: cmwe, centimeters water equivalent\n", "# 2: mmGH, millimeters geoid height\n", "# 3: mmCU, millimeters elastic crustal deformation\n", @@ -502,17 +525,21 @@ "# 5: mbar, millibars equivalent surface pressure\n", "dfactor = factors.get(gravtk.units.bycode(UNITS))\n", "# units strings for output files and plots\n", - "unit_label = ['cm', 'mm', 'mm', u'\\u03BCGal', 'mb']\n", - "unit_name = ['Equivalent Water Thickness', 'Geoid Height',\n", - " 'Elastic Crustal Uplift', 'Gravitational Undulation',\n", - " 'Equivalent Surface Pressure']\n", + "unit_label = ['cm', 'mm', 'mm', '\\u03bcGal', 'mb']\n", + "unit_name = [\n", + " 'Equivalent Water Thickness',\n", + " 'Geoid Height',\n", + " 'Elastic Crustal Uplift',\n", + " 'Gravitational Undulation',\n", + " 'Equivalent Surface Pressure',\n", + "]\n", "\n", "# converting harmonics to truncated, smoothed coefficients in units\n", "# combining harmonics to calculate output spatial fields\n", "# output spatial grid\n", "grid.data = np.zeros((nlat, nlon, nt))\n", "grid.mask = np.zeros((nlat, nlon, nt), dtype=bool)\n", - "for i,grace_month in enumerate(GRACE_Ylms.month):\n", + "for i, grace_month in enumerate(GRACE_Ylms.month):\n", " # GRACE/GRACE-FO harmonics for time t\n", " # and monthly files to be removed\n", " if widgets.destripe.value:\n", @@ -524,10 +551,11 @@ " # Remove GIA rate for time\n", " Ylms.subtract(GIA_Ylms.index(i))\n", " # smooth harmonics and convert to output units\n", - " Ylms.convolve(dfactor*wt)\n", + " Ylms.convolve(dfactor * wt)\n", " # convert spherical harmonics to output spatial grid\n", - " grid.data[:,:,i] = gravtk.harmonic_summation(Ylms.clm, Ylms.slm,\n", - " grid.lon, grid.lat, LMAX=LMAX, MMAX=MMAX, PLM=PLM).T" + " grid.data[:, :, i] = gravtk.harmonic_summation(\n", + " Ylms.clm, Ylms.slm, grid.lon, grid.lat, LMAX=LMAX, MMAX=MMAX, PLM=PLM\n", + " ).T" ] }, { @@ -545,16 +573,33 @@ "outputs": [], "source": [ "# output to netCDF4 or HDF5\n", - "suffix = dict(netCDF4='nc',HDF5='H5')\n", + "suffix = dict(netCDF4='nc', HDF5='H5')\n", "file_format = '{0}_{1}_{2}{3}{4}_{5}_L{6:d}{7}{8}{9}_{10:03d}-{11:03d}.{12}'\n", - "if widgets.format in ('netCDF4','HDF5'):\n", - " FILE = file_format.format(PROC,DREL,DSET,gia_str,GRACE_Ylms.title,\n", - " widgets.units.value,LMAX,order_str,gw_str,ds_str,\n", - " months[0],months[-1],suffix[widgets.format])\n", - " grid.to_file(GRACE_Ylms.directory.joinpath(FILE),\n", - " format=widgets.format, varname='z',\n", - " units=widgets.units.value, longname=unit_name[UNITS-1],\n", - " title='GRACE/GRACE-FO Spatial Data', date=True)\n" + "if widgets.format in ('netCDF4', 'HDF5'):\n", + " FILE = file_format.format(\n", + " PROC,\n", + " DREL,\n", + " DSET,\n", + " gia_str,\n", + " GRACE_Ylms.title,\n", + " widgets.units.value,\n", + " LMAX,\n", + " order_str,\n", + " gw_str,\n", + " ds_str,\n", + " months[0],\n", + " months[-1],\n", + " suffix[widgets.format],\n", + " )\n", + " grid.to_file(\n", + " GRACE_Ylms.directory.joinpath(FILE),\n", + " format=widgets.format,\n", + " varname='z',\n", + " units=widgets.units.value,\n", + " longname=unit_name[UNITS - 1],\n", + " title='GRACE/GRACE-FO Spatial Data',\n", + " date=True,\n", + " )" ] }, { @@ -576,7 +621,7 @@ "vmax = np.ceil(np.max(grid.data)).astype(np.int64)\n", "cmap1 = gravtk.tools.colormap(vmin=vmin, vmax=vmax)\n", "# display widgets for setting GRACE/GRACE-FO regression plot parameters\n", - "ipywidgets.VBox([cmap1.range,cmap1.step,cmap1.name,cmap1.reverse])" + "ipywidgets.VBox([cmap1.range, cmap1.step, cmap1.name, cmap1.reverse])" ] }, { @@ -586,18 +631,39 @@ "outputs": [], "source": [ "%matplotlib inline\n", - "fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(10.375,6.625),\n", - " subplot_kw=dict(projection=ccrs.PlateCarree()))\n", + "fig, ax1 = plt.subplots(\n", + " num=1,\n", + " nrows=1,\n", + " ncols=1,\n", + " figsize=(10.375, 6.625),\n", + " subplot_kw=dict(projection=ccrs.PlateCarree()),\n", + ")\n", "\n", "# levels and normalization for plot range\n", - "im = ax1.imshow(np.zeros((nlat, nlon)), interpolation='nearest',\n", - " norm=cmap1.norm, cmap=cmap1.value, transform=ccrs.PlateCarree(),\n", - " extent=grid.extent, origin='upper', animated=True)\n", + "im = ax1.imshow(\n", + " np.zeros((nlat, nlon)),\n", + " interpolation='nearest',\n", + " norm=cmap1.norm,\n", + " cmap=cmap1.value,\n", + " transform=ccrs.PlateCarree(),\n", + " extent=grid.extent,\n", + " origin='upper',\n", + " animated=True,\n", + ")\n", "ax1.coastlines('50m')\n", "\n", "# add date label (year-calendar month e.g. 2002-01)\n", - "time_text = ax1.text(0.025, 0.025, '', transform=fig.transFigure,\n", - " color='k', size=24, weight='bold', ha='left', va='baseline')\n", + "time_text = ax1.text(\n", + " 0.025,\n", + " 0.025,\n", + " '',\n", + " transform=fig.transFigure,\n", + " color='k',\n", + " size=24,\n", + " weight='bold',\n", + " ha='left',\n", + " va='baseline',\n", + ")\n", "\n", "# Add horizontal colorbar and adjust size\n", "# extend = add extension triangles to upper and lower bounds\n", @@ -605,36 +671,47 @@ "# pad = distance from main plot axis\n", "# shrink = percent size of colorbar\n", "# aspect = lengthXwidth aspect of colorbar\n", - "cbar = plt.colorbar(im, ax=ax1, extend='both', extendfrac=0.0375,\n", - " orientation='horizontal', pad=0.025, shrink=0.85,\n", - " aspect=22, drawedges=False)\n", + "cbar = plt.colorbar(\n", + " im,\n", + " ax=ax1,\n", + " extend='both',\n", + " extendfrac=0.0375,\n", + " orientation='horizontal',\n", + " pad=0.025,\n", + " shrink=0.85,\n", + " aspect=22,\n", + " drawedges=False,\n", + ")\n", "# rasterized colorbar to remove lines\n", "cbar.solids.set_rasterized(True)\n", "# Add label to the colorbar\n", - "cbar.ax.set_xlabel(unit_name[UNITS-1], labelpad=10, fontsize=24)\n", - "cbar.ax.set_ylabel(unit_label[UNITS-1], fontsize=24, rotation=0)\n", + "cbar.ax.set_xlabel(unit_name[UNITS - 1], labelpad=10, fontsize=24)\n", + "cbar.ax.set_ylabel(unit_label[UNITS - 1], fontsize=24, rotation=0)\n", "cbar.ax.yaxis.set_label_coords(1.045, 0.1)\n", "# Set the tick levels for the colorbar\n", "cbar.set_ticks(cmap1.levels)\n", "cbar.set_ticklabels(cmap1.label)\n", "# ticks lines all the way across\n", - "cbar.ax.tick_params(which='both', width=1, length=26, labelsize=24,\n", - " direction='in')\n", - " \n", + "cbar.ax.tick_params(\n", + " which='both', width=1, length=26, labelsize=24, direction='in'\n", + ")\n", + "\n", "# stronger linewidth on frame\n", "ax1.spines['geo'].set_linewidth(2.0)\n", "ax1.spines['geo'].set_capstyle('projecting')\n", "# adjust subplot within figure\n", "fig.patch.set_facecolor('white')\n", - "fig.subplots_adjust(left=0.02,right=0.98,bottom=0.05,top=0.98)\n", - " \n", + "fig.subplots_adjust(left=0.02, right=0.98, bottom=0.05, top=0.98)\n", + "\n", + "\n", "# animate frames\n", "def animate_frames(i):\n", " # set image\n", - " im.set_data(grid.data[:,:,i])\n", + " im.set_data(grid.data[:, :, i])\n", " # add date label (year-calendar month e.g. 2002-01)\n", - " year,month = gravtk.time.grace_to_calendar(grid.month[i])\n", - " time_text.set_text(u'{0:4d}\\u2013{1:02d}'.format(year,month))\n", + " year, month = gravtk.time.grace_to_calendar(grid.month[i])\n", + " time_text.set_text('{0:4d}\\u2013{1:02d}'.format(year, month))\n", + "\n", "\n", "# set animation\n", "anim = animation.FuncAnimation(fig, animate_frames, frames=nt)\n", @@ -673,13 +750,13 @@ "# cyclical options\n", "cyclicLabel = ipywidgets.Label('Cyclical Terms:')\n", "cyclicCheckbox = {}\n", - "for key in ['Annual','Semi-Annual']:\n", + "for key in ['Annual', 'Semi-Annual']:\n", " cyclicCheckbox[key] = ipywidgets.Checkbox(\n", " value=True,\n", " description=key,\n", " disabled=False,\n", " )\n", - "cyclic = ipywidgets.HBox([cyclicLabel,*cyclicCheckbox.values()])\n", + "cyclic = ipywidgets.HBox([cyclicLabel, *cyclicCheckbox.values()])\n", "\n", "# custom fit terms\n", "termsLabel = ipywidgets.Label('Fit Terms:')\n", @@ -688,10 +765,10 @@ " description='S2 Tide',\n", " disabled=False,\n", ")\n", - "terms = ipywidgets.HBox([termsLabel,termsCheckbox])\n", + "terms = ipywidgets.HBox([termsLabel, termsCheckbox])\n", "\n", "# display widgets for setting GRACE/GRACE-FO regression parameters\n", - "ipywidgets.VBox([orderText,cyclic,terms])" + "ipywidgets.VBox([orderText, cyclic, terms])" ] }, { @@ -702,16 +779,21 @@ "source": [ "# build list of regression fit components\n", "ORDER = orderText.value\n", - "PHASES = {'Annual':1.0,'Semi-Annual':0.5}\n", - "CYCLES = [v for k,v in PHASES.items() if cyclicCheckbox[k].value]\n", + "PHASES = {'Annual': 1.0, 'Semi-Annual': 0.5}\n", + "CYCLES = [v for k, v in PHASES.items() if cyclicCheckbox[k].value]\n", "TERMS = []\n", "if termsCheckbox.value:\n", " TERMS.extend(gravtk.time_series.aliasing_terms(grid.time))\n", "# total number of fit terms\n", - "ncomp = (ORDER + 1) + 2*len(CYCLES) + len(TERMS)\n", + "ncomp = (ORDER + 1) + 2 * len(CYCLES) + len(TERMS)\n", "# Allocating memory for output variables\n", - "out = gravtk.spatial(spacing=grid.spacing, nlon=nlon, nlat=nlat,\n", - " extent=grid.extent, fill_value=grid.fill_value)\n", + "out = gravtk.spatial(\n", + " spacing=grid.spacing,\n", + " nlon=nlon,\n", + " nlat=nlat,\n", + " extent=grid.extent,\n", + " fill_value=grid.fill_value,\n", + ")\n", "out.data = np.zeros((nlat, nlon, ncomp))\n", "# update mask and dimensions\n", "out.update_mask()\n", @@ -720,11 +802,16 @@ "for i in range(nlat):\n", " for j in range(nlon):\n", " # Calculating the regression coefficients\n", - " tsbeta = gravtk.time_series.regress(grid.time, grid.data[i,j,:],\n", - " ORDER=ORDER, CYCLES=CYCLES, TERMS=TERMS)\n", + " tsbeta = gravtk.time_series.regress(\n", + " grid.time,\n", + " grid.data[i, j, :],\n", + " ORDER=ORDER,\n", + " CYCLES=CYCLES,\n", + " TERMS=TERMS,\n", + " )\n", " # save regression components\n", " for k in range(0, ncomp):\n", - " out.data[i,j,k] = tsbeta['beta'][k]" + " out.data[i, j, k] = tsbeta['beta'][k]" ] }, { @@ -743,27 +830,53 @@ "outputs": [], "source": [ "# strings for polynomial terms\n", - "if (ORDER == 0):# Mean\n", + "if ORDER == 0: # Mean\n", " variable_longname = ['Mean']\n", - "elif (ORDER == 1):# Trend\n", - " variable_longname = ['Constant','Trend']\n", - "elif (ORDER == 2):# Quadratic\n", - " variable_longname = ['Constant','Linear','Quadratic']\n", - "unit_suffix = [' yr$^{{{0:d}}}$'.format(-o) if o else '' for o in range(ORDER+1)]\n", + "elif ORDER == 1: # Trend\n", + " variable_longname = ['Constant', 'Trend']\n", + "elif ORDER == 2: # Quadratic\n", + " variable_longname = ['Constant', 'Linear', 'Quadratic']\n", + "unit_suffix = [\n", + " ' yr$^{{{0:d}}}$'.format(-o) if o else '' for o in range(ORDER + 1)\n", + "]\n", "# strings for cyclical terms\n", "cyclic_longname = {}\n", "cyclic_longname['Annual'] = ['Annual Sine', 'Annual Cosine']\n", "cyclic_longname['Semi-Annual'] = ['Semi-Annual Sine', 'Semi-Annual Cosine']\n", "# strings for custom fit terms\n", "terms_longname = {}\n", - "terms_longname['S2 Tide (GRACE)'] = ['S2 Tidal Alias Sine (GRACE)', 'S2 Tidal Alias Cosine (GRACE)']\n", - "terms_longname['S2 Tide (GRACE-FO)'] = ['S2 Tidal Alias Sine (GRACE-FO)', 'S2 Tidal Alias Cosine (GRACE-FO)']\n", + "terms_longname['S2 Tide (GRACE)'] = [\n", + " 'S2 Tidal Alias Sine (GRACE)',\n", + " 'S2 Tidal Alias Cosine (GRACE)',\n", + "]\n", + "terms_longname['S2 Tide (GRACE-FO)'] = [\n", + " 'S2 Tidal Alias Sine (GRACE-FO)',\n", + " 'S2 Tidal Alias Cosine (GRACE-FO)',\n", + "]\n", "\n", "# combined strings for all components\n", - "variable_longname.extend([i for k,v in cyclic_longname.items() for i in v if cyclicCheckbox[k].value])\n", - "unit_suffix.extend(['' for k,v in cyclic_longname.items() for i in v if cyclicCheckbox[k].value])\n", - "variable_longname.extend([i for k,v in terms_longname.items() for i in v if termsCheckbox.value])\n", - "unit_suffix.extend(['' for k,v in terms_longname.items() for i in v if termsCheckbox.value])\n", + "variable_longname.extend(\n", + " [\n", + " i\n", + " for k, v in cyclic_longname.items()\n", + " for i in v\n", + " if cyclicCheckbox[k].value\n", + " ]\n", + ")\n", + "unit_suffix.extend(\n", + " [\n", + " ''\n", + " for k, v in cyclic_longname.items()\n", + " for i in v\n", + " if cyclicCheckbox[k].value\n", + " ]\n", + ")\n", + "variable_longname.extend(\n", + " [i for k, v in terms_longname.items() for i in v if termsCheckbox.value]\n", + ")\n", + "unit_suffix.extend(\n", + " ['' for k, v in terms_longname.items() for i in v if termsCheckbox.value]\n", + ")\n", "\n", "# variable of interest\n", "variableDropdown = ipywidgets.Dropdown(\n", @@ -775,25 +888,29 @@ "\n", "# slider for the plot min and max for normalization\n", "i = variableDropdown.index\n", - "vmin = np.min(out.data[:,:,i]).astype(np.int64)\n", - "vmax = np.ceil(np.max(out.data[:,:,i])).astype(np.int64)\n", + "vmin = np.min(out.data[:, :, i]).astype(np.int64)\n", + "vmax = np.ceil(np.max(out.data[:, :, i])).astype(np.int64)\n", "cmap2 = gravtk.tools.colormap(vmin=vmin, vmax=vmax)\n", "\n", + "\n", "# set range and step size for variable\n", "def set_range_and_step(sender):\n", " i = variableDropdown.index\n", - " cmin = np.min(out.data[:,:,i]).astype(np.int64)\n", - " cmax = np.ceil(np.max(out.data[:,:,i])).astype(np.int64)\n", + " cmin = np.min(out.data[:, :, i]).astype(np.int64)\n", + " cmax = np.ceil(np.max(out.data[:, :, i])).astype(np.int64)\n", " cmap2.range.min = cmin\n", " cmap2.range.max = cmax\n", - " cmap2.range.value = [cmin,cmax]\n", + " cmap2.range.value = [cmin, cmax]\n", " cmap2.step.max = cmax - cmin\n", "\n", + "\n", "# watch variable widget for changes\n", "variableDropdown.observe(set_range_and_step)\n", "\n", "# display widgets for setting GRACE/GRACE-FO regression plot parameters\n", - "ipywidgets.VBox([variableDropdown,cmap2.range,cmap2.step,cmap2.name,cmap2.reverse])" + "ipywidgets.VBox(\n", + " [variableDropdown, cmap2.range, cmap2.step, cmap2.name, cmap2.reverse]\n", + ")" ] }, { @@ -810,14 +927,25 @@ "metadata": {}, "outputs": [], "source": [ - "fig, ax2 = plt.subplots(num=2, nrows=1, ncols=1, figsize=(10.375,6.625),\n", - " subplot_kw=dict(projection=ccrs.PlateCarree()))\n", + "fig, ax2 = plt.subplots(\n", + " num=2,\n", + " nrows=1,\n", + " ncols=1,\n", + " figsize=(10.375, 6.625),\n", + " subplot_kw=dict(projection=ccrs.PlateCarree()),\n", + ")\n", "\n", "# levels and normalization for plot range\n", "i = variableDropdown.index\n", - "im = ax2.imshow(out.data[:,:,i], interpolation='nearest',\n", - " norm=cmap2.norm, cmap=cmap2.value, transform=ccrs.PlateCarree(),\n", - " extent=grid.extent, origin='upper')\n", + "im = ax2.imshow(\n", + " out.data[:, :, i],\n", + " interpolation='nearest',\n", + " norm=cmap2.norm,\n", + " cmap=cmap2.value,\n", + " transform=ccrs.PlateCarree(),\n", + " extent=grid.extent,\n", + " origin='upper',\n", + ")\n", "ax2.coastlines('50m')\n", "\n", "# Add horizontal colorbar and adjust size\n", @@ -826,27 +954,36 @@ "# pad = distance from main plot axis\n", "# shrink = percent size of colorbar\n", "# aspect = lengthXwidth aspect of colorbar\n", - "cbar = plt.colorbar(im, ax=ax2, extend='both', extendfrac=0.0375,\n", - " orientation='horizontal', pad=0.025, shrink=0.85,\n", - " aspect=22, drawedges=False)\n", + "cbar = plt.colorbar(\n", + " im,\n", + " ax=ax2,\n", + " extend='both',\n", + " extendfrac=0.0375,\n", + " orientation='horizontal',\n", + " pad=0.025,\n", + " shrink=0.85,\n", + " aspect=22,\n", + " drawedges=False,\n", + ")\n", "# rasterized colorbar to remove lines\n", "cbar.solids.set_rasterized(True)\n", "# Add label to the colorbar\n", - "lbl = f'{unit_name[UNITS-1]} [{unit_label[UNITS-1]}{unit_suffix[i]}]'\n", + "lbl = f'{unit_name[UNITS - 1]} [{unit_label[UNITS - 1]}{unit_suffix[i]}]'\n", "cbar.ax.set_xlabel(lbl, labelpad=10, fontsize=24)\n", "# Set the tick levels for the colorbar\n", "cbar.set_ticks(cmap2.levels)\n", "cbar.set_ticklabels(cmap2.label)\n", "# ticks lines all the way across\n", - "cbar.ax.tick_params(which='both', width=1, length=26, labelsize=24,\n", - " direction='in')\n", - " \n", + "cbar.ax.tick_params(\n", + " which='both', width=1, length=26, labelsize=24, direction='in'\n", + ")\n", + "\n", "# stronger linewidth on frame\n", "ax2.spines['geo'].set_linewidth(2.0)\n", "ax2.spines['geo'].set_capstyle('projecting')\n", "# adjust subplot within figure\n", "fig.patch.set_facecolor('white')\n", - "fig.subplots_adjust(left=0.02,right=0.98,bottom=0.05,top=0.98)\n", + "fig.subplots_adjust(left=0.02, right=0.98, bottom=0.05, top=0.98)\n", "plt.show()" ] } diff --git a/doc/source/user_guide/NASA-Earthdata.ipynb b/doc/source/user_guide/NASA-Earthdata.ipynb index b7b15bff..43224b13 100644 --- a/doc/source/user_guide/NASA-Earthdata.ipynb +++ b/doc/source/user_guide/NASA-Earthdata.ipynb @@ -72,11 +72,13 @@ "source": [ "from IPython import get_ipython\n", "\n", + "\n", "def list_formatter(var, pp, *args, **kwargs):\n", - " pp.text(\"\\n\".join(var))\n", + " pp.text('\\n'.join(var))\n", + "\n", "\n", "plain = get_ipython().display_formatter.formatters['text/plain']\n", - "plain.for_type(list, list_formatter);\n" + "plain.for_type(list, list_formatter);" ] }, { @@ -106,9 +108,19 @@ ], "source": [ "import gravity_toolkit as gravtk\n", - "ids,urls,mtimes = gravtk.utilities.cmr(mission='grace',\n", - " center='JPL', release='RL06', version='0', level='L2', product='GSM',\n", - " solution='BA01', provider='POCLOUD', endpoint='data', verbose=True)\n", + "\n", + "ids, urls, mtimes = gravtk.utilities.cmr(\n", + " mission='grace',\n", + " center='JPL',\n", + " release='RL06',\n", + " version='0',\n", + " level='L2',\n", + " product='GSM',\n", + " solution='BA01',\n", + " provider='POCLOUD',\n", + " endpoint='data',\n", + " verbose=True,\n", + ")\n", "display(urls[:10])" ] }, diff --git a/geocenter/calc_degree_one.py b/geocenter/calc_degree_one.py index 879caeaf..271af509 100755 --- a/geocenter/calc_degree_one.py +++ b/geocenter/calc_degree_one.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" calc_degree_one.py Written by Tyler Sutterley (07/2026) @@ -260,6 +260,7 @@ Forked 06/2013 from calc_deg_one.pro Written 09/2012 """ + from __future__ import print_function import sys @@ -284,6 +285,7 @@ ticker = gravtk.utilities.import_dependency('matplotlib.ticker') netCDF4 = gravtk.utilities.import_dependency('netCDF4') + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -293,10 +295,25 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: import GRACE/GRACE-FO GSM files for a given months range -def load_grace_GSM(base_dir, PROC, DREL, START, END, MISSING, LMAX, - MMAX=None, SLR_C20=None, SLR_21=None, SLR_22=None, SLR_C30=None, - SLR_C40=None, SLR_C50=None, POLE_TIDE=False): +def load_grace_GSM( + base_dir, + PROC, + DREL, + START, + END, + MISSING, + LMAX, + MMAX=None, + SLR_C20=None, + SLR_21=None, + SLR_22=None, + SLR_C30=None, + SLR_C40=None, + SLR_C50=None, + POLE_TIDE=False, +): # GRACE/GRACE-FO dataset DSET = 'GSM' # do not import degree 1 coefficients for the GRACE GSM solution @@ -306,14 +323,31 @@ def load_grace_GSM(base_dir, PROC, DREL, START, END, MISSING, LMAX, # replacing low-degree harmonics with SLR values if specified # correcting for Pole-Tide if specified # atmospheric jumps will be corrected externally if specified - grace_Ylms = gravtk.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_C40=SLR_C40, SLR_C50=SLR_C50, POLE_TIDE=POLE_TIDE, - ATM=False, MODEL_DEG1=False) + grace_Ylms = gravtk.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_C40=SLR_C40, + SLR_C50=SLR_C50, + POLE_TIDE=POLE_TIDE, + ATM=False, + MODEL_DEG1=False, + ) # returning input variables as a harmonics object return gravtk.harmonics().from_dict(grace_Ylms) + # PURPOSE: import GRACE/GRACE-FO dealiasing files for a given months range def load_AOD(base_dir, PROC, DREL, DSET, START, END, MISSING, LMAX): # do not replace low degree harmonics for AOD solutions @@ -322,12 +356,24 @@ def load_AOD(base_dir, PROC, DREL, DSET, START, END, MISSING, LMAX): # 0: No degree 1 replacement DEG1 = 0 # reading GRACE/GRACE-FO AOD solutions for input date range - grace_Ylms = gravtk.grace_input_months(base_dir, - PROC, DREL, DSET, LMAX, START, END, MISSING, SLR_C20, DEG1, - POLE_TIDE=False, ATM=False) + grace_Ylms = gravtk.grace_input_months( + base_dir, + PROC, + DREL, + DSET, + LMAX, + START, + END, + MISSING, + SLR_C20, + DEG1, + POLE_TIDE=False, + ATM=False, + ) # returning input variables as a harmonics object return gravtk.harmonics().from_dict(grace_Ylms) + # PURPOSE: model the seasonal component of an initial degree 1 model # using preliminary estimates of annual and semi-annual variations from LWM # as calculated in Chen et al. (1999), doi:10.1029/1998JB900019 @@ -352,17 +398,27 @@ def model_seasonal_geocenter(grace_date): SAPz = 75.0 # calculate each geocenter component from the amplitude and phase # converting the phase from degrees to radians - X = AAx*np.sin(2.0*np.pi*grace_date + np.radians(APx)) + \ - SAAx*np.sin(4.0*np.pi*grace_date + np.radians(SAPx)) - Y = AAy*np.sin(2.0*np.pi*grace_date + np.radians(APy)) + \ - SAAy*np.sin(4.0*np.pi*grace_date + np.radians(SAPy)) - Z = AAz*np.sin(2.0*np.pi*grace_date + np.radians(APz)) + \ - SAAz*np.sin(4.0*np.pi*grace_date + np.radians(SAPz)) - DEG1 = gravtk.geocenter(X=X-X.mean(), Y=Y-Y.mean(), Z=Z-Z.mean()) + X = AAx * np.sin( + 2.0 * np.pi * grace_date + np.radians(APx) + ) + SAAx * np.sin(4.0 * np.pi * grace_date + np.radians(SAPx)) + Y = AAy * np.sin( + 2.0 * np.pi * grace_date + np.radians(APy) + ) + SAAy * np.sin(4.0 * np.pi * grace_date + np.radians(SAPy)) + Z = AAz * np.sin( + 2.0 * np.pi * grace_date + np.radians(APz) + ) + SAAz * np.sin(4.0 * np.pi * grace_date + np.radians(SAPz)) + DEG1 = gravtk.geocenter(X=X - X.mean(), Y=Y - Y.mean(), Z=Z - Z.mean()) return DEG1.from_cartesian() + # PURPOSE: calculate a geocenter time-series -def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, +def calc_degree_one( + base_dir, + PROC, + DREL, + MODEL, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -394,8 +450,8 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, LANDMASK=None, PLOT=False, COPY=False, - MODE=0o775): - + MODE=0o775, +): # output directory base_dir = pathlib.Path(base_dir).expanduser().absolute() DIRECTORY = base_dir.joinpath('geocenter') @@ -433,36 +489,36 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, attributes['eustatic_sea_level'] = 'uniform_redistribution' # output flag for low-degree harmonic replacements - if SLR_21 in ('CSR','GFZ','GSFC'): + if SLR_21 in ('CSR', 'GFZ', 'GSFC'): C21_str = f'_w{SLR_21}_21' else: C21_str = '' - if SLR_22 in ('CSR','GSFC'): + if SLR_22 in ('CSR', 'GSFC'): C22_str = f'_w{SLR_22}_22' else: C22_str = '' if SLR_C30 in ('GSFC',): # C30 replacement now default for all solutions C30_str = '' - elif SLR_C30 in ('CSR','GFZ','LARES'): + elif SLR_C30 in ('CSR', 'GFZ', 'LARES'): C30_str = f'_w{SLR_C30}_C30' else: C30_str = '' - if SLR_C40 in ('CSR','GSFC','LARES'): + if SLR_C40 in ('CSR', 'GSFC', 'LARES'): C40_str = f'_w{SLR_C40}_C40' else: C40_str = '' - if SLR_C50 in ('CSR','GSFC','LARES'): + if SLR_C50 in ('CSR', 'GSFC', 'LARES'): C50_str = f'_w{SLR_C50}_C50' else: C50_str = '' # combine satellite laser ranging flags - slr_str = ''.join([C21_str,C22_str,C30_str,C40_str,C50_str]) + slr_str = ''.join([C21_str, C22_str, C30_str, C40_str, C50_str]) # read load love numbers - LOVE = gravtk.load_love_numbers(EXPANSION, - LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF', - FORMAT='class') + LOVE = gravtk.load_love_numbers( + EXPANSION, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF', FORMAT='class' + ) # add attributes for earth model and love numbers attributes['earth_model'] = LOVE.model attributes['earth_love_numbers'] = LOVE.citation @@ -480,8 +536,8 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) - rho_e = factors.rho_e# Average Density of the Earth [g/cm^3] - rad_e = factors.rad_e# Average Radius of the Earth [cm] + rho_e = factors.rho_e # Average Density of the Earth [g/cm^3] + rad_e = factors.rad_e # Average Radius of the Earth [cm] l = factors.l # Factor for converting to Mass SH dfactor = factors.cmwe @@ -492,11 +548,12 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # Read Smoothed Ocean and Land Functions # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) # degree spacing and grid dimensions # will create GRACE spatial fields with same dimensions - dlon,dlat = landsea.spacing + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # spatial parameters in radians dphi = np.radians(dlon) @@ -508,8 +565,8 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, land_function = np.zeros((nlon, nlat), dtype=np.float64) # extract land function from file # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data.T >= 1) & (landsea.data.T <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data.T >= 1) & (landsea.data.T <= 3)) + land_function[indx, indy] = 1.0 # calculate ocean function from land function ocean_function = 1.0 - land_function @@ -520,22 +577,43 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # calculate spherical harmonics of ocean function to degree 1 # mass is equivalent to 1 cm ocean height change # eustatic ratio = -land total/ocean total - ocean_Ylms = gravtk.gen_stokes(ocean_function, - landsea.lon, landsea.lat, UNITS=1, LMIN=0, LMAX=1, - LOVE=LOVE, PLM=PLM[:2,:2,:]) + ocean_Ylms = gravtk.gen_stokes( + ocean_function, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + LOVE=LOVE, + PLM=PLM[:2, :2, :], + ) # Gaussian Smoothing (Jekeli, 1981) - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) attributes['smoothing_radius'] = f'{RAD:0.0f} km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) # load GRACE/GRACE-FO data - GSM_Ylms = load_grace_GSM(base_dir, PROC, DREL, START, END, MISSING, LMAX, - MMAX=MMAX, SLR_C20=SLR_C20, SLR_21=SLR_21, SLR_22=SLR_22, - SLR_C30=SLR_C30, SLR_C40=SLR_C40, SLR_C50=SLR_C50, POLE_TIDE=POLE_TIDE) + GSM_Ylms = load_grace_GSM( + base_dir, + PROC, + DREL, + START, + END, + MISSING, + LMAX, + MMAX=MMAX, + SLR_C20=SLR_C20, + SLR_21=SLR_21, + SLR_22=SLR_22, + SLR_C30=SLR_C30, + SLR_C40=SLR_C40, + SLR_C50=SLR_C50, + POLE_TIDE=POLE_TIDE, + ) GAD_Ylms = load_AOD(base_dir, PROC, DREL, 'GAD', START, END, MISSING, LMAX) GAC_Ylms = load_AOD(base_dir, PROC, DREL, 'GAC', START, END, MISSING, LMAX) # add attributes for input GRACE/GRACE-FO spherical harmonics @@ -544,8 +622,9 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # 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 = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GSM_Ylms.subtract(mean_Ylms) attributes['lineage'].append(MEAN_FILE.name) @@ -597,9 +676,15 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # NOTE: following Swenson (2008): do not use the kl Load Love number # to convert the GAD coefficients into coefficients of mass as # the GAC and GAD products are computed with a Load Love number of 0 - GAD.C10[:] = rho_e*rad_e*np.squeeze(GAD_Ylms.clm[1,0,:])*(2.0 + 1.0)/3.0 - GAD.C11[:] = rho_e*rad_e*np.squeeze(GAD_Ylms.clm[1,1,:])*(2.0 + 1.0)/3.0 - GAD.S11[:] = rho_e*rad_e*np.squeeze(GAD_Ylms.slm[1,1,:])*(2.0 + 1.0)/3.0 + GAD.C10[:] = ( + rho_e * rad_e * np.squeeze(GAD_Ylms.clm[1, 0, :]) * (2.0 + 1.0) / 3.0 + ) + GAD.C11[:] = ( + rho_e * rad_e * np.squeeze(GAD_Ylms.clm[1, 1, :]) * (2.0 + 1.0) / 3.0 + ) + GAD.S11[:] = ( + rho_e * rad_e * np.squeeze(GAD_Ylms.slm[1, 1, :]) * (2.0 + 1.0) / 3.0 + ) # removing the mean of the GAD OBP coefficients GAD.mean(apply=True) @@ -608,10 +693,9 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, ATM_Ylms.time[:] = np.copy(GSM_Ylms.time) ATM_Ylms.month[:] = np.copy(GSM_Ylms.month) if ATM: - atm_corr = gravtk.read_ecmwf_corrections(base_dir, - LMAX, ATM_Ylms.month) - ATM_Ylms.clm[:,:,:] = np.copy(atm_corr['clm']) - ATM_Ylms.slm[:,:,:] = np.copy(atm_corr['slm']) + atm_corr = gravtk.read_ecmwf_corrections(base_dir, LMAX, ATM_Ylms.month) + ATM_Ylms.clm[:, :, :] = np.copy(atm_corr['clm']) + ATM_Ylms.slm[:, :, :] = np.copy(atm_corr['slm']) # removing the mean of the atmospheric jump correction coefficients ATM_Ylms.mean(apply=True) # truncate to degree and order LMAX/MMAX @@ -620,14 +704,15 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, atm = gravtk.geocenter().from_harmonics(ATM_Ylms) # read bottom pressure model if applicable - if MODEL not in ('OMCT','MPIOM'): + if MODEL not in ('OMCT', 'MPIOM'): # read input data files for ascii (txt), netCDF4 (nc) or HDF5 (H5) MODEL_INDEX = pathlib.Path(MODEL_INDEX).expanduser().absolute() - OBP_Ylms = gravtk.harmonics().from_index(MODEL_INDEX, - format=DATAFORM) + OBP_Ylms = gravtk.harmonics().from_index(MODEL_INDEX, format=DATAFORM) attributes['lineage'].extend([f.name for f in OBP_Ylms.filename]) # reduce to GRACE/GRACE-FO months and truncate to degree and order - OBP_Ylms = OBP_Ylms.subset(GSM_Ylms.month).truncate(lmax=LMAX,mmax=MMAX) + OBP_Ylms = OBP_Ylms.subset(GSM_Ylms.month).truncate( + lmax=LMAX, mmax=MMAX + ) # filter ocean bottom pressure coefficients if DESTRIPE: OBP_Ylms = OBP_Ylms.destripe() @@ -644,22 +729,24 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, 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) + 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'): + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - Ylms = gravtk.harmonics().from_file(REMOVE_FILE, - format=REMOVEFORM) + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) attributes['lineage'].append(Ylms.filename) - elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'): + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,removeform = REMOVEFORM.split('-') + _, removeform = REMOVEFORM.split('-') # index containing files in data format - Ylms = gravtk.harmonics().from_index(REMOVE_FILE, - format=removeform) + Ylms = gravtk.harmonics().from_index( + REMOVE_FILE, format=removeform + ) attributes['lineage'].extend([f.name for f in Ylms.filename]) # reduce to GRACE/GRACE-FO months and truncate to degree and order Ylms = Ylms.subset(GSM_Ylms.month).truncate(lmax=LMAX, mmax=MMAX) @@ -669,14 +756,14 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, 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] + 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 + 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] + 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() @@ -691,21 +778,23 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # output [m,phi] m = GSM_Ylms.m # Integration factors (solid angle) - int_fact = np.sin(th)*dphi*dth + int_fact = np.sin(th) * dphi * dth # 4-pi normalization - norm = 1.0/(4.0*np.pi) + norm = 1.0 / (4.0 * np.pi) # calculating cos(m*phi) and sin(m*phi) using Euler's formula - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", m, phi)) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', m, phi)) # Legendre polynomials for degree 1 - P10 = np.squeeze(PLM[1,0,:]) - P11 = np.squeeze(PLM[1,1,:]) + P10 = np.squeeze(PLM[1, 0, :]) + P11 = np.squeeze(PLM[1, 1, :]) # PLM for spherical harmonic degrees 2+ up to LMAX # converted into mass and smoothed if specified - plmout = np.zeros((LMAX+1, MMAX+1, nlat)) + plmout = np.zeros((LMAX + 1, MMAX + 1, nlat)) # convert to smoothed coefficients of mass # Convolving plms with degree dependent factor and smoothing - plmout[:] = np.einsum("l,l,lmh->lmh", dfactor, wt, PLM[:LMAX+1,:MMAX+1,:]) + plmout[:] = np.einsum( + 'l,l,lmh->lmh', dfactor, wt, PLM[: LMAX + 1, : MMAX + 1, :] + ) # Initializing 3x3 I-Parameter matrix # (see equations 12 and 13 of Swenson et al., 2008) @@ -713,21 +802,39 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # I-Parameter matrix accounts for the fact that the GRACE data only # includes spherical harmonic degrees greater than or equal to 2 # C10, C11, S11 - PC10 = np.einsum("h...,p...->ph...", P10, m_phi[0,:].real) - PC11 = np.einsum("h...,p...->ph...", P11, m_phi[1,:].real) - PS11 = np.einsum("h...,p...->ph...", P11, m_phi[1,:].imag) + PC10 = np.einsum('h...,p...->ph...', P10, m_phi[0, :].real) + PC11 = np.einsum('h...,p...->ph...', P11, m_phi[1, :].real) + PS11 = np.einsum('h...,p...->ph...', P11, m_phi[1, :].imag) # C10: C10, C11, S11 - IMAT[0,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PC10) - IMAT[1,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PC11) - IMAT[2,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PS11) + IMAT[0, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PC10 + ) + IMAT[1, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PC11 + ) + IMAT[2, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PS11 + ) # C11: C10, C11, S11 - IMAT[0,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PC10) - IMAT[1,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PC11) - IMAT[2,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PS11) + IMAT[0, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PC10 + ) + IMAT[1, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PC11 + ) + IMAT[2, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PS11 + ) # S11: C10, C11, S11 - IMAT[0,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PC10) - IMAT[1,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PC11) - IMAT[2,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PS11) + IMAT[0, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PC10 + ) + IMAT[1, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PC11 + ) + IMAT[2, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PS11 + ) # get seasonal variations of an initial geocenter correction # for use in the land water mass calculation @@ -751,7 +858,7 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, G.C11 = np.zeros((n_files)) G.S11 = np.zeros((n_files)) # DMAT is the degree one matrix ((C10,C11,S11) x Time) in terms of mass - DMAT = np.zeros((3,n_files)) + DMAT = np.zeros((3, n_files)) # degree 1 iterations iteration = gravtk.geocenter() iteration.C10 = np.zeros((n_files, max_iter)) @@ -768,36 +875,44 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, Ylms.subtract(ATM_Ylms.index(t)) Ylms.subtract(remove_Ylms.index(t)) # subset GRACE to degrees 2+ for calculating ocean mass - l2 = slice(2, LMAX+1) - pconv = np.einsum("lmh...,lm...->mh...", plmout[l2, :, :], Ylms.ilm[l2, :]) + l2 = slice(2, LMAX + 1) + pconv = np.einsum( + 'lmh...,lm...->mh...', plmout[l2, :, :], Ylms.ilm[l2, :] + ) # Multiplying by c/s(phi#m) to get surface density in cmwe (lon,lat) # ccos/ssin are mXphi, pcos/psin are mXtheta: resultant matrices are phiXtheta # The summation over spherical harmonic order is in this multiplication - rmass = np.einsum("mp...,mh...->ph...", m_phi, pconv).real + rmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # calculate G matrix parameters through a summation of each latitude # summation of integration factors, Legendre polynomials, # (convolution of order and harmonics) and the ocean mass at t - G.C10[t] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, rmass) - G.C11[t] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, rmass) - G.S11[t] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, rmass) + G.C10[t] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, rmass + ) + G.C11[t] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, rmass + ) + G.S11[t] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, rmass + ) # calculate degree one solution for each iteration (or single if not) while (eps > eps_max) and (n_iter < max_iter): # for each file for t in range(n_files): # calculate eustatic component from GRACE (can iterate) - if (n_iter == 0): + if n_iter == 0: # for first iteration (will be only iteration if not ITERATIVE): # seasonal component of geocenter variation for land water - GSM_Ylms.clm[1,0,t] = seasonal_geocenter.C10[t] - GSM_Ylms.clm[1,1,t] = seasonal_geocenter.C11[t] - GSM_Ylms.slm[1,1,t] = seasonal_geocenter.S11[t] + GSM_Ylms.clm[1, 0, t] = seasonal_geocenter.C10[t] + GSM_Ylms.clm[1, 1, t] = seasonal_geocenter.C11[t] + GSM_Ylms.slm[1, 1, t] = seasonal_geocenter.S11[t] else: # for all others: use previous iteration of inversion # for each of the geocenter solutions (C10, C11, S11) - GSM_Ylms.clm[1,0,t] = iteration.C10[t,n_iter-1] - GSM_Ylms.clm[1,1,t] = iteration.C11[t,n_iter-1] - GSM_Ylms.slm[1,1,t] = iteration.S11[t,n_iter-1] + GSM_Ylms.clm[1, 0, t] = iteration.C10[t, n_iter - 1] + GSM_Ylms.clm[1, 1, t] = iteration.C11[t, n_iter - 1] + GSM_Ylms.slm[1, 1, t] = iteration.S11[t, n_iter - 1] # Summing product of plms and c/slms over all SH degrees # Removing monthly GIA signal, atmospheric correction @@ -808,13 +923,15 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, Ylms.subtract(remove_Ylms.index(t)) # for land water: use an initial seasonal geocenter estimate # from Chen et al. (1999) then the iterative if specified - l1 = slice(1, LMAX+1) - pconv = np.einsum("lmh...,lm...->mh...", plmout[l1, :, :], Ylms.ilm[l1, :]) + l1 = slice(1, LMAX + 1) + pconv = np.einsum( + 'lmh...,lm...->mh...', plmout[l1, :, :], Ylms.ilm[l1, :] + ) # Multiplying by c/s(phi#m) to get surface density in cm w.e. (lonxlat) # ccos/ssin are mXphi, pcos/psin are mXtheta: resultant matrices are phiXtheta # The summation over spherical harmonic order is in this multiplication - lmass = np.einsum("mp...,mh...->ph...", m_phi, pconv).real + lmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # use sea level fingerprints or eustatic from GRACE land components if FINGERPRINT: @@ -824,49 +941,82 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # NOTE: this is an unscaled GRACE estimate that uses the # buffered land function when solving the sea-level equation. # possible improvement using scaled estimate with real coastlines - land_Ylms = gravtk.gen_stokes(lmass*land_function, - landsea.lon, landsea.lat, UNITS=1, LMIN=0, - LMAX=EXPANSION, PLM=PLM, LOVE=LOVE) + land_Ylms = gravtk.gen_stokes( + lmass * land_function, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=EXPANSION, + PLM=PLM, + LOVE=LOVE, + ) # 2) calculate sea level fingerprints of land mass at time t # use maximum of 3 iterations for computational efficiency - sea_level = gravtk.sea_level_equation(land_Ylms.clm, land_Ylms.slm, - landsea.lon, landsea.lat, land_function, LMAX=EXPANSION, - LOVE=LOVE, BODY_TIDE_LOVE=0, FLUID_LOVE=0, ITERATIONS=3, - POLAR=True, PLM=PLM, FILL_VALUE=0) + sea_level = gravtk.sea_level_equation( + land_Ylms.clm, + land_Ylms.slm, + landsea.lon, + landsea.lat, + land_function, + LMAX=EXPANSION, + LOVE=LOVE, + BODY_TIDE_LOVE=0, + FLUID_LOVE=0, + ITERATIONS=3, + POLAR=True, + PLM=PLM, + FILL_VALUE=0, + ) # 3) convert sea level fingerprints into spherical harmonics - slf_Ylms = gravtk.gen_stokes(sea_level, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=1, PLM=PLM[:2,:2,:], LOVE=LOVE) + slf_Ylms = gravtk.gen_stokes( + sea_level, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + PLM=PLM[:2, :2, :], + LOVE=LOVE, + ) # 4) convert the slf degree 1 harmonics to mass with dfactor - eustatic.C10[t] = slf_Ylms.clm[1,0]*dfactor[1] - eustatic.C11[t] = slf_Ylms.clm[1,1]*dfactor[1] - eustatic.S11[t] = slf_Ylms.slm[1,1]*dfactor[1] + eustatic.C10[t] = slf_Ylms.clm[1, 0] * dfactor[1] + eustatic.C11[t] = slf_Ylms.clm[1, 1] * dfactor[1] + eustatic.S11[t] = slf_Ylms.slm[1, 1] * dfactor[1] else: # steps to calculate eustatic component from GRACE land-water change: # 1) calculate total mass of 1 cm of ocean height (calculated above) # 2) calculate total land mass at time t (GRACE*land function) # NOTE: possible improvement using the sea-level equation to solve # for the spatial pattern of sea level from the land water mass - land_Ylms = gravtk.gen_stokes(lmass*land_function, - landsea.lon, landsea.lat, UNITS=1, LMIN=0, LMAX=1, - PLM=PLM[:2,:2,:], LOVE=LOVE) + land_Ylms = gravtk.gen_stokes( + lmass * land_function, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + PLM=PLM[:2, :2, :], + LOVE=LOVE, + ) # 3) calculate ratio between the total land mass and the total mass # of 1 cm of ocean height (negative as positive land = sea level drop) # this converts the total land change to ocean height change - eustatic_ratio = -land_Ylms.clm[0,0]/ocean_Ylms.clm[0,0] + eustatic_ratio = -land_Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0] # 4) scale degree one coefficients of ocean function with ratio # and convert the eustatic degree 1 harmonics to mass with dfactor - scale_factor = eustatic_ratio*dfactor[1] - eustatic.C10[t] = ocean_Ylms.clm[1,0]*scale_factor - eustatic.C11[t] = ocean_Ylms.clm[1,1]*scale_factor - eustatic.S11[t] = ocean_Ylms.slm[1,1]*scale_factor + scale_factor = eustatic_ratio * dfactor[1] + eustatic.C10[t] = ocean_Ylms.clm[1, 0] * scale_factor + eustatic.C11[t] = ocean_Ylms.clm[1, 1] * scale_factor + eustatic.S11[t] = ocean_Ylms.slm[1, 1] * scale_factor # eustatic coefficients of degree 1 # for OMCT/MPIOM: # equal to the eustatic component only as OMCT/MPIOM model is # already removed from the GRACE/GRACE-FO GSM coefficients - CMAT = np.array([eustatic.C10[t],eustatic.C11[t],eustatic.S11[t]]) + CMAT = np.array([eustatic.C10[t], eustatic.C11[t], eustatic.S11[t]]) # replacing the OBP harmonics of degree 1 - if MODEL not in ('OMCT','MPIOM'): + if MODEL not in ('OMCT', 'MPIOM'): # calculate difference between ECCO and GAD as the OMCT/MPIOM # model is already removed from the GRACE GSM coefficients GADMAT = np.array([GAD.C10[t], GAD.C11[t], GAD.S11[t]]) @@ -882,32 +1032,57 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # the G Matrix until (C10, C11, S11) converge # for OMCT/MPIOM: min(eustatic from land - measured ocean) # for ECCO: min((OBP-GAD) + eustatic from land - measured ocean) - if (SOLVER == 'inv'): - DMAT[:,t] = np.dot(np.linalg.inv(IMAT), (CMAT-GMAT)) - elif (SOLVER == 'lstsq'): - DMAT[:,t] = np.linalg.lstsq(IMAT, (CMAT-GMAT), rcond=-1)[0] + if SOLVER == 'inv': + DMAT[:, t] = np.dot(np.linalg.inv(IMAT), (CMAT - GMAT)) + elif SOLVER == 'lstsq': + DMAT[:, t] = np.linalg.lstsq(IMAT, (CMAT - GMAT), rcond=-1)[0] elif SOLVER in ('gelsd', 'gelsy', 'gelss'): - DMAT[:,t], res, rnk, s = scipy.linalg.lstsq(IMAT, (CMAT-GMAT), - lapack_driver=SOLVER) + DMAT[:, t], res, rnk, s = scipy.linalg.lstsq( + IMAT, (CMAT - GMAT), lapack_driver=SOLVER + ) # save geocenter for iteration and time t after restoring fields - iteration.C10[t,n_iter] = DMAT[0,t]/dfactor[1] + \ - gia.C10[t] + atm.C10[t] + remove.C10[t] - iteration.C11[t,n_iter] = DMAT[1,t]/dfactor[1] + \ - gia.C11[t] + atm.C11[t] + remove.C11[t] - iteration.S11[t,n_iter] = DMAT[2,t]/dfactor[1] + \ - gia.S11[t] + atm.S11[t] + remove.S11[t] + iteration.C10[t, n_iter] = ( + DMAT[0, t] / dfactor[1] + + gia.C10[t] + + atm.C10[t] + + remove.C10[t] + ) + iteration.C11[t, n_iter] = ( + DMAT[1, t] / dfactor[1] + + gia.C11[t] + + atm.C11[t] + + remove.C11[t] + ) + iteration.S11[t, n_iter] = ( + DMAT[2, t] / dfactor[1] + + gia.S11[t] + + atm.S11[t] + + remove.S11[t] + ) # remove mean of each solution for iteration - iteration.C10[:,n_iter] -= iteration.C10[:,n_iter].mean() - iteration.C11[:,n_iter] -= iteration.C11[:,n_iter].mean() - iteration.S11[:,n_iter] -= iteration.S11[:,n_iter].mean() + iteration.C10[:, n_iter] -= iteration.C10[:, n_iter].mean() + iteration.C11[:, n_iter] -= iteration.C11[:, n_iter].mean() + iteration.S11[:, n_iter] -= iteration.S11[:, n_iter].mean() # calculate difference between original geocenter coefficients and the # calculated coefficients for each of the geocenter solutions - sigma_C10 = np.sum((GSM_Ylms.clm[1,0,:] - iteration.C10[:,n_iter])**2) - sigma_C11 = np.sum((GSM_Ylms.clm[1,1,:] - iteration.C11[:,n_iter])**2) - sigma_S11 = np.sum((GSM_Ylms.slm[1,1,:] - iteration.S11[:,n_iter])**2) - power = GSM_Ylms.clm[1,0,:]**2 + GSM_Ylms.clm[1,1,:]**2 + GSM_Ylms.slm[1,1,:]**2 - eps = np.sqrt(sigma_C10 + sigma_C11 + sigma_S11)/np.sqrt(np.sum(power)) + sigma_C10 = np.sum( + (GSM_Ylms.clm[1, 0, :] - iteration.C10[:, n_iter]) ** 2 + ) + sigma_C11 = np.sum( + (GSM_Ylms.clm[1, 1, :] - iteration.C11[:, n_iter]) ** 2 + ) + sigma_S11 = np.sum( + (GSM_Ylms.slm[1, 1, :] - iteration.S11[:, n_iter]) ** 2 + ) + power = ( + GSM_Ylms.clm[1, 0, :] ** 2 + + GSM_Ylms.clm[1, 1, :] ** 2 + + GSM_Ylms.slm[1, 1, :] ** 2 + ) + eps = np.sqrt(sigma_C10 + sigma_C11 + sigma_S11) / np.sqrt( + np.sum(power) + ) # add 1 to n_iter counter n_iter += 1 @@ -918,9 +1093,9 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # for each of the geocenter solutions (C10, C11, S11) # for the iterative case this will be the final iteration DEG1 = gravtk.geocenter() - DEG1.C10 = DMAT[0,:]/dfactor[1] + gia.C10[:] + atm.C10[:] + remove.C10[:] - DEG1.C11 = DMAT[1,:]/dfactor[1] + gia.C11[:] + atm.C11[:] + remove.C11[:] - DEG1.S11 = DMAT[2,:]/dfactor[1] + gia.S11[:] + atm.S11[:] + remove.S11[:] + DEG1.C10 = DMAT[0, :] / dfactor[1] + gia.C10[:] + atm.C10[:] + remove.C10[:] + DEG1.C11 = DMAT[1, :] / dfactor[1] + gia.C11[:] + atm.C11[:] + remove.C11[:] + DEG1.S11 = DMAT[2, :] / dfactor[1] + gia.S11[:] + atm.S11[:] + remove.S11[:] # remove mean of geocenter for each component DEG1.mean(apply=True) # calculate geocenter variations with dealiasing restored @@ -932,24 +1107,44 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, output_format = '{0:11.4f}{1:14.6e}{2:14.6e}{3:14.6e} {4:03d}\n' # public file format in fully normalized spherical harmonics # before and after restoring the atmospheric and oceanic dealiasing - for AOD in ['','_wAOD']: + for AOD in ['', '_wAOD']: # local version with all descriptor flags - a1=(PROC,DREL,MODEL,slf_str,iter_str,slr_str,gia_str,AOD,ds_str,'txt') + a1 = ( + PROC, + DREL, + MODEL, + slf_str, + iter_str, + slr_str, + gia_str, + AOD, + ds_str, + 'txt', + ) FILE1 = DIRECTORY.joinpath(file_format.format(*a1)) fid1 = FILE1.open(mode='w', encoding='utf8') # print headers for cases with and without dealiasing print_header(fid1) - print_harmonic(fid1,LOVE.kl[1]) - print_global(fid1,PROC,DREL,MODEL.replace('_',' '),AOD,GIA_Ylms_rate, - SLR_C20,SLR_21,GSM_Ylms.month) - print_variables(fid1,'single precision','fully normalized') + print_harmonic(fid1, LOVE.kl[1]) + print_global( + fid1, + PROC, + DREL, + MODEL.replace('_', ' '), + AOD, + GIA_Ylms_rate, + SLR_C20, + SLR_21, + GSM_Ylms.month, + ) + print_variables(fid1, 'single precision', 'fully normalized') # for each GRACE/GRACE-FO month - for t,mon in enumerate(GSM_Ylms.month): + for t, mon in enumerate(GSM_Ylms.month): # geocenter coefficients with and without AOD restored if AOD: - args=(tdec[t],aod.C10[t],aod.C11[t],aod.S11[t],mon) + args = (tdec[t], aod.C10[t], aod.C11[t], aod.S11[t], mon) else: - args=(tdec[t],DEG1.C10[t],DEG1.C11[t],DEG1.S11[t],mon) + args = (tdec[t], DEG1.C10[t], DEG1.C11[t], DEG1.S11[t], mon) # output geocenter coefficients to file fid1.write(output_format.format(*args)) # close the output file @@ -960,21 +1155,43 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # create public and archival copies of data if COPY: # create symbolic link for public distribution without flags - a2=(PROC,DREL,MODEL,slf_str,iter_str,'','',AOD,'','txt') + a2 = (PROC, DREL, MODEL, slf_str, iter_str, '', '', AOD, '', 'txt') FILE2 = DIRECTORY.joinpath(file_format.format(*a2)) - os.symlink(FILE1,FILE2) if not FILE2.exists() else None + os.symlink(FILE1, FILE2) if not FILE2.exists() else None output_files.append(FILE2) # create copy of file with date for archiving - today = time.strftime('_%Y-%m-%d',time.localtime()) - a3=(PROC,DREL,MODEL,slf_str,iter_str,'','',AOD,today,'txt') + today = time.strftime('_%Y-%m-%d', time.localtime()) + a3 = ( + PROC, + DREL, + MODEL, + slf_str, + iter_str, + '', + '', + AOD, + today, + 'txt', + ) FILE3 = DIRECTORY.joinpath(file_format.format(*a3)) - shutil.copyfile(FILE1,FILE3) + shutil.copyfile(FILE1, FILE3) # copy modification times and permissions for archive file - shutil.copystat(FILE1,FILE3) + shutil.copystat(FILE1, FILE3) output_files.append(FILE3) # output all degree 1 coefficients as a netCDF4 file - a4=(PROC,DREL,MODEL,slf_str,iter_str,slr_str,gia_str,'',ds_str,'nc') + a4 = ( + PROC, + DREL, + MODEL, + slf_str, + iter_str, + slr_str, + gia_str, + '', + ds_str, + 'nc', + ) FILE4 = DIRECTORY.joinpath(file_format.format(*a4)) fileID = netCDF4.Dataset(FILE4, mode='w') # Defining the NetCDF4 dimensions @@ -1006,16 +1223,23 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, nc['time'][:] = tdec[:].copy() nc['month'][:] = months[:].copy() # set attributes for time and month - for key in ('time','month'): + for key in ('time', 'month'): for att_name, att_val in attrs[key].items(): nc[key].setncattr(att_name, att_val) # degree 1 coefficients from the iterative solution for key in iteration.fields: var = iteration.get(key) - nc[key] = fileID.createVariable(key, var.dtype, - ('time','iteration',), zlib=True) - nc[key][:] = var[:,:n_iter] + nc[key] = fileID.createVariable( + key, + var.dtype, + ( + 'time', + 'iteration', + ), + zlib=True, + ) + nc[key][:] = var[:, :n_iter] for att_name, att_val in attrs[key].items(): nc[key].setncattr(att_name, att_val) @@ -1023,11 +1247,10 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, nc['AOD'] = {} g1 = fileID.createGroup('AOD') g1.description = f'Atmospheric and oceanic dealiasing' - gac = GAC.scale(1.0/dfactor[1]) + gac = GAC.scale(1.0 / dfactor[1]) for key in gac.fields: var = gac.get(key) - nc['AOD'][key] = g1.createVariable(key, var.dtype, - ('time',), zlib=True) + nc['AOD'][key] = g1.createVariable(key, var.dtype, ('time',), zlib=True) nc['AOD'][key][:] = var[:] for att_name, att_val in attrs[key].items(): nc['AOD'][key].setncattr(att_name, att_val) @@ -1036,14 +1259,13 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, nc['OBP'] = {} g2 = fileID.createGroup('OBP') g2.description = f'Ocean bottom pressure from {MODEL}' - if MODEL not in ('OMCT','MPIOM'): - obp = OBP.scale(1.0/dfactor[1]) + if MODEL not in ('OMCT', 'MPIOM'): + obp = OBP.scale(1.0 / dfactor[1]) else: - obp = GAD.scale(1.0/dfactor[1]) + obp = GAD.scale(1.0 / dfactor[1]) for key in obp.fields: var = obp.get(key) - nc['OBP'][key] = g2.createVariable(key, var.dtype, - ('time',), zlib=True) + nc['OBP'][key] = g2.createVariable(key, var.dtype, ('time',), zlib=True) nc['OBP'][key][:] = var[:] for att_name, att_val in attrs[key].items(): nc['OBP'][key].setncattr(att_name, att_val) @@ -1052,11 +1274,10 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, nc['OWM'] = {} g3 = fileID.createGroup('OWM') g3.description = f'Ocean water mass from {MISSION}' - owm = G.scale(1.0/dfactor[1]) + owm = G.scale(1.0 / dfactor[1]) for key in owm.fields: var = owm.get(key) - nc['OWM'][key] = g3.createVariable(key, var.dtype, - ('time',), zlib=True) + nc['OWM'][key] = g3.createVariable(key, var.dtype, ('time',), zlib=True) nc['OWM'][key][:] = var[:] for att_name, att_val in attrs[key].items(): nc['OWM'][key].setncattr(att_name, att_val) @@ -1065,11 +1286,10 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, nc['ESL'] = {} g4 = fileID.createGroup('ESL') g4.description = f'Eustatic sea level from {MISSION} land water mass' - esl = eustatic.scale(1.0/dfactor[1]) + esl = eustatic.scale(1.0 / dfactor[1]) for key in esl.fields: var = esl.get(key) - nc['ESL'][key] = g4.createVariable(key, var.dtype, - ('time',), zlib=True) + nc['ESL'][key] = g4.createVariable(key, var.dtype, ('time',), zlib=True) nc['ESL'][key][:] = var[:] for att_name, att_val in attrs[key].items(): nc['ESL'][key].setncattr(att_name, att_val) @@ -1078,7 +1298,7 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, for att_name, att_val in attributes.items(): fileID.setncattr(att_name, att_val) # define creation date attribute - fileID.date_created = time.strftime('%Y-%m-%d',time.localtime()) + fileID.date_created = time.strftime('%Y-%m-%d', time.localtime()) # close the output file fileID.close() # set the permissions mode of the output file @@ -1093,34 +1313,41 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # - eustatic sea level geocenter # - G-matrix ocean water mass components ax = {} - fig, (ax[0], ax[1], ax[2]) = plt.subplots(num=1, nrows=3, - sharex=True, sharey=True, figsize=(6,9)) - ii = np.nonzero((tdec >= 2003.) & (tdec < 2008.)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, nrows=3, sharex=True, sharey=True, figsize=(6, 9) + ) + ii = np.nonzero((tdec >= 2003.0) & (tdec < 2008.0)) # remove means of individual geocenter components - if MODEL not in ('OMCT','MPIOM'): - OBP.mean(apply=True,indices=ii) - G.mean(apply=True,indices=ii) - GAD.mean(apply=True,indices=ii) - eustatic.mean(apply=True,indices=ii) - for i,key in enumerate(G.fields): + if MODEL not in ('OMCT', 'MPIOM'): + OBP.mean(apply=True, indices=ii) + G.mean(apply=True, indices=ii) + GAD.mean(apply=True, indices=ii) + eustatic.mean(apply=True, indices=ii) + for i, key in enumerate(G.fields): # plot ocean bottom pressure for alternative models - if MODEL not in ('OMCT','MPIOM'): - ax[i].plot(tdec, 10.*OBP.get(key), color='#1ed565', lw=2) + if MODEL not in ('OMCT', 'MPIOM'): + ax[i].plot(tdec, 10.0 * OBP.get(key), color='#1ed565', lw=2) # plot GRACE components - ax[i].plot(tdec, 10.*G.get(key), color='orange', lw=2) + ax[i].plot(tdec, 10.0 * G.get(key), color='orange', lw=2) # plot OMCT/MPIOM ocean bottom pressure - ax[i].plot(tdec, 10.*GAD.get(key), color='blue', lw=2) + ax[i].plot(tdec, 10.0 * GAD.get(key), color='blue', lw=2) # plot eustatic components - ax[i].plot(tdec, 10.*eustatic.get(key), color='r', lw=2) + ax[i].plot(tdec, 10.0 * eustatic.get(key), color='r', lw=2) ax[i].set_ylabel('[mm]', fontsize=14) # add axis labels and adjust font sizes for axis ticks # axis label - artist = offsetbox.AnchoredText(key, pad=0., - prop=dict(size=16,weight='bold'), frameon=False, loc=2) + artist = offsetbox.AnchoredText( + key, + pad=0.0, + prop=dict(size=16, weight='bold'), + frameon=False, + loc=2, + ) ax[i].add_artist(artist) # axes tick adjustments - ax[i].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[i].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # labels and set limits to Swenson range ax[2].set_xlabel('Time [Yr]', fontsize=14) ax[2].set_xlim(2003, 2007) @@ -1130,11 +1357,13 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, ax[2].yaxis.set_ticks(np.arange(-6, 8, 2)) ax[2].xaxis.get_major_formatter().set_useOffset(False) # adjust locations of subplots and save to file - fig.subplots_adjust(left=0.1,right=0.96,bottom=0.06,top=0.98,hspace=0.1) - args = (PROC,DREL,MODEL,slf_str,iter_str,slr_str,gia_str,ds_str) + fig.subplots_adjust( + left=0.1, right=0.96, bottom=0.06, top=0.98, hspace=0.1 + ) + args = (PROC, DREL, MODEL, slf_str, iter_str, slr_str, gia_str, ds_str) FILE = 'Swenson_Figure_1_{0}_{1}_{2}{3}{4}{5}{6}{7}.pdf'.format(*args) PLOT1 = DIRECTORY.joinpath(FILE) - metadata = {'Title':pathlib.Path(sys.argv[0]).name} + metadata = {'Title': pathlib.Path(sys.argv[0]).name} plt.savefig(PLOT1, format='pdf', metadata=metadata) plt.clf() # set the permissions mode of the output files @@ -1145,41 +1374,50 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, if PLOT and ITERATIVE: # 3 row plot (C10, C11 and S11) ax = {} - fig, (ax[0], ax[1], ax[2]) = plt.subplots(num=2, nrows=3, - sharex=True, figsize=(6,9)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=2, nrows=3, sharex=True, figsize=(6, 9) + ) # show solutions for each iteration cmap = copy.copy(cm.rainbow) - plot_colors = iter(cmap(np.linspace(0,1,n_iter))) - iteration_mmwe = iteration.scale(10.0*dfactor[1]) + plot_colors = iter(cmap(np.linspace(0, 1, n_iter))) + iteration_mmwe = iteration.scale(10.0 * dfactor[1]) for j in range(n_iter): c = next(plot_colors) # C10, C11 and S11 - ax[0].plot(GSM_Ylms.month,iteration_mmwe.C10[:,j],c=c) - ax[1].plot(GSM_Ylms.month,iteration_mmwe.C11[:,j],c=c) - ax[2].plot(GSM_Ylms.month,iteration_mmwe.S11[:,j],c=c) + ax[0].plot(GSM_Ylms.month, iteration_mmwe.C10[:, j], c=c) + ax[1].plot(GSM_Ylms.month, iteration_mmwe.C11[:, j], c=c) + ax[2].plot(GSM_Ylms.month, iteration_mmwe.S11[:, j], c=c) # add axis labels and adjust font sizes for axis ticks - for i,key in enumerate(iteration_mmwe.fields): + for i, key in enumerate(iteration_mmwe.fields): ax[i].set_ylabel('mm', fontsize=14) # axis label - artist = offsetbox.AnchoredText(key, pad=0., - prop=dict(size=16,weight='bold'), frameon=False, loc=2) + artist = offsetbox.AnchoredText( + key, + pad=0.0, + prop=dict(size=16, weight='bold'), + frameon=False, + loc=2, + ) ax[i].add_artist(artist) # axes tick adjustments - ax[i].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[i].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # labels and set limits ax[2].set_xlabel('Grace Month', fontsize=14) - xmin = np.floor(GSM_Ylms.month[0]/10.)*10. - xmax = np.ceil(GSM_Ylms.month[-1]/10.)*10. - ax[2].set_xlim(xmin,xmax) + xmin = np.floor(GSM_Ylms.month[0] / 10.0) * 10.0 + xmax = np.ceil(GSM_Ylms.month[-1] / 10.0) * 10.0 + ax[2].set_xlim(xmin, xmax) ax[2].xaxis.set_minor_locator(ticker.MultipleLocator(5)) ax[2].xaxis.get_major_formatter().set_useOffset(False) # adjust locations of subplots and save to file - fig.subplots_adjust(left=0.12,right=0.94,bottom=0.06,top=0.98,hspace=0.1) - args = (PROC,DREL,MODEL,slf_str,slr_str,gia_str,ds_str) + fig.subplots_adjust( + left=0.12, right=0.94, bottom=0.06, top=0.98, hspace=0.1 + ) + args = (PROC, DREL, MODEL, slf_str, slr_str, gia_str, ds_str) FILE = 'Geocenter_Iterative_{0}_{1}_{2}{3}{4}{5}{6}.pdf'.format(*args) PLOT2 = DIRECTORY.joinpath(FILE) - metadata = {'Title':pathlib.Path(sys.argv[0]).name} + metadata = {'Title': pathlib.Path(sys.argv[0]).name} plt.savefig(PLOT2, format='pdf', metadata=metadata) plt.clf() # set the permissions mode of the output files @@ -1189,63 +1427,90 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # return the list of output files and the number of iterations return (output_files, n_iter) + # PURPOSE: print YAML header to top of file def print_header(fid): # print header fid.write('{0}:\n'.format('header')) # data dimensions fid.write(' {0}:\n'.format('dimensions')) - fid.write(' {0:22}: {1:d}\n'.format('degree',1)) - fid.write(' {0:22}: {1:d}\n'.format('order',1)) + fid.write(' {0:22}: {1:d}\n'.format('degree', 1)) + fid.write(' {0:22}: {1:d}\n'.format('order', 1)) fid.write('\n') + # PURPOSE: print spherical harmonic attributes to YAML header -def print_harmonic(fid,kl): +def print_harmonic(fid, kl): # non-standard attributes fid.write(' {0}:\n'.format('non-standard_attributes')) # load love number fid.write(' {0:22}:\n'.format('love_number')) long_name = 'Gravitational Load Love Number of Degree 1 (k1)' - fid.write(' {0:20}: {1}\n'.format('long_name',long_name)) - fid.write(' {0:20}: {1:0.3f}\n'.format('value',kl)) + fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) + fid.write(' {0:20}: {1:0.3f}\n'.format('value', kl)) # data format data_format = '(f11.4,3e14.6,i4)' - fid.write(' {0:22}: {1}\n'.format('formatting_string',data_format)) + fid.write(' {0:22}: {1}\n'.format('formatting_string', data_format)) fid.write('\n') + # PURPOSE: print global attributes to YAML header -def print_global(fid,PROC,DREL,MODEL,AOD,GIA,SLR,S21,month): +def print_global(fid, PROC, DREL, MODEL, AOD, GIA, SLR, S21, month): fid.write(' {0}:\n'.format('global_attributes')) MISSION = 'GRACE/GRACE-FO' - title = '{0} Geocenter Coefficients {1} {2}'.format(MISSION,PROC,DREL) - fid.write(' {0:22}: {1}\n'.format('title',title)) + title = '{0} Geocenter Coefficients {1} {2}'.format(MISSION, PROC, DREL) + fid.write(' {0:22}: {1}\n'.format('title', title)) summary = [] - summary.append(('Geocenter coefficients derived from {0} mission ' - 'measurements and {1} ocean model outputs.').format(MISSION,MODEL)) + summary.append( + ( + 'Geocenter coefficients derived from {0} mission ' + 'measurements and {1} ocean model outputs.' + ).format(MISSION, MODEL) + ) if AOD: - summary.append((' These coefficients represent the largest-scale ' - 'variability of atmospheric, oceanic, hydrologic, cryospheric, ' - 'and solid Earth processes.')) + summary.append( + ( + ' These coefficients represent the largest-scale ' + 'variability of atmospheric, oceanic, hydrologic, cryospheric, ' + 'and solid Earth processes.' + ) + ) else: - summary.append((' These coefficients represent the largest-scale ' - 'variability of hydrologic, cryospheric, and solid Earth ' - 'processes. In addition, the coefficients represent the ' - 'atmospheric and oceanic processes not captured in the {0} {1} ' - 'de-aliasing product.').format(MISSION,DREL)) + summary.append( + ( + ' These coefficients represent the largest-scale ' + 'variability of hydrologic, cryospheric, and solid Earth ' + 'processes. In addition, the coefficients represent the ' + 'atmospheric and oceanic processes not captured in the {0} {1} ' + 'de-aliasing product.' + ).format(MISSION, DREL) + ) # get GIA parameters - summary.append((' Glacial Isostatic Adjustment (GIA) estimates from ' - '{0} have been restored.').format(GIA.citation)) + summary.append( + ( + ' Glacial Isostatic Adjustment (GIA) estimates from ' + '{0} have been restored.' + ).format(GIA.citation) + ) if AOD: - summary.append((' Monthly atmospheric and oceanic de-aliasing product ' - 'has been restored.')) + summary.append( + ( + ' Monthly atmospheric and oceanic de-aliasing product ' + 'has been restored.' + ) + ) elif (DREL == 'RL05') and not AOD: - summary.append((' ECMWF corrections from Fagiolini et al. (2015) have ' - 'been restored.')) - fid.write(' {0:22}: {1}\n'.format('summary',''.join(summary))) + summary.append( + ( + ' ECMWF corrections from Fagiolini et al. (2015) have ' + 'been restored.' + ) + ) + fid.write(' {0:22}: {1}\n'.format('summary', ''.join(summary))) project = [] project.append('NASA Gravity Recovery And Climate Experiment (GRACE)') project.append('GRACE Follow-On (GRACE-FO)') if (DREL == 'RL06') else None - fid.write(' {0:22}: {1}\n'.format('project',', '.join(project))) + fid.write(' {0:22}: {1}\n'.format('project', ', '.join(project))) keywords = [] keywords.append('GRACE') keywords.append('GRACE-FO') if (DREL == 'RL06') else None @@ -1256,85 +1521,129 @@ def print_global(fid,PROC,DREL,MODEL,AOD,GIA,SLR,S21,month): keywords.append('Time Variable Gravity') keywords.append('Mass Transport') keywords.append('Satellite Geodesy') - fid.write(' {0:22}: {1}\n'.format('keywords',', '.join(keywords))) + fid.write(' {0:22}: {1}\n'.format('keywords', ', '.join(keywords))) vocabulary = 'NASA Global Change Master Directory (GCMD) Science Keywords' - fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary',vocabulary)) + fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary', vocabulary)) hist = '{0} Level-3 Data created at UC Irvine'.format(MISSION) - fid.write(' {0:22}: {1}\n'.format('history',hist)) + fid.write(' {0:22}: {1}\n'.format('history', hist)) src = 'An inversion using {0} measurements and {1} ocean model outputs.' if AOD: - src += (' Atmospheric and oceanic variation restored using the {2} ' - 'de-aliasing product.') - args = (MISSION,MODEL,DREL) - fid.write(' {0:22}: {1}\n'.format('source',src.format(*args))) + src += ( + ' Atmospheric and oceanic variation restored using the {2} ' + 'de-aliasing product.' + ) + args = (MISSION, MODEL, DREL) + fid.write(' {0:22}: {1}\n'.format('source', src.format(*args))) # fid.write(' {0:22}: {1}\n'.format('platform','GRACE-A, GRACE-B')) # vocabulary = 'NASA Global Change Master Directory platform keywords' # fid.write(' {0:22}: {1}\n'.format('platform_vocabulary',vocabulary)) # fid.write(' {0:22}: {1}\n'.format('instrument','ACC,KBR,GPS,SCA')) # vocabulary = 'NASA Global Change Master Directory instrument keywords' # fid.write(' {0:22}: {1}\n'.format('instrument_vocabulary',vocabulary)) - fid.write(' {0:22}: {1:d}\n'.format('processing_level',3)) + fid.write(' {0:22}: {1:d}\n'.format('processing_level', 3)) ack = [] - ack.append(('Work was supported by an appointment to the NASA Postdoctoral ' - 'Program at NASA Goddard Space Flight Center, administered by ' - 'Universities Space Research Association under contract with NASA')) + ack.append( + ( + 'Work was supported by an appointment to the NASA Postdoctoral ' + 'Program at NASA Goddard Space Flight Center, administered by ' + 'Universities Space Research Association under contract with NASA' + ) + ) ack.append('GRACE is a joint mission of NASA (USA) and DLR (Germany)') - if (DREL == 'RL06'): - ack.append('GRACE-FO is a joint mission of NASA (USA) and GFZ (Germany)') - fid.write(' {0:22}: {1}\n'.format('acknowledgement','. '.join(ack))) + if DREL == 'RL06': + ack.append( + 'GRACE-FO is a joint mission of NASA (USA) and GFZ (Germany)' + ) + fid.write(' {0:22}: {1}\n'.format('acknowledgement', '. '.join(ack))) PRODUCT_VERSION = f'Release-{DREL[2:]}' - fid.write(' {0:22}: {1}\n'.format('product_version',PRODUCT_VERSION)) + fid.write(' {0:22}: {1}\n'.format('product_version', PRODUCT_VERSION)) fid.write(' {0:22}:\n'.format('references')) reference = [] # geocenter citations - reference.append(('T. C. Sutterley, and I. Velicogna, "Improved estimates ' - 'of geocenter variability from time-variable gravity and ocean model ' - 'outputs", Remote Sensing, 11(18), 2108, (2019). ' - 'https://doi.org/10.3390/rs11182108')) - reference.append(('S. C. Swenson, D. P. Chambers, and J. Wahr, "Estimating ' - 'geocenter variations from a combination of GRACE and ocean model ' - 'output", Journal of Geophysical Research - Solid Earth, 113(B08410), ' - '(2008). https://doi.org/10.1029/2007JB005338')) + reference.append( + ( + 'T. C. Sutterley, and I. Velicogna, "Improved estimates ' + 'of geocenter variability from time-variable gravity and ocean model ' + 'outputs", Remote Sensing, 11(18), 2108, (2019). ' + 'https://doi.org/10.3390/rs11182108' + ) + ) + reference.append( + ( + 'S. C. Swenson, D. P. Chambers, and J. Wahr, "Estimating ' + 'geocenter variations from a combination of GRACE and ocean model ' + 'output", Journal of Geophysical Research - Solid Earth, 113(B08410), ' + '(2008). https://doi.org/10.1029/2007JB005338' + ) + ) # GIA citation reference.append(GIA.reference) # ECMWF jump corrections citation if (DREL == 'RL05') and not AOD: - reference.append(('E. Fagiolini, F. Flechtner, M. Horwath, H. Dobslaw, ' - '''"Correction of inconsistencies in ECMWF's operational ''' - '''analysis data during de-aliasing of GRACE gravity models", ''' - 'Geophysical Journal International, 202(3), 2150, (2015). ' - 'https://doi.org/10.1093/gji/ggv276')) + reference.append( + ( + 'E. Fagiolini, F. Flechtner, M. Horwath, H. Dobslaw, ' + """"Correction of inconsistencies in ECMWF's operational """ + """analysis data during de-aliasing of GRACE gravity models", """ + 'Geophysical Journal International, 202(3), 2150, (2015). ' + 'https://doi.org/10.1093/gji/ggv276' + ) + ) # SLR citation for a given solution - if (SLR == 'CSR'): - reference.append(('M. Cheng, B. D. Tapley, and J. C. Ries, ' - '''"Deceleration in the Earth's oblateness", Journal of ''' - 'Geophysical Research: Solid Earth, 118(2), 740-747, (2013). ' - 'https://doi.org/10.1002/jgrb.50058')) - elif (SLR == 'GSFC'): - reference.append(('B. D. Loomis, K. E. Rachlin, and S. B. Luthcke, ' - '"Improved Earth Oblateness Rate Reveals Increased Ice Sheet Losses ' - 'and Mass-Driven Sea Level Rise", Geophysical Research Letters, ' - '46(12), 6910-6917, (2019). https://doi.org/10.1029/2019GL082929')) - reference.append(('B. D. Loomis, K. E. Rachlin, D. N. Wiese, ' - 'F. W. Landerer, and S. B. Luthcke, "Replacing GRACE/GRACE-FO C30 ' - 'with satellite laser ranging: Impacts on Antarctic Ice Sheet mass ' - 'change", Geophysical Research Letters, 47(3), (2020). ' - 'https://doi.org/10.1029/2019GL085488')) - elif (SLR == 'GFZ'): - reference.append(('R. Koenig, P. Schreiner, and C. Dahle, "Monthly ' - 'estimates of C(2,0) generated by GFZ from SLR satellites based ' - 'on GFZ GRACE/GRACE-FO RL06 background models." V. 1.0. GFZ Data ' - 'Services, (2019). http://doi.org/10.5880/GFZ.GRAVIS_06_C20_SLR')) - if (S21 == 'CSR'): - reference.append(('M. Cheng, J. C. Ries, and B. D. Tapley, ' - '''"Variations of the Earth's figure axis from satellite laser ''' - 'ranging and GRACE", Journal of Geophysical Research: Solid Earth, ' - '116, B01409, (2011). https://doi.org/10.1029/2010JB000850')) - elif (S21 == 'GFZ'): - reference.append(('C. Dahle and M. Murboeck, "Post-processed ' - 'GRACE/GRACE-FO Geopotential GSM Coefficients GFZ RL06 ' - '(Level-2B Product)." V. 0002. GFZ Data Services, (2019). ' - 'http://doi.org/10.5880/GFZ.GRAVIS_06_L2B')) + if SLR == 'CSR': + reference.append( + ( + 'M. Cheng, B. D. Tapley, and J. C. Ries, ' + """"Deceleration in the Earth's oblateness", Journal of """ + 'Geophysical Research: Solid Earth, 118(2), 740-747, (2013). ' + 'https://doi.org/10.1002/jgrb.50058' + ) + ) + elif SLR == 'GSFC': + reference.append( + ( + 'B. D. Loomis, K. E. Rachlin, and S. B. Luthcke, ' + '"Improved Earth Oblateness Rate Reveals Increased Ice Sheet Losses ' + 'and Mass-Driven Sea Level Rise", Geophysical Research Letters, ' + '46(12), 6910-6917, (2019). https://doi.org/10.1029/2019GL082929' + ) + ) + reference.append( + ( + 'B. D. Loomis, K. E. Rachlin, D. N. Wiese, ' + 'F. W. Landerer, and S. B. Luthcke, "Replacing GRACE/GRACE-FO C30 ' + 'with satellite laser ranging: Impacts on Antarctic Ice Sheet mass ' + 'change", Geophysical Research Letters, 47(3), (2020). ' + 'https://doi.org/10.1029/2019GL085488' + ) + ) + elif SLR == 'GFZ': + reference.append( + ( + 'R. Koenig, P. Schreiner, and C. Dahle, "Monthly ' + 'estimates of C(2,0) generated by GFZ from SLR satellites based ' + 'on GFZ GRACE/GRACE-FO RL06 background models." V. 1.0. GFZ Data ' + 'Services, (2019). http://doi.org/10.5880/GFZ.GRAVIS_06_C20_SLR' + ) + ) + if S21 == 'CSR': + reference.append( + ( + 'M. Cheng, J. C. Ries, and B. D. Tapley, ' + """"Variations of the Earth's figure axis from satellite laser """ + 'ranging and GRACE", Journal of Geophysical Research: Solid Earth, ' + '116, B01409, (2011). https://doi.org/10.1029/2010JB000850' + ) + ) + elif S21 == 'GFZ': + reference.append( + ( + 'C. Dahle and M. Murboeck, "Post-processed ' + 'GRACE/GRACE-FO Geopotential GSM Coefficients GFZ RL06 ' + '(Level-2B Product)." V. 0002. GFZ Data Services, (2019). ' + 'http://doi.org/10.5880/GFZ.GRAVIS_06_L2B' + ) + ) # print list of references for ref in reference: fid.write(' - {0}\n'.format(ref)) @@ -1346,19 +1655,24 @@ def print_global(fid,PROC,DREL,MODEL,AOD,GIA,SLR,S21,month): fid.write(' {0:22}: {1}\n'.format('creator_url', url)) fid.write(' {0:22}: {1}\n'.format('creator_type', 'group')) inst = 'University of Washington; University of California, Irvine' - fid.write(' {0:22}: {1}\n'.format('creator_institution',inst)) + fid.write(' {0:22}: {1}\n'.format('creator_institution', inst)) # date range and date created - calendar_year,calendar_month = gravtk.time.grace_to_calendar(month) - start_time = '{0:4.0f}-{1:02.0f}'.format(calendar_year[0],calendar_month[0]) + calendar_year, calendar_month = gravtk.time.grace_to_calendar(month) + start_time = '{0:4.0f}-{1:02.0f}'.format( + calendar_year[0], calendar_month[0] + ) fid.write(' {0:22}: {1}\n'.format('time_coverage_start', start_time)) - end_time = '{0:4.0f}-{1:02.0f}'.format(calendar_year[-1],calendar_month[-1]) + end_time = '{0:4.0f}-{1:02.0f}'.format( + calendar_year[-1], calendar_month[-1] + ) fid.write(' {0:22}: {1}\n'.format('time_coverage_end', end_time)) - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) fid.write(' {0:22}: {1}\n'.format('date_created', today)) fid.write('\n') + # PURPOSE: print variable descriptions to YAML header -def print_variables(fid,data_precision,data_units): +def print_variables(fid, data_precision, data_units): # variables fid.write(' {0}:\n'.format('variables')) # time @@ -1401,10 +1715,11 @@ def print_variables(fid,data_precision,data_units): # end of header fid.write('\n\n# End of YAML header\n') + # PURPOSE: print a file log for the GRACE degree one analysis def output_log_file(input_arguments, output_files, n_iter): # format: calc_degree_one_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'calc_degree_one_run_{0}_PID-{1:d}.log'.format(*args) DIRECTORY = pathlib.Path(input_arguments.directory).joinpath('geocenter') # create a unique log and open the log file @@ -1424,10 +1739,11 @@ def output_log_file(input_arguments, output_files, n_iter): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE degree one analysis def output_error_log_file(input_arguments): # format: calc_degree_one_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'calc_degree_one_failed_run_{0}_PID-{1:d}.log'.format(*args) DIRECTORY = pathlib.Path(input_arguments.directory).joinpath('geocenter') # create a unique log and open the log file @@ -1443,6 +1759,7 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -1450,63 +1767,144 @@ def arguments(): coefficients of degree 2 and greater, and ocean bottom pressure variations from ECCO and OMCT/MPIOM """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) - parser.convert_arg_line_to_args = \ - gravtk.utilities.convert_arg_line_to_args + parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') - parser.add_argument('--kl','-k', - type=float, default=0.021, nargs='?', - help='Degree 1 gravitational Load Love number') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) + parser.add_argument( + '--kl', + '-k', + type=float, + default=0.021, + nargs='?', + help='Degree 1 gravitational Load Love number', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -1522,40 +1920,75 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # correct for pole tide drift follow Wahr et al. (2015) - parser.add_argument('--pole-tide', - default=False, action='store_true', - help='Correct for pole tide drift') + parser.add_argument( + '--pole-tide', + default=False, + action='store_true', + help='Correct for pole tide drift', + ) # replace low degree harmonics with values from Satellite Laser Ranging - parser.add_argument('--slr-c20', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C20 coefficients with SLR values') - parser.add_argument('--slr-21', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C21 and S21 coefficients with SLR values') - parser.add_argument('--slr-22', - type=str, default=None, choices=['CSR','GSFC'], - help='Replace C22 and S22 coefficients with SLR values') - parser.add_argument('--slr-c30', - type=str, default=None, choices=['CSR','GFZ','GSFC','LARES'], - help='Replace C30 coefficients with SLR values') - parser.add_argument('--slr-c40', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C40 coefficients with SLR values') - parser.add_argument('--slr-c50', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C50 coefficients with SLR values') + parser.add_argument( + '--slr-c20', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C20 coefficients with SLR values', + ) + parser.add_argument( + '--slr-21', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C21 and S21 coefficients with SLR values', + ) + parser.add_argument( + '--slr-22', + type=str, + default=None, + choices=['CSR', 'GSFC'], + help='Replace C22 and S22 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c30', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC', 'LARES'], + help='Replace C30 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c40', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C40 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c50', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C50 coefficients with SLR values', + ) # ocean model list choices = [] choices.append('OMCT') @@ -1565,91 +1998,157 @@ def arguments(): choices.append('ECCO_V4r3') choices.append('ECCO_V4r4') choices.append('ECCO_V5alpha') - parser.add_argument('--ocean-model', - metavar='MODEL', type=str, - default='MPIOM', choices=choices, - help='Ocean model to use') + parser.add_argument( + '--ocean-model', + metavar='MODEL', + type=str, + default='MPIOM', + choices=choices, + help='Ocean model to use', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format for ocean models') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format for ocean models', + ) # index file for ocean model harmonics - parser.add_argument('--ocean-file', + parser.add_argument( + '--ocean-file', type=pathlib.Path, - help='Index file for ocean model harmonics') + help='Index file for ocean model harmonics', + ) # mean file to remove - parser.add_argument('--mean-file', + parser.add_argument( + '--mean-file', type=pathlib.Path, - help='GRACE/GRACE-FO mean file to remove from the harmonic data') + help='GRACE/GRACE-FO mean file to remove from the harmonic data', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--mean-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5','gfc'], - help='Input data format for GRACE/GRACE-FO mean file') + parser.add_argument( + '--mean-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5', 'gfc'], + help='Input data format for GRACE/GRACE-FO mean file', + ) # monthly files to be removed from the GRACE/GRACE-FO data - parser.add_argument('--remove-file', - type=pathlib.Path, nargs='+', - help='Monthly files to be removed from the GRACE/GRACE-FO data') + parser.add_argument( + '--remove-file', + type=pathlib.Path, + nargs='+', + help='Monthly files to be removed from the GRACE/GRACE-FO data', + ) choices = [] - choices.extend(['ascii','netCDF4','HDF5']) - choices.extend(['index-ascii','index-netCDF4','index-HDF5']) - parser.add_argument('--remove-format', - type=str, nargs='+', choices=choices, - help='Input data format for files to be removed') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + choices.extend(['ascii', 'netCDF4', 'HDF5']) + choices.extend(['index-ascii', 'index-netCDF4', 'index-HDF5']) + parser.add_argument( + '--remove-format', + type=str, + nargs='+', + choices=choices, + help='Input data format for files to be removed', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # run with iterative scheme - parser.add_argument('--iterative', - default=False, action='store_true', - help='Iterate degree one solutions') + parser.add_argument( + '--iterative', + default=False, + action='store_true', + help='Iterate degree one solutions', + ) # least squares solver - choices = ('inv','lstsq','gelsd', 'gelsy', 'gelss') - parser.add_argument('--solver','-s', - type=str, default='lstsq', choices=choices, - help='Least squares solver for degree one solutions') + choices = ('inv', 'lstsq', 'gelsd', 'gelsy', 'gelss') + parser.add_argument( + '--solver', + '-s', + type=str, + default='lstsq', + choices=choices, + help='Least squares solver for degree one solutions', + ) # run with sea level fingerprints - parser.add_argument('--fingerprint', - default=False, action='store_true', - help='Redistribute land-water flux using sea level fingerprints') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--fingerprint', + default=False, + action='store_true', + help='Redistribute land-water flux using sea level fingerprints', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # land-sea mask for calculating ocean mass and land water flux - land_mask_file = gravtk.utilities.get_data_path(['data','land_fcn_300km.nc']) - parser.add_argument('--mask', + land_mask_file = gravtk.utilities.get_data_path( + ['data', 'land_fcn_300km.nc'] + ) + parser.add_argument( + '--mask', type=pathlib.Path, default=land_mask_file, - help='Land-sea mask for calculating ocean mass and land water flux') + help='Land-sea mask for calculating ocean mass and land water flux', + ) # create output plots - parser.add_argument('--plot','-p', - default=False, action='store_true', - help='Create output plots for components and iterations') + parser.add_argument( + '--plot', + '-p', + default=False, + action='store_true', + help='Create output plots for components and iterations', + ) # copy output files - parser.add_argument('--copy','-C', - default=False, action='store_true', - help='Copy output files for distribution and archival') + parser.add_argument( + '--copy', + '-C', + default=False, + action='store_true', + help='Copy output files for distribution and archival', + ) # Output log file for each job in forms # calc_degree_one_run_2002-04-01_PID-00000.log # calc_degree_one_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -1659,7 +2158,7 @@ def main(): try: info(args) # run calc_degree_one algorithm with parameters - output_files,n_iter = calc_degree_one( + output_files, n_iter = calc_degree_one( args.directory, args.center, args.release, @@ -1697,18 +2196,20 @@ def main(): LANDMASK=args.mask, PLOT=args.plot, COPY=args.copy, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files,n_iter) + if args.log: # write successful job completion log file + output_log_file(args, output_files, n_iter) + # run main program if __name__ == '__main__': diff --git a/geocenter/delta_degree_one.py b/geocenter/delta_degree_one.py index 8ea80363..68830bce 100644 --- a/geocenter/delta_degree_one.py +++ b/geocenter/delta_degree_one.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" delta_degree_one.py Written by Tyler Sutterley (07/2026) @@ -188,6 +188,7 @@ Updated 06/2019: added parameter LANDMASK for setting the land-sea mask Written 11/2018 """ + from __future__ import print_function import sys @@ -202,6 +203,7 @@ import scipy.linalg import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -211,8 +213,14 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate the satellite error for a geocenter time-series -def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, +def delta_degree_one( + base_dir, + PROC, + DREL, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -236,8 +244,8 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, FINGERPRINT=False, EXPANSION=None, LANDMASK=None, - MODE=0o775): - + MODE=0o775, +): # output directory base_dir = pathlib.Path(base_dir).expanduser().absolute() DIRECTORY = base_dir.joinpath('geocenter') @@ -260,40 +268,40 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # output flag for using sea level fingerprints slf_str = '_SLF' if FINGERPRINT else '' # output flag for low-degree harmonic replacements - if SLR_21 in ('CSR','GFZ','GSFC'): + if SLR_21 in ('CSR', 'GFZ', 'GSFC'): C21_str = f'_w{SLR_21}_21' else: C21_str = '' - if SLR_22 in ('CSR','GSFC'): + if SLR_22 in ('CSR', 'GSFC'): C22_str = f'_w{SLR_22}_22' else: C22_str = '' if SLR_C30 in ('GSFC',): # C30 replacement now default for all solutions C30_str = '' - elif SLR_C30 in ('CSR','GFZ','LARES'): + elif SLR_C30 in ('CSR', 'GFZ', 'LARES'): C30_str = f'_w{SLR_C30}_C30' else: C30_str = '' - if SLR_C40 in ('CSR','GSFC','LARES'): + if SLR_C40 in ('CSR', 'GSFC', 'LARES'): C40_str = f'_w{SLR_C40}_C40' else: C40_str = '' - if SLR_C50 in ('CSR','GSFC','LARES'): + if SLR_C50 in ('CSR', 'GSFC', 'LARES'): C50_str = f'_w{SLR_C50}_C50' else: C50_str = '' # combine satellite laser ranging flags - slr_str = ''.join([C21_str,C22_str,C30_str,C40_str,C50_str]) + slr_str = ''.join([C21_str, C22_str, C30_str, C40_str, C50_str]) # ocean model string model_str = 'MPIOM' if (DREL == 'RL06') else 'OMCT' # suffix for input ascii, netcdf and HDF5 files suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # read load love numbers - LOVE = gravtk.load_love_numbers(EXPANSION, - LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF', - FORMAT='class') + LOVE = gravtk.load_love_numbers( + EXPANSION, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF', FORMAT='class' + ) # set gravitational load love number to a specific value if LOVE_K1: LOVE.kl[1] = np.copy(LOVE_K1) @@ -303,8 +311,8 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) - rho_e = factors.rho_e# Average Density of the Earth [g/cm^3] - rad_e = factors.rad_e# Average Radius of the Earth [cm] + rho_e = factors.rho_e # Average Density of the Earth [g/cm^3] + rad_e = factors.rad_e # Average Radius of the Earth [cm] l = factors.l # Factor for converting to Mass SH dfactor = factors.cmwe @@ -312,24 +320,25 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # Read Smoothed Ocean and Land Functions # smoothed functions are from the read_ocean_function.py program # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, - varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) # degree spacing and grid dimensions # will create GRACE spatial fields with same dimensions - dlon,dlat = landsea.spacing + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # spatial parameters in radians dphi = np.radians(dlon) dth = np.radians(dlat) # longitude and colatitude in radians - phi = np.radians(landsea.lon[np.newaxis,:]) + phi = np.radians(landsea.lon[np.newaxis, :]) th = np.radians(90.0 - np.squeeze(landsea.lat)) # create land function - land_function = np.zeros((nlon, nlat),dtype=np.float64) + land_function = np.zeros((nlon, nlat), dtype=np.float64) # extract land function from file # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data.T >= 1) & (landsea.data.T <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data.T >= 1) & (landsea.data.T <= 3)) + land_function[indx, indy] = 1.0 # calculate ocean function from land function ocean_function = 1.0 - land_function @@ -339,30 +348,56 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # calculate spherical harmonics of ocean function to degree 1 # mass is equivalent to 1 cm ocean height change # eustatic ratio = -land total/ocean total - ocean_Ylms = gravtk.gen_stokes(ocean_function, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=1, LOVE=LOVE, PLM=PLM[:2,:2,:]) + ocean_Ylms = gravtk.gen_stokes( + ocean_function, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + LOVE=LOVE, + PLM=PLM[:2, :2, :], + ) # Gaussian Smoothing (Jekeli, 1981) - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) # reading GRACE months for input date range # replacing low-degree harmonics with SLR values if specified # correcting for Pole-Tide and Atmospheric Jumps if specified - Ylms = gravtk.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_C40=SLR_C40, SLR_C50=SLR_C50, - POLE_TIDE=POLE_TIDE, ATM=ATM, MODEL_DEG1=False) + Ylms = gravtk.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_C40=SLR_C40, + SLR_C50=SLR_C50, + POLE_TIDE=POLE_TIDE, + ATM=ATM, + MODEL_DEG1=False, + ) # create harmonics object from GRACE/GRACE-FO data GSM_Ylms = gravtk.harmonics().from_dict(Ylms) # 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 = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GSM_Ylms.subtract(mean_Ylms) else: @@ -380,8 +415,18 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # calculating GRACE/GRACE-FO error (Wahr et al. 2006) # output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) - fargs = (PROC,DREL,DSET,LMAX,order_str,ds_str,atm_str,GSM_Ylms.month[0], - GSM_Ylms.month[-1], suffix[DATAFORM]) + fargs = ( + PROC, + DREL, + DSET, + LMAX, + order_str, + ds_str, + atm_str, + GSM_Ylms.month[0], + GSM_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 = GSM_Ylms.directory.joinpath(delta_format.format(*fargs)) # check full path of the GRACE directory for delta file @@ -393,38 +438,41 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # Delta coefficients of GRACE time series (Error components) delta_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - delta_Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - delta_Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + 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'))): + 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 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 + 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']): + for cs, csharm in enumerate(['clm', 'slm']): # calculate GRACE Error (Noise of smoothed time-series) # With Annual and Semi-Annual Terms val1 = getattr(GSM_Ylms, csharm) - smth = gravtk.time_series.smooth(GSM_Ylms.time, - val1[l,m,:], HFWTH=HFWTH) + smth = gravtk.time_series.smooth( + GSM_Ylms.time, val1[l, m, :], HFWTH=HFWTH + ) # number of smoothed points nsmth = len(smth['data']) tsmth = np.mean(smth['time']) # GRACE/GRACE-FO delta Ylms # variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) - val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth) + val2[l, m] = np.sqrt(np.sum(smth['noise'] ** 2) / nsmth) # attributes for output files attributes = {} attributes['title'] = 'GRACE/GRACE-FO Spherical Harmonic Errors' - attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # save GRACE/GRACE-FO delta harmonics to file delta_Ylms.time = np.copy(tsmth) delta_Ylms.month = np.int64(nsmth) @@ -435,8 +483,7 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, output_files.append(DELTA_FILE) else: # read GRACE/GRACE-FO delta harmonics from file - delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, - format=DATAFORM) + delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, format=DATAFORM) # 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) @@ -446,21 +493,23 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # output [m,phi] m = GSM_Ylms.m # Integration factors (solid angle) - int_fact = np.sin(th)*dphi*dth + int_fact = np.sin(th) * dphi * dth # 4-pi normalization - norm = 1.0/(4.0*np.pi) + norm = 1.0 / (4.0 * np.pi) # calculating cos(m*phi) and sin(m*phi) using Euler's formula - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", m, phi)) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', m, phi)) # Legendre polynomials for degree 1 - P10 = np.squeeze(PLM[1,0,:]) - P11 = np.squeeze(PLM[1,1,:]) + P10 = np.squeeze(PLM[1, 0, :]) + P11 = np.squeeze(PLM[1, 1, :]) # PLM for spherical harmonic degrees 2+ up to LMAX # converted into mass and smoothed if specified - plmout = np.zeros((LMAX+1, MMAX+1, nlat)) + plmout = np.zeros((LMAX + 1, MMAX + 1, nlat)) # convert to smoothed coefficients of mass # Convolving plms with degree dependent factor and smoothing - plmout[:] = np.einsum("l,l,lmh->lmh", dfactor, wt, PLM[:LMAX+1,:MMAX+1,:]) + plmout[:] = np.einsum( + 'l,l,lmh->lmh', dfactor, wt, PLM[: LMAX + 1, : MMAX + 1, :] + ) # Initializing 3x3 I-Parameter matrix # (see equations 12 and 13 of Swenson et al., 2008) @@ -468,21 +517,39 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # I-Parameter matrix accounts for the fact that the GRACE data only # includes spherical harmonic degrees greater than or equal to 2 # C10, C11, S11 - PC10 = np.einsum("h...,p...->ph...", P10, m_phi[0,:].real) - PC11 = np.einsum("h...,p...->ph...", P11, m_phi[1,:].real) - PS11 = np.einsum("h...,p...->ph...", P11, m_phi[1,:].imag) + PC10 = np.einsum('h...,p...->ph...', P10, m_phi[0, :].real) + PC11 = np.einsum('h...,p...->ph...', P11, m_phi[1, :].real) + PS11 = np.einsum('h...,p...->ph...', P11, m_phi[1, :].imag) # C10: C10, C11, S11 - IMAT[0,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PC10) - IMAT[1,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PC11) - IMAT[2,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PS11) + IMAT[0, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PC10 + ) + IMAT[1, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PC11 + ) + IMAT[2, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PS11 + ) # C11: C10, C11, S11 - IMAT[0,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PC10) - IMAT[1,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PC11) - IMAT[2,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PS11) + IMAT[0, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PC10 + ) + IMAT[1, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PC11 + ) + IMAT[2, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PS11 + ) # S11: C10, C11, S11 - IMAT[0,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PC10) - IMAT[1,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PC11) - IMAT[2,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PS11) + IMAT[0, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PC10 + ) + IMAT[1, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PC11 + ) + IMAT[2, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PS11 + ) # iterate solutions: if not single iteration n_iter = 0 @@ -507,43 +574,53 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # calculate non-iterated terms (G-matrix parameters) # calculate geocenter component of ocean mass with GRACE # subset GRACE to degrees 2+ for calculating ocean mass - l2 = slice(2, LMAX+1) - pconv = np.einsum("lmh...,lm...->mh...", plmout[l2, :, :], delta_Ylms.ilm[l2, :]) + l2 = slice(2, LMAX + 1) + pconv = np.einsum( + 'lmh...,lm...->mh...', plmout[l2, :, :], delta_Ylms.ilm[l2, :] + ) # Multiplying by c/s(phi#m) to get surface density in cmwe (lon,lat) # ccos/ssin are mXphi, pcos/psin are mXtheta: resultant matrices are phiXtheta # The summation over spherical harmonic order is in this multiplication - rmass = np.einsum("mp...,mh...->ph...", m_phi, pconv).real + rmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # calculate G matrix parameters through a summation of each latitude # summation of integration factors, Legendre polynomials, # (convolution of order and harmonics) and the ocean mass at t - G.C10 = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, rmass) - G.C11 = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, rmass) - G.S11 = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, rmass) + G.C10 = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, rmass + ) + G.C11 = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, rmass + ) + G.S11 = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, rmass + ) # calculate degree one solution for each iteration (or single if not) while (eps > eps_max) and (n_iter < max_iter): # calculate eustatic component from GRACE (can iterate) - if (n_iter == 0): + if n_iter == 0: # for first iteration (will be only iteration if not ITERATIVE): # seasonal component of geocenter variation for land water - delta_Ylms.clm[1,0] = 0.0 - delta_Ylms.clm[1,1] = 0.0 - delta_Ylms.slm[1,1] = 0.0 + delta_Ylms.clm[1, 0] = 0.0 + delta_Ylms.clm[1, 1] = 0.0 + delta_Ylms.slm[1, 1] = 0.0 else: # for all others: use previous iteration of inversion # for each of the geocenter solutions (C10, C11, S11) - delta_Ylms.clm[1,0] = iteration.C10[n_iter-1] - delta_Ylms.clm[1,1] = iteration.C11[n_iter-1] - delta_Ylms.slm[1,1] = iteration.S11[n_iter-1] + delta_Ylms.clm[1, 0] = iteration.C10[n_iter - 1] + delta_Ylms.clm[1, 1] = iteration.C11[n_iter - 1] + delta_Ylms.slm[1, 1] = iteration.S11[n_iter - 1] # Summing product of plms and c/slms over all SH degrees - l1 = slice(1, LMAX+1) - pconv = np.einsum("lmh...,lm...->mh...", plmout[l1, :, :], delta_Ylms.ilm[l1, :]) + l1 = slice(1, LMAX + 1) + pconv = np.einsum( + 'lmh...,lm...->mh...', plmout[l1, :, :], delta_Ylms.ilm[l1, :] + ) # Multiplying by c/s(phi#m) to get surface density in cm w.e. (lonxlat) # ccos/ssin are mXphi, pcos/psin are mXtheta: resultant matrices are phiXtheta # The summation over spherical harmonic order is in this multiplication - lmass = np.einsum("mp...,mh...->ph...", m_phi, pconv).real + lmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # use sea level fingerprints or eustatic from GRACE land components if FINGERPRINT: @@ -553,36 +630,74 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # NOTE: this is an unscaled GRACE estimate that uses the # buffered land function when solving the sea-level equation. # possible improvement using scaled estimate with real coastlines - land_Ylms = gravtk.gen_stokes(land_function*lmass, landsea.lon, - landsea.lat, UNITS=1, LMIN=0, LMAX=EXPANSION, LOVE=LOVE) + land_Ylms = gravtk.gen_stokes( + land_function * lmass, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=EXPANSION, + LOVE=LOVE, + ) # 2) calculate sea level fingerprints of land mass at time t # use maximum of 3 iterations for computational efficiency - sea_level = gravtk.sea_level_equation(land_Ylms.clm, land_Ylms.slm, - landsea.lon, landsea.lat, land_function, LMAX=EXPANSION, - LOVE=LOVE, BODY_TIDE_LOVE=0, FLUID_LOVE=0, ITERATIONS=3, - POLAR=True, FILL_VALUE=0) + sea_level = gravtk.sea_level_equation( + land_Ylms.clm, + land_Ylms.slm, + landsea.lon, + landsea.lat, + land_function, + LMAX=EXPANSION, + LOVE=LOVE, + BODY_TIDE_LOVE=0, + FLUID_LOVE=0, + ITERATIONS=3, + POLAR=True, + FILL_VALUE=0, + ) # 3) convert sea level fingerprints into spherical harmonics - slf_Ylms = gravtk.gen_stokes(sea_level, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=1, PLM=PLM[:2,:2,:], LOVE=LOVE) + slf_Ylms = gravtk.gen_stokes( + sea_level, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + PLM=PLM[:2, :2, :], + LOVE=LOVE, + ) # 4) convert the slf degree 1 harmonics to mass with dfactor - eustatic = gravtk.geocenter().from_harmonics(slf_Ylms).scale(dfactor[1]) + eustatic = ( + gravtk.geocenter().from_harmonics(slf_Ylms).scale(dfactor[1]) + ) else: # steps to calculate eustatic component from GRACE land-water change: # 1) calculate total mass of 1 cm of ocean height (calculated above) # 2) calculate total land mass at time t (GRACE*land function) # NOTE: possible improvement using the sea-level equation to solve # for the spatial pattern of sea level from the land water mass - land_Ylms = gravtk.gen_stokes(lmass*land_function, - landsea.lon, landsea.lat, UNITS=1, LMIN=0, LMAX=1, - PLM=PLM[:2,:2,:], LOVE=LOVE) + land_Ylms = gravtk.gen_stokes( + lmass * land_function, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + PLM=PLM[:2, :2, :], + LOVE=LOVE, + ) # 3) calculate ratio between the total land mass and the total mass # of 1 cm of ocean height (negative as positive land = sea level drop) # this converts the total land change to ocean height change - eustatic_ratio = -land_Ylms.clm[0,0]/ocean_Ylms.clm[0,0] + eustatic_ratio = -land_Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0] # 4) scale degree one coefficients of ocean function with ratio # and convert the eustatic degree 1 harmonics to mass with dfactor - scale_factor = eustatic_ratio*dfactor[1] - eustatic = gravtk.geocenter().from_harmonics(ocean_Ylms).scale(scale_factor) + scale_factor = eustatic_ratio * dfactor[1] + eustatic = ( + gravtk.geocenter() + .from_harmonics(ocean_Ylms) + .scale(scale_factor) + ) # eustatic coefficients of degree 1 CMAT = np.array([eustatic.C10, eustatic.C11, eustatic.S11]) @@ -592,26 +707,31 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # this is mathematically equivalent to an iterative procedure # whereby the initial degree one coefficients are used to update # the G Matrix until (C10, C11, S11) converge - if (SOLVER == 'inv'): - DMAT = np.dot(np.linalg.inv(IMAT), (CMAT-GMAT)) - elif (SOLVER == 'lstsq'): - DMAT = np.linalg.lstsq(IMAT, (CMAT-GMAT), rcond=-1)[0] + if SOLVER == 'inv': + DMAT = np.dot(np.linalg.inv(IMAT), (CMAT - GMAT)) + elif SOLVER == 'lstsq': + DMAT = np.linalg.lstsq(IMAT, (CMAT - GMAT), rcond=-1)[0] elif SOLVER in ('gelsd', 'gelsy', 'gelss'): - DMAT, res, rnk, s = scipy.linalg.lstsq(IMAT, (CMAT-GMAT), - lapack_driver=SOLVER) + DMAT, res, rnk, s = scipy.linalg.lstsq( + IMAT, (CMAT - GMAT), lapack_driver=SOLVER + ) # save geocenter for iteration and time t - iteration.C10[n_iter] = DMAT[0]/dfactor[1] - iteration.C11[n_iter] = DMAT[1]/dfactor[1] - iteration.S11[n_iter] = DMAT[2]/dfactor[1] + iteration.C10[n_iter] = DMAT[0] / dfactor[1] + iteration.C11[n_iter] = DMAT[1] / dfactor[1] + iteration.S11[n_iter] = DMAT[2] / dfactor[1] # calculate difference between original geocenter coefficients and the # calculated coefficients for each of the geocenter solutions - sigma_C10 = (delta_Ylms.clm[1,0] - iteration.C10[n_iter])**2 - sigma_C11 = (delta_Ylms.clm[1,1] - iteration.C11[n_iter])**2 - sigma_S11 = (delta_Ylms.slm[1,1] - iteration.S11[n_iter])**2 - power = iteration.C10[n_iter]**2 + \ - iteration.C11[n_iter]**2 + \ - iteration.S11[n_iter]**2 - eps = np.sqrt(sigma_C10 + sigma_C11 + sigma_S11)/np.sqrt(np.sum(power)) + sigma_C10 = (delta_Ylms.clm[1, 0] - iteration.C10[n_iter]) ** 2 + sigma_C11 = (delta_Ylms.clm[1, 1] - iteration.C11[n_iter]) ** 2 + sigma_S11 = (delta_Ylms.slm[1, 1] - iteration.S11[n_iter]) ** 2 + power = ( + iteration.C10[n_iter] ** 2 + + iteration.C11[n_iter] ** 2 + + iteration.S11[n_iter] ** 2 + ) + eps = np.sqrt(sigma_C10 + sigma_C11 + sigma_S11) / np.sqrt( + np.sum(power) + ) # add 1 to n_iter counter n_iter += 1 @@ -619,24 +739,42 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # for each of the geocenter solutions (C10, C11, S11) # for the iterative case this will be the final iteration DEG1 = gravtk.geocenter() - DEG1.C10,DEG1.C11,DEG1.S11 = DMAT/dfactor[1] + DEG1.C10, DEG1.C11, DEG1.S11 = DMAT / dfactor[1] # output degree 1 coefficients file_format = '{0}_{1}_{2}{3}{4}{5}{6}{7}{8}.{9}' output_format = '{0:11.4f}{1:14.6e}{2:14.6e}{3:14.6e} {4:03d}\n' # public file format in fully normalized spherical harmonics # local version with all descriptor flags - a1=(PROC,DREL,model_str,slf_str,iter_str,slr_str,'',delta_str,ds_str,'txt') + a1 = ( + PROC, + DREL, + model_str, + slf_str, + iter_str, + slr_str, + '', + delta_str, + ds_str, + 'txt', + ) FILE1 = DIRECTORY.joinpath(file_format.format(*a1)) fid1 = FILE1.open(mode='w', encoding='utf8') # print headers print_header(fid1) - print_harmonic(fid1,LOVE.kl[1]) - print_global(fid1,PROC,DREL,model_str.replace('_',' '), - SLR_C20,SLR_21,GSM_Ylms.month) - print_variables(fid1,'single precision','fully normalized') + print_harmonic(fid1, LOVE.kl[1]) + print_global( + fid1, + PROC, + DREL, + model_str.replace('_', ' '), + SLR_C20, + SLR_21, + GSM_Ylms.month, + ) + print_variables(fid1, 'single precision', 'fully normalized') # output geocenter coefficients to file - fid1.write(output_format.format(tsmth,DEG1.C10,DEG1.C11,DEG1.S11,nsmth)) + fid1.write(output_format.format(tsmth, DEG1.C10, DEG1.C11, DEG1.S11, nsmth)) # close the output file fid1.close() # set the permissions mode of the output file @@ -646,49 +784,60 @@ def delta_degree_one(base_dir, PROC, DREL, LMAX, RAD, # return the list of output files and the number of iterations return (output_files, n_iter) + # PURPOSE: print YAML header to top of file def print_header(fid): # print header fid.write('{0}:\n'.format('header')) # data dimensions fid.write(' {0}:\n'.format('dimensions')) - fid.write(' {0:22}: {1:d}\n'.format('degree',1)) - fid.write(' {0:22}: {1:d}\n'.format('order',1)) + fid.write(' {0:22}: {1:d}\n'.format('degree', 1)) + fid.write(' {0:22}: {1:d}\n'.format('order', 1)) fid.write('\n') + # PURPOSE: print spherical harmonic attributes to YAML header -def print_harmonic(fid,kl): +def print_harmonic(fid, kl): # non-standard attributes fid.write(' {0}:\n'.format('non-standard_attributes')) # load love number fid.write(' {0:22}:\n'.format('love_number')) long_name = 'Gravitational Load Love Number of Degree 1 (k1)' - fid.write(' {0:20}: {1}\n'.format('long_name',long_name)) - fid.write(' {0:20}: {1:0.3f}\n'.format('value',kl)) + fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) + fid.write(' {0:20}: {1:0.3f}\n'.format('value', kl)) # data format data_format = '(f11.4,3e14.6,i4)' - fid.write(' {0:22}: {1}\n'.format('formatting_string',data_format)) + fid.write(' {0:22}: {1}\n'.format('formatting_string', data_format)) fid.write('\n') + # PURPOSE: print global attributes to YAML header -def print_global(fid,PROC,DREL,MODEL,SLR,S21,month): +def print_global(fid, PROC, DREL, MODEL, SLR, S21, month): fid.write(' {0}:\n'.format('global_attributes')) MISSION = 'GRACE/GRACE-FO' - title = '{0} Geocenter Coefficients {1} {2}'.format(MISSION,PROC,DREL) - fid.write(' {0:22}: {1}\n'.format('title',title)) + title = '{0} Geocenter Coefficients {1} {2}'.format(MISSION, PROC, DREL) + fid.write(' {0:22}: {1}\n'.format('title', title)) summary = [] - summary.append(('Geocenter error coefficients derived from {0} mission ' - 'measurements and {1} ocean model outputs.').format(MISSION,MODEL)) - summary.append((' These coefficients represent the largest-scale ' - 'variability of hydrologic, cryospheric, and solid Earth ' - 'processes. In addition, the coefficients represent the ' - 'atmospheric and oceanic processes not captured in the {0} {1} ' - 'de-aliasing product.').format(MISSION,DREL)) - fid.write(' {0:22}: {1}\n'.format('summary',''.join(summary))) + summary.append( + ( + 'Geocenter error coefficients derived from {0} mission ' + 'measurements and {1} ocean model outputs.' + ).format(MISSION, MODEL) + ) + summary.append( + ( + ' These coefficients represent the largest-scale ' + 'variability of hydrologic, cryospheric, and solid Earth ' + 'processes. In addition, the coefficients represent the ' + 'atmospheric and oceanic processes not captured in the {0} {1} ' + 'de-aliasing product.' + ).format(MISSION, DREL) + ) + fid.write(' {0:22}: {1}\n'.format('summary', ''.join(summary))) project = [] project.append('NASA Gravity Recovery And Climate Experiment (GRACE)') project.append('GRACE Follow-On (GRACE-FO)') if (DREL == 'RL06') else None - fid.write(' {0:22}: {1}\n'.format('project',', '.join(project))) + fid.write(' {0:22}: {1}\n'.format('project', ', '.join(project))) keywords = [] keywords.append('GRACE') keywords.append('GRACE-FO') if (DREL == 'RL06') else None @@ -699,80 +848,122 @@ def print_global(fid,PROC,DREL,MODEL,SLR,S21,month): keywords.append('Time Variable Gravity') keywords.append('Mass Transport') keywords.append('Satellite Geodesy') - fid.write(' {0:22}: {1}\n'.format('keywords',', '.join(keywords))) + fid.write(' {0:22}: {1}\n'.format('keywords', ', '.join(keywords))) vocabulary = 'NASA Global Change Master Directory (GCMD) Science Keywords' - fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary',vocabulary)) + fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary', vocabulary)) hist = '{0} Level-3 Data created at UC Irvine'.format(MISSION) - fid.write(' {0:22}: {1}\n'.format('history',hist)) + fid.write(' {0:22}: {1}\n'.format('history', hist)) src = 'An inversion using {0} measurements and {1} ocean model outputs.' - args = (MISSION,MODEL,DREL) - fid.write(' {0:22}: {1}\n'.format('source',src.format(*args))) + args = (MISSION, MODEL, DREL) + fid.write(' {0:22}: {1}\n'.format('source', src.format(*args))) # fid.write(' {0:22}: {1}\n'.format('platform','GRACE-A, GRACE-B')) # vocabulary = 'NASA Global Change Master Directory platform keywords' # fid.write(' {0:22}: {1}\n'.format('platform_vocabulary',vocabulary)) # fid.write(' {0:22}: {1}\n'.format('instrument','ACC,KBR,GPS,SCA')) # vocabulary = 'NASA Global Change Master Directory instrument keywords' # fid.write(' {0:22}: {1}\n'.format('instrument_vocabulary',vocabulary)) - fid.write(' {0:22}: {1:d}\n'.format('processing_level',3)) + fid.write(' {0:22}: {1:d}\n'.format('processing_level', 3)) ack = [] - ack.append(('Work was supported by an appointment to the NASA Postdoctoral ' - 'Program at NASA Goddard Space Flight Center, administered by ' - 'Universities Space Research Association under contract with NASA')) + ack.append( + ( + 'Work was supported by an appointment to the NASA Postdoctoral ' + 'Program at NASA Goddard Space Flight Center, administered by ' + 'Universities Space Research Association under contract with NASA' + ) + ) ack.append('GRACE is a joint mission of NASA (USA) and DLR (Germany)') - if (DREL == 'RL06'): - ack.append('GRACE-FO is a joint mission of NASA (USA) and GFZ (Germany)') - fid.write(' {0:22}: {1}\n'.format('acknowledgement','. '.join(ack))) + if DREL == 'RL06': + ack.append( + 'GRACE-FO is a joint mission of NASA (USA) and GFZ (Germany)' + ) + fid.write(' {0:22}: {1}\n'.format('acknowledgement', '. '.join(ack))) PRODUCT_VERSION = f'Release-{DREL[2:]}' - fid.write(' {0:22}: {1}\n'.format('product_version',PRODUCT_VERSION)) + fid.write(' {0:22}: {1}\n'.format('product_version', PRODUCT_VERSION)) fid.write(' {0:22}:\n'.format('references')) reference = [] # geocenter citations - reference.append(('T. C. Sutterley, and I. Velicogna, "Improved estimates ' - 'of geocenter variability from time-variable gravity and ocean model ' - 'outputs", Remote Sensing, 11(18), 2108, (2019). ' - 'https://doi.org/10.3390/rs11182108')) - reference.append(('S. C. Swenson, D. P. Chambers, and J. Wahr, "Estimating ' - 'geocenter variations from a combination of GRACE and ocean model ' - 'output", Journal of Geophysical Research - Solid Earth, 113(B08410), ' - '(2008). https://doi.org/10.1029/2007JB005338')) + reference.append( + ( + 'T. C. Sutterley, and I. Velicogna, "Improved estimates ' + 'of geocenter variability from time-variable gravity and ocean model ' + 'outputs", Remote Sensing, 11(18), 2108, (2019). ' + 'https://doi.org/10.3390/rs11182108' + ) + ) + reference.append( + ( + 'S. C. Swenson, D. P. Chambers, and J. Wahr, "Estimating ' + 'geocenter variations from a combination of GRACE and ocean model ' + 'output", Journal of Geophysical Research - Solid Earth, 113(B08410), ' + '(2008). https://doi.org/10.1029/2007JB005338' + ) + ) # ECMWF jump corrections citation - if (DREL == 'RL05'): - reference.append(('E. Fagiolini, F. Flechtner, M. Horwath, H. Dobslaw, ' - '''"Correction of inconsistencies in ECMWF's operational ''' - '''analysis data during de-aliasing of GRACE gravity models", ''' - 'Geophysical Journal International, 202(3), 2150, (2015). ' - 'https://doi.org/10.1093/gji/ggv276')) + if DREL == 'RL05': + reference.append( + ( + 'E. Fagiolini, F. Flechtner, M. Horwath, H. Dobslaw, ' + """"Correction of inconsistencies in ECMWF's operational """ + """analysis data during de-aliasing of GRACE gravity models", """ + 'Geophysical Journal International, 202(3), 2150, (2015). ' + 'https://doi.org/10.1093/gji/ggv276' + ) + ) # SLR citation for a given solution - if (SLR == 'CSR'): - reference.append(('M. Cheng, B. D. Tapley, and J. C. Ries, ' - '''"Deceleration in the Earth's oblateness", Journal of ''' - 'Geophysical Research: Solid Earth, 118(2), 740-747, (2013). ' - 'https://doi.org/10.1002/jgrb.50058')) - elif (SLR == 'GSFC'): - reference.append(('B. D. Loomis, K. E. Rachlin, and S. B. Luthcke, ' - '"Improved Earth Oblateness Rate Reveals Increased Ice Sheet Losses ' - 'and Mass-Driven Sea Level Rise", Geophysical Research Letters, ' - '46(12), 6910-6917, (2019). https://doi.org/10.1029/2019GL082929')) - reference.append(('B. D. Loomis, K. E. Rachlin, D. N. Wiese, ' - 'F. W. Landerer, and S. B. Luthcke, "Replacing GRACE/GRACE-FO C30 ' - 'with satellite laser ranging: Impacts on Antarctic Ice Sheet mass ' - 'change", Geophysical Research Letters, 47(3), (2020). ' - 'https://doi.org/10.1029/2019GL085488')) - elif (SLR == 'GFZ'): - reference.append(('R. Koenig, P. Schreiner, and C. Dahle, "Monthly ' - 'estimates of C(2,0) generated by GFZ from SLR satellites based ' - 'on GFZ GRACE/GRACE-FO RL06 background models." V. 1.0. GFZ Data ' - 'Services, (2019). http://doi.org/10.5880/GFZ.GRAVIS_06_C20_SLR')) - if (S21 == 'CSR'): - reference.append(('M. Cheng, J. C. Ries, and B. D. Tapley, ' - '''"Variations of the Earth's figure axis from satellite laser ''' - 'ranging and GRACE", Journal of Geophysical Research: Solid Earth, ' - '116, B01409, (2011). https://doi.org/10.1029/2010JB000850')) - elif (S21 == 'GFZ'): - reference.append(('C. Dahle and M. Murboeck, "Post-processed ' - 'GRACE/GRACE-FO Geopotential GSM Coefficients GFZ RL06 ' - '(Level-2B Product)." V. 0002. GFZ Data Services, (2019). ' - 'http://doi.org/10.5880/GFZ.GRAVIS_06_L2B')) + if SLR == 'CSR': + reference.append( + ( + 'M. Cheng, B. D. Tapley, and J. C. Ries, ' + """"Deceleration in the Earth's oblateness", Journal of """ + 'Geophysical Research: Solid Earth, 118(2), 740-747, (2013). ' + 'https://doi.org/10.1002/jgrb.50058' + ) + ) + elif SLR == 'GSFC': + reference.append( + ( + 'B. D. Loomis, K. E. Rachlin, and S. B. Luthcke, ' + '"Improved Earth Oblateness Rate Reveals Increased Ice Sheet Losses ' + 'and Mass-Driven Sea Level Rise", Geophysical Research Letters, ' + '46(12), 6910-6917, (2019). https://doi.org/10.1029/2019GL082929' + ) + ) + reference.append( + ( + 'B. D. Loomis, K. E. Rachlin, D. N. Wiese, ' + 'F. W. Landerer, and S. B. Luthcke, "Replacing GRACE/GRACE-FO C30 ' + 'with satellite laser ranging: Impacts on Antarctic Ice Sheet mass ' + 'change", Geophysical Research Letters, 47(3), (2020). ' + 'https://doi.org/10.1029/2019GL085488' + ) + ) + elif SLR == 'GFZ': + reference.append( + ( + 'R. Koenig, P. Schreiner, and C. Dahle, "Monthly ' + 'estimates of C(2,0) generated by GFZ from SLR satellites based ' + 'on GFZ GRACE/GRACE-FO RL06 background models." V. 1.0. GFZ Data ' + 'Services, (2019). http://doi.org/10.5880/GFZ.GRAVIS_06_C20_SLR' + ) + ) + if S21 == 'CSR': + reference.append( + ( + 'M. Cheng, J. C. Ries, and B. D. Tapley, ' + """"Variations of the Earth's figure axis from satellite laser """ + 'ranging and GRACE", Journal of Geophysical Research: Solid Earth, ' + '116, B01409, (2011). https://doi.org/10.1029/2010JB000850' + ) + ) + elif S21 == 'GFZ': + reference.append( + ( + 'C. Dahle and M. Murboeck, "Post-processed ' + 'GRACE/GRACE-FO Geopotential GSM Coefficients GFZ RL06 ' + '(Level-2B Product)." V. 0002. GFZ Data Services, (2019). ' + 'http://doi.org/10.5880/GFZ.GRAVIS_06_L2B' + ) + ) # print list of references for ref in reference: fid.write(' - {0}\n'.format(ref)) @@ -784,19 +975,24 @@ def print_global(fid,PROC,DREL,MODEL,SLR,S21,month): fid.write(' {0:22}: {1}\n'.format('creator_url', url)) fid.write(' {0:22}: {1}\n'.format('creator_type', 'group')) inst = 'University of Washington; University of California, Irvine' - fid.write(' {0:22}: {1}\n'.format('creator_institution',inst)) + fid.write(' {0:22}: {1}\n'.format('creator_institution', inst)) # date range and date created - calendar_year,calendar_month = gravtk.time.grace_to_calendar(month) - start_time = '{0:4.0f}-{1:02.0f}'.format(calendar_year[0],calendar_month[0]) + calendar_year, calendar_month = gravtk.time.grace_to_calendar(month) + start_time = '{0:4.0f}-{1:02.0f}'.format( + calendar_year[0], calendar_month[0] + ) fid.write(' {0:22}: {1}\n'.format('time_coverage_start', start_time)) - end_time = '{0:4.0f}-{1:02.0f}'.format(calendar_year[-1],calendar_month[-1]) + end_time = '{0:4.0f}-{1:02.0f}'.format( + calendar_year[-1], calendar_month[-1] + ) fid.write(' {0:22}: {1}\n'.format('time_coverage_end', end_time)) - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) fid.write(' {0:22}: {1}\n'.format('date_created', today)) fid.write('\n') + # PURPOSE: print variable descriptions to YAML header -def print_variables(fid,data_precision,data_units): +def print_variables(fid, data_precision, data_units): # variables fid.write(' {0}:\n'.format('variables')) # time @@ -839,10 +1035,11 @@ def print_variables(fid,data_precision,data_units): # end of header fid.write('\n\n# End of YAML header\n') + # PURPOSE: print a file log for the GRACE degree one analysis def output_log_file(input_arguments, output_files, n_iter): # format: delta_degree_one_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'delta_degree_one_run_{0}_PID-{1:d}.log'.format(*args) DIRECTORY = pathlib.Path(input_arguments.directory).joinpath('geocenter') # create a unique log and open the log file @@ -862,10 +1059,11 @@ def output_log_file(input_arguments, output_files, n_iter): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE degree one analysis def output_error_log_file(input_arguments): # format: delta_degree_one_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'delta_degree_one_failed_run_{0}_PID-{1:d}.log'.format(*args) DIRECTORY = pathlib.Path(input_arguments.directory).joinpath('geocenter') # create a unique log and open the log file @@ -881,6 +1079,7 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -888,145 +1087,298 @@ def arguments(): coefficients of degree 2 and greater, and ocean bottom pressure variations from OMCT/MPIOM """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') - parser.add_argument('--kl','-k', - type=float, default=0.021, - help='Degree 1 gravitational Load Love number') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) + parser.add_argument( + '--kl', + '-k', + type=float, + default=0.021, + help='Degree 1 gravitational Load Love number', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # correct for pole tide drift follow Wahr et al. (2015) - parser.add_argument('--pole-tide', - default=False, action='store_true', - help='Correct for pole tide drift') + parser.add_argument( + '--pole-tide', + default=False, + action='store_true', + help='Correct for pole tide drift', + ) # replace low degree harmonics with values from Satellite Laser Ranging - parser.add_argument('--slr-c20', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C20 coefficients with SLR values') - parser.add_argument('--slr-21', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C21 and S21 coefficients with SLR values') - parser.add_argument('--slr-22', - type=str, default=None, choices=['CSR','GSFC'], - help='Replace C22 and S22 coefficients with SLR values') - parser.add_argument('--slr-c30', - type=str, default=None, choices=['CSR','GFZ','GSFC','LARES'], - help='Replace C30 coefficients with SLR values') - parser.add_argument('--slr-c40', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C40 coefficients with SLR values') - parser.add_argument('--slr-c50', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C50 coefficients with SLR values') + parser.add_argument( + '--slr-c20', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C20 coefficients with SLR values', + ) + parser.add_argument( + '--slr-21', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C21 and S21 coefficients with SLR values', + ) + parser.add_argument( + '--slr-22', + type=str, + default=None, + choices=['CSR', 'GSFC'], + help='Replace C22 and S22 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c30', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC', 'LARES'], + help='Replace C30 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c40', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C40 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c50', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C50 coefficients with SLR values', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/Output data format for delta harmonics file') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/Output data format for delta harmonics file', + ) # mean file to remove - parser.add_argument('--mean-file', + parser.add_argument( + '--mean-file', type=pathlib.Path, - help='GRACE/GRACE-FO mean file to remove from the harmonic data') + help='GRACE/GRACE-FO mean file to remove from the harmonic data', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--mean-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5','gfc'], - help='Input data format for GRACE/GRACE-FO mean file') + parser.add_argument( + '--mean-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5', 'gfc'], + help='Input data format for GRACE/GRACE-FO mean file', + ) # run with iterative scheme - parser.add_argument('--iterative', - default=False, action='store_true', - help='Iterate degree one solutions') + parser.add_argument( + '--iterative', + default=False, + action='store_true', + help='Iterate degree one solutions', + ) # least squares solver - choices = ('inv','lstsq','gelsd', 'gelsy', 'gelss') - parser.add_argument('--solver','-s', - type=str, default='lstsq', choices=choices, - help='Least squares solver for degree one solutions') + choices = ('inv', 'lstsq', 'gelsd', 'gelsy', 'gelss') + parser.add_argument( + '--solver', + '-s', + type=str, + default='lstsq', + choices=choices, + help='Least squares solver for degree one solutions', + ) # run with sea level fingerprints - parser.add_argument('--fingerprint', - default=False, action='store_true', - help='Redistribute land-water flux using sea level fingerprints') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--fingerprint', + default=False, + action='store_true', + help='Redistribute land-water flux using sea level fingerprints', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # land-sea mask for calculating ocean mass and land water flux - land_mask_file = gravtk.utilities.get_data_path(['data','land_fcn_300km.nc']) - parser.add_argument('--mask', + land_mask_file = gravtk.utilities.get_data_path( + ['data', 'land_fcn_300km.nc'] + ) + parser.add_argument( + '--mask', type=pathlib.Path, default=land_mask_file, - help='Land-sea mask for calculating ocean mass and land water flux') + help='Land-sea mask for calculating ocean mass and land water flux', + ) # Output log file for each job in forms # delta_degree_one_run_2002-04-01_PID-00000.log # delta_degree_one_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -1036,7 +1388,7 @@ def main(): try: info(args) # run delta_degree_one algorithm with parameters - output_files,n_iter = delta_degree_one( + output_files, n_iter = delta_degree_one( args.directory, args.center, args.release, @@ -1065,18 +1417,20 @@ def main(): FINGERPRINT=args.fingerprint, EXPANSION=args.expansion, LANDMASK=args.mask, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files,n_iter) + if args.log: # write successful job completion log file + output_log_file(args, output_files, n_iter) + # run main program if __name__ == '__main__': diff --git a/geocenter/geocenter_compare_tellus.py b/geocenter/geocenter_compare_tellus.py index 7ed393cf..8a0961df 100644 --- a/geocenter/geocenter_compare_tellus.py +++ b/geocenter/geocenter_compare_tellus.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" geocenter_compare_tellus.py Written by Tyler Sutterley (01/2025) Plots the GRACE/GRACE-FO geocenter time series for different @@ -27,6 +27,7 @@ Updated 11/2021: use gravity_toolkit geocenter class for operations Written 05/2021 """ + from __future__ import print_function import pathlib @@ -34,175 +35,212 @@ import warnings import numpy as np import gravity_toolkit as gravtk + # attempt imports try: import matplotlib import matplotlib.font_manager import matplotlib.pyplot as plt import matplotlib.offsetbox + # rebuilt the matplotlib fonts and set parameters matplotlib.font_manager._load_fontmanager() matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) + # PURPOSE: plots the GRACE/GRACE-FO geocenter time series -def geocenter_compare_tellus(grace_dir,DREL,START_MON,END_MON,MISSING): +def geocenter_compare_tellus(grace_dir, DREL, START_MON, END_MON, MISSING): # GRACE months - GAP = [187,188,189,190,191,192,193,194,195,196,197] - months = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] + months = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) # labels for each scenario - input_flags = ['','iter','SLF_iter','SLF_iter_wSLR21'] - input_labels = ['Static','Iterated','Iterated SLF'] + input_flags = ['', 'iter', 'SLF_iter', 'SLF_iter_wSLR21'] + input_labels = ['Static', 'Iterated', 'Iterated SLF'] # labels for Release-6 - PROC = ['CSR','GFZ','JPL'] - model_str = 'OMCT' if DREL in ('RL04','RL05') else 'MPIOM' + PROC = ['CSR', 'GFZ', 'JPL'] + model_str = 'OMCT' if DREL in ('RL04', 'RL05') else 'MPIOM' # degree one coefficient labels - fig_labels = ['C11','S11','C10'] - axes_labels = dict(C10='c)',C11='a)',S11='b)') - ylabels = dict(C10='z',C11='x',S11='y') + fig_labels = ['C11', 'S11', 'C10'] + axes_labels = dict(C10='c)', C11='a)', S11='b)') + ylabels = dict(C10='z', C11='x', S11='y') # plot colors for each dataset - plot_colors = {'Iterated SLF':'darkorchid','GFZ GravIS':'darkorange', - 'JPL Tellus':'mediumseagreen'} + plot_colors = { + 'Iterated SLF': 'darkorchid', + 'GFZ GravIS': 'darkorange', + 'JPL Tellus': 'mediumseagreen', + } # plot geocenter estimates for each processing center - for k,pr in enumerate(PROC): + for k, pr in enumerate(PROC): # 3 row plot (C10, C11 and S11) ax = {} - fig,(ax[0],ax[1],ax[2])=plt.subplots(num=1,ncols=3, - sharey=True,figsize=(9,4)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, ncols=3, sharey=True, figsize=(9, 4) + ) # additionally plot GFZ with SLR replaced pole tide - if (pr == 'GFZwPT'): - fargs = ('GFZ',DREL,model_str,input_flags[3]) + if pr == 'GFZwPT': + fargs = ('GFZ', DREL, model_str, input_flags[3]) else: - fargs = (pr,DREL,model_str,input_flags[2]) + fargs = (pr, DREL, model_str, input_flags[2]) # read geocenter file for processing center and model grace_file = '{0}_{1}_{2}_{3}.txt'.format(*fargs) DEG1 = gravtk.geocenter().from_UCI(grace_dir.joinpath(grace_file)) # indices for mean months - kk, = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) + (kk,) = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) DEG1.mean(apply=True, indices=kk) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # plot model outputs # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) val = getattr(DEG1, ylabels[key].upper()) - for i,m in enumerate(months): + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] data[i] = val[mm] # plot all dates - ax[j].plot(tdec, data, color=plot_colors['Iterated SLF'], - label='Iterated SLF') + ax[j].plot( + tdec, + data, + color=plot_colors['Iterated SLF'], + label='Iterated SLF', + ) - if (pr == 'GFZwPT'): + if pr == 'GFZwPT': grace_file = 'GRAVIS-2B_GFZOP_GEOCENTER_0002.dat' - DEG1 = gravtk.geocenter().from_gravis(grace_dir.joinpath(grace_file)) + DEG1 = gravtk.geocenter().from_gravis( + grace_dir.joinpath(grace_file) + ) # indices for mean months - kk, = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) + (kk,) = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) DEG1.mean(apply=True, indices=kk) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # plot model outputs val = getattr(DEG1, ylabels[key].upper()) val -= val[kk].mean() # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) - for i,m in enumerate(months): + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] data[i] = val[mm] # plot all dates - ax[j].plot(tdec, data, color=plot_colors['GFZ GravIS'], - label='GFZ GravIS') + ax[j].plot( + tdec, + data, + color=plot_colors['GFZ GravIS'], + label='GFZ GravIS', + ) # Running function read_tellus_geocenter.py grace_file = f'TN-13_GEOC_{pr}_{DREL}.txt' - DEG1 = gravtk.geocenter().from_tellus(grace_dir.joinpath(grace_file), - JPL=True) + DEG1 = gravtk.geocenter().from_tellus( + grace_dir.joinpath(grace_file), JPL=True + ) # indices for mean months - kk, = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) + (kk,) = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) DEG1.mean(apply=True, indices=kk) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # plot model outputs # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) val = getattr(DEG1, ylabels[key].upper()) - for i,m in enumerate(months): + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] data[i] = val[mm] # plot all dates - ax[j].plot(tdec, data, color=plot_colors['JPL Tellus'], - label='JPL Tellus') + ax[j].plot( + tdec, data, color=plot_colors['JPL Tellus'], label='JPL Tellus' + ) # add axis labels and adjust font sizes for axis ticks - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9,day=3,hour=12,minute=12) - ax[j].axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax[j].axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(8, 4)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(DEG1.month == 186) - kk, = np.flatnonzero(DEG1.month == 198) - vs = ax[j].axvspan(DEG1.time[jj],DEG1.time[kk], - color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (4,2) + (jj,) = np.flatnonzero(DEG1.month == 186) + (kk,) = np.flatnonzero(DEG1.month == 198) + vs = ax[j].axvspan( + DEG1.time[jj], + DEG1.time[kk], + color='0.5', + ls='dashed', + alpha=0.15, + ) + vs._dashes = (4, 2) # axis label ax[j].set_title(ylabels[key], style='italic', fontsize=14) - artist = matplotlib.offsetbox.AnchoredText(axes_labels[key], pad=0., - prop=dict(size=16, weight='bold'), frameon=False, loc=2) + artist = matplotlib.offsetbox.AnchoredText( + axes_labels[key], + pad=0.0, + prop=dict(size=16, weight='bold'), + frameon=False, + loc=2, + ) ax[j].add_artist(artist) ax[j].set_xlabel('Time [Yr]', fontsize=14) # set ticks - xmin = 2002 + (START_MON + 1.0)//12.0 - xmax = 2002 + (END_MON + 1.0)/12.0 + xmin = 2002 + (START_MON + 1.0) // 12.0 + xmax = 2002 + (END_MON + 1.0) / 12.0 major_ticks = np.arange(2005, xmax, 5) ax[j].xaxis.set_ticks(major_ticks) - minor_ticks = sorted(set(np.arange(xmin, xmax, 1)) - set(major_ticks)) + minor_ticks = sorted( + set(np.arange(xmin, xmax, 1)) - set(major_ticks) + ) ax[j].xaxis.set_ticks(minor_ticks, minor=True) ax[j].set_xlim(xmin, xmax) - ax[j].set_ylim(-9.5,8.5) + ax[j].set_ylim(-9.5, 8.5) # axes tick adjustments - ax[j].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[j].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # add legend - lgd = ax[0].legend(loc=3,frameon=False) + lgd = ax[0].legend(loc=3, frameon=False) lgd.get_frame().set_alpha(1.0) for line in lgd.get_lines(): line.set_linewidth(6) - for i,text in enumerate(lgd.get_texts()): + for i, text in enumerate(lgd.get_texts()): text.set_weight('bold') text.set_color(plot_colors[text.get_text()]) # labels and set limits ax[0].set_ylabel('Geocenter Variation [mm]', fontsize=14) # adjust locations of subplots - fig.subplots_adjust(left=0.06,right=0.98,bottom=0.12,top=0.94,wspace=0.05) + fig.subplots_adjust( + left=0.06, right=0.98, bottom=0.12, top=0.94, wspace=0.05 + ) # save figure to file OUTPUT_FIGURE = f'TN13_SV19_{pr}_{DREL}.pdf' plt.savefig(grace_dir.joinpath(OUTPUT_FIGURE), format='pdf', dpi=300) plt.clf() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -212,39 +250,86 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, - default='RL06', choices=['RL04','RL05','RL06'], - help='GRACE/GRACE-FO data release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + choices=['RL04', 'RL05', 'RL06'], + help='GRACE/GRACE-FO data release', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series') - parser.add_argument('--end','-E', - type=int, default=231, - help='Ending GRACE/GRACE-FO month for time series') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167,172, - 177,178,182,200,201] - parser.add_argument('--missing','-M', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months in time series') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=231, + help='Ending GRACE/GRACE-FO month for time series', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-M', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months in time series', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run program with parameters - geocenter_compare_tellus(args.directory, args.release, - args.start, args.end, args.missing) + geocenter_compare_tellus( + args.directory, args.release, args.start, args.end, args.missing + ) + # run main program if __name__ == '__main__': diff --git a/geocenter/geocenter_monte_carlo.py b/geocenter/geocenter_monte_carlo.py index 959ec27a..c5721c0e 100644 --- a/geocenter/geocenter_monte_carlo.py +++ b/geocenter/geocenter_monte_carlo.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" geocenter_monte_carlo.py Written by Tyler Sutterley (01/2025) @@ -24,6 +24,7 @@ Updated 12/2021: adjust minimum x limit based on starting GRACE month Written 11/2021 """ + from __future__ import print_function import pathlib @@ -39,22 +40,24 @@ import matplotlib.pyplot as plt import matplotlib.cm as cm import matplotlib.offsetbox + # rebuilt the matplotlib fonts and set parameters matplotlib.font_manager._load_fontmanager() matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) + # PURPOSE: plots the GRACE/GRACE-FO geocenter time series -def geocenter_monte_carlo(grace_dir,PROC,DREL,START_MON,END_MON,MISSING): +def geocenter_monte_carlo(grace_dir, PROC, DREL, START_MON, END_MON, MISSING): # GRACE months - GAP = [187,188,189,190,191,192,193,194,195,196,197] - months = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] + months = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) nmon = len(months) # labels for Release-6 - model_str = 'OMCT' if DREL in ('RL04','RL05') else 'MPIOM' + model_str = 'OMCT' if DREL in ('RL04', 'RL05') else 'MPIOM' # GIA and processing labels input_flag = 'SLF' gia_str = '_AW13_ice6g_GA' @@ -62,38 +65,40 @@ def geocenter_monte_carlo(grace_dir,PROC,DREL,START_MON,END_MON,MISSING): ds_str = '_FL' # degree one coefficient labels - fig_labels = ['C11','S11','C10'] - axes_labels = dict(C10='c)',C11='a)',S11='b)') - ylabels = dict(C10='z',C11='x',S11='y') + fig_labels = ['C11', 'S11', 'C10'] + axes_labels = dict(C10='c)', C11='a)', S11='b)') + ylabels = dict(C10='z', C11='x', S11='y') # 3 row plot (C10, C11 and S11) ax = {} - fig,(ax[0],ax[1],ax[2])=plt.subplots(num=1,ncols=3,sharey=True,figsize=(9,4)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, ncols=3, sharey=True, figsize=(9, 4) + ) # read geocenter file for processing center and model - fargs = (PROC,DREL,model_str,input_flag,gia_str,delta_str,ds_str) + fargs = (PROC, DREL, model_str, input_flag, gia_str, delta_str, ds_str) grace_file = '{0}_{1}_{2}_{3}{4}{5}{6}.nc'.format(*fargs) DEG1 = gravtk.geocenter().from_netCDF4(grace_dir.joinpath(grace_file)) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # number of monte carlo runs - _,nruns = np.shape(DEG1.C10) + _, nruns = np.shape(DEG1.C10) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # create a time series with nans for missing months - tdec = np.full((nmon),np.nan,dtype=np.float64) - data = np.full((nmon,nruns),np.nan,dtype=np.float64) + tdec = np.full((nmon), np.nan, dtype=np.float64) + data = np.full((nmon, nruns), np.nan, dtype=np.float64) val = getattr(DEG1, ylabels[key].upper()) - for i,m in enumerate(months): + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] - data[i,:] = val[mm,:] + data[i, :] = val[mm, :] # show solutions for each iteration - plot_colors = iter(cm.rainbow(np.linspace(0,1,nruns))) + plot_colors = iter(cm.rainbow(np.linspace(0, 1, nruns))) # mean of all monte carlo solutions MEAN = np.mean(data, axis=1) nvalid = np.count_nonzero(np.isfinite(MEAN)) @@ -103,58 +108,71 @@ def geocenter_monte_carlo(grace_dir,PROC,DREL,START_MON,END_MON,MISSING): for k in range(nruns): color_k = next(plot_colors) # plot all dates - ax[j].plot(tdec, data[:,k], color=color_k) + ax[j].plot(tdec, data[:, k], color=color_k) # variance off of the mean - variance[k] = np.nansum((data[:,k] - MEAN)**2)/nvalid - if (np.nanmax(np.abs(data[:,k] - MEAN)) > max_var): - max_var = np.nanmax(np.abs(data[:,k] - MEAN)) + variance[k] = np.nansum((data[:, k] - MEAN) ** 2) / nvalid + if np.nanmax(np.abs(data[:, k] - MEAN)) > max_var: + max_var = np.nanmax(np.abs(data[:, k] - MEAN)) # add mean solution ax[j].plot(tdec, MEAN, color='k', lw=1) # calculate total RMS - RMS = np.nansum(np.sqrt(variance))/nruns + RMS = np.nansum(np.sqrt(variance)) / nruns # add axis labels and adjust font sizes for axis ticks # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9,day=3,hour=12,minute=12) - ax[j].axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax[j].axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(8, 4)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(DEG1.month == 186) - kk, = np.flatnonzero(DEG1.month == 198) - vs = ax[j].axvspan(DEG1.time[jj],DEG1.time[kk], - color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (4,2) + (jj,) = np.flatnonzero(DEG1.month == 186) + (kk,) = np.flatnonzero(DEG1.month == 198) + vs = ax[j].axvspan( + DEG1.time[jj], DEG1.time[kk], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (4, 2) # axis label ax[j].set_title(ylabels[key], style='italic', fontsize=14) - artist = matplotlib.offsetbox.AnchoredText(axes_labels[key], pad=0., - prop=dict(size=16,weight='bold'), frameon=False, loc=2) + artist = matplotlib.offsetbox.AnchoredText( + axes_labels[key], + pad=0.0, + prop=dict(size=16, weight='bold'), + frameon=False, + loc=2, + ) ax[j].add_artist(artist) lbl = r'$\sigma$' + f' = {RMS:0.2f} mm\nmax = {max_var:0.2f} mm' - artist = matplotlib.offsetbox.AnchoredText(lbl, pad=0., - prop=dict(size=12), frameon=False, loc=3) + artist = matplotlib.offsetbox.AnchoredText( + lbl, pad=0.0, prop=dict(size=12), frameon=False, loc=3 + ) ax[j].add_artist(artist) ax[j].set_xlabel('Time [Yr]', fontsize=14) # set ticks - xmin = 2002 + (START_MON + 1.0)//12.0 - xmax = 2002 + (END_MON + 1.0)/12.0 + xmin = 2002 + (START_MON + 1.0) // 12.0 + xmax = 2002 + (END_MON + 1.0) / 12.0 major_ticks = np.arange(2005, xmax, 5) ax[j].xaxis.set_ticks(major_ticks) minor_ticks = sorted(set(np.arange(xmin, xmax, 1)) - set(major_ticks)) ax[j].xaxis.set_ticks(minor_ticks, minor=True) ax[j].set_xlim(xmin, xmax) - ax[j].set_ylim(-9.5,8.5) + ax[j].set_ylim(-9.5, 8.5) # axes tick adjustments - ax[j].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[j].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # labels and set limits ax[0].set_ylabel(f'{PROC} Geocenter Variation [mm]', fontsize=14) # adjust locations of subplots - fig.subplots_adjust(left=0.06,right=0.98,bottom=0.12,top=0.94,wspace=0.05) + fig.subplots_adjust( + left=0.06, right=0.98, bottom=0.12, top=0.94, wspace=0.05 + ) # save figure to file OUTPUT_FIGURE = f'SV19_{PROC}_{DREL}_monte_carlo.pdf' plt.savefig(grace_dir.joinpath(OUTPUT_FIGURE), format='pdf', dpi=300) plt.clf() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -163,43 +181,100 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, - default='RL06', choices=['RL04','RL05','RL06'], - help='GRACE/GRACE-FO data release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + choices=['RL04', 'RL05', 'RL06'], + help='GRACE/GRACE-FO data release', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series') - parser.add_argument('--end','-E', - type=int, default=236, - help='Ending GRACE/GRACE-FO month for time series') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167,172, - 177,178,182,200,201] - parser.add_argument('--missing','-M', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months in time series') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=236, + help='Ending GRACE/GRACE-FO month for time series', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-M', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months in time series', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run program with parameters - geocenter_monte_carlo(args.directory, args.center, args.release, - args.start, args.end, args.missing) + geocenter_monte_carlo( + args.directory, + args.center, + args.release, + args.start, + args.end, + args.missing, + ) + # run main program if __name__ == '__main__': diff --git a/geocenter/geocenter_ocean_models.py b/geocenter/geocenter_ocean_models.py index de676f84..c4bcc71d 100644 --- a/geocenter/geocenter_ocean_models.py +++ b/geocenter/geocenter_ocean_models.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" geocenter_ocean_models.py Written by Tyler Sutterley (01/2025) Plots the GRACE/GRACE-FO geocenter time series comparing results @@ -34,6 +34,7 @@ Updated 11/2019: adjust axes and set directory to full path Updated 09/2019: for public release of time series to references page """ + from __future__ import print_function import pathlib @@ -48,107 +49,133 @@ import matplotlib.font_manager import matplotlib.pyplot as plt import matplotlib.offsetbox + # rebuilt the matplotlib fonts and set parameters matplotlib.font_manager._load_fontmanager() matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) + # PURPOSE: plots the GRACE/GRACE-FO geocenter time series # comparing results using different ocean bottom pressure estimates -def geocenter_ocean_models(grace_dir,PROC,DREL,MODEL,START_MON,END_MON,MISSING): +def geocenter_ocean_models( + grace_dir, PROC, DREL, MODEL, START_MON, END_MON, MISSING +): # GRACE months - GAP = [187,188,189,190,191,192,193,194,195,196,197] - months = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] + months = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) # labels for each scenario - input_flags = ['','iter','SLF_iter'] - input_labels = ['Static','Iterated','Iterated SLF'] + input_flags = ['', 'iter', 'SLF_iter'] + input_labels = ['Static', 'Iterated', 'Iterated SLF'] # degree one coefficient labels - fig_labels = ['C11','S11','C10'] - axes_labels = dict(C10='c)',C11='a)',S11='b)') - ylabels = dict(C10='z',C11='x',S11='y') + fig_labels = ['C11', 'S11', 'C10'] + axes_labels = dict(C10='c)', C11='a)', S11='b)') + ylabels = dict(C10='z', C11='x', S11='y') # list of plot colors - plot_colors = ['darkorange','darkorchid','mediumseagreen','dodgerblue','0.4'] + plot_colors = [ + 'darkorange', + 'darkorchid', + 'mediumseagreen', + 'dodgerblue', + '0.4', + ] # 3 row plot (C10, C11 and S11) ax = {} - fig,(ax[0],ax[1],ax[2])=plt.subplots(num=1,ncols=3,sharey=True,figsize=(9,4)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, ncols=3, sharey=True, figsize=(9, 4) + ) # plot geocenter estimates for each processing center - for k,mdl in enumerate(MODEL): + for k, mdl in enumerate(MODEL): # read geocenter file for processing center and model - grace_file = '{0}_{1}_{2}_{3}.txt'.format(PROC,DREL,mdl,input_flags[2]) + grace_file = '{0}_{1}_{2}_{3}.txt'.format( + PROC, DREL, mdl, input_flags[2] + ) DEG1 = gravtk.geocenter().from_UCI(grace_dir.joinpath(grace_file)) # indices for mean months - kk, = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) + (kk,) = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) DEG1.mean(apply=True, indices=kk) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) val = getattr(DEG1, ylabels[key].upper()) - for i,m in enumerate(months): + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] data[i] = val[mm] # plot all dates - label = mdl.replace('_','-') + label = mdl.replace('_', '-') ax[j].plot(tdec, data, color=plot_colors[k], label=label) # read geocenter file for processing center and model - model_str = 'OMCT' if DREL in ('RL04','RL05') else 'MPIOM' - grace_file = '{0}_{1}_{2}_{3}.txt'.format(PROC,DREL,model_str,input_flags[2]) + model_str = 'OMCT' if DREL in ('RL04', 'RL05') else 'MPIOM' + grace_file = '{0}_{1}_{2}_{3}.txt'.format( + PROC, DREL, model_str, input_flags[2] + ) DEG1 = gravtk.geocenter().from_UCI(grace_dir.joinpath(grace_file)) # add axis labels and adjust font sizes for axis ticks - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(DEG1.month == 186) - kk, = np.flatnonzero(DEG1.month == 198) - ax[j].axvspan(DEG1.time[jj],DEG1.time[kk], - color='0.5',ls='dashed',alpha=0.15) + (jj,) = np.flatnonzero(DEG1.month == 186) + (kk,) = np.flatnonzero(DEG1.month == 198) + ax[j].axvspan( + DEG1.time[jj], DEG1.time[kk], color='0.5', ls='dashed', alpha=0.15 + ) # axis label ax[j].set_title(ylabels[key], style='italic', fontsize=14) - artist = matplotlib.offsetbox.AnchoredText(axes_labels[key], pad=0., - prop=dict(size=16,weight='bold'), frameon=False, loc=2) + artist = matplotlib.offsetbox.AnchoredText( + axes_labels[key], + pad=0.0, + prop=dict(size=16, weight='bold'), + frameon=False, + loc=2, + ) ax[j].add_artist(artist) ax[j].set_xlabel('Time [Yr]', fontsize=14) # set ticks - xmin = 2002 + (START_MON + 1.0)//12.0 - xmax = 2002 + (END_MON + 1.0)/12.0 + xmin = 2002 + (START_MON + 1.0) // 12.0 + xmax = 2002 + (END_MON + 1.0) / 12.0 major_ticks = np.arange(2005, xmax, 5) ax[j].xaxis.set_ticks(major_ticks) minor_ticks = sorted(set(np.arange(xmin, xmax, 1)) - set(major_ticks)) ax[j].xaxis.set_ticks(minor_ticks, minor=True) ax[j].set_xlim(xmin, xmax) - ax[j].set_ylim(-9.5,8.5) + ax[j].set_ylim(-9.5, 8.5) # axes tick adjustments - ax[j].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[j].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # add legend - lgd = ax[0].legend(loc=3,frameon=False) + lgd = ax[0].legend(loc=3, frameon=False) lgd.get_frame().set_alpha(1.0) for line in lgd.get_lines(): line.set_linewidth(6) - for i,text in enumerate(lgd.get_texts()): + for i, text in enumerate(lgd.get_texts()): text.set_weight('bold') text.set_color(plot_colors[i]) # labels and set limits ax[0].set_ylabel('Geocenter Variation [mm]', fontsize=14) # adjust locations of subplots - fig.subplots_adjust(left=0.06,right=0.98,bottom=0.12,top=0.94,wspace=0.05) + fig.subplots_adjust( + left=0.06, right=0.98, bottom=0.12, top=0.94, wspace=0.05 + ) # save figure to file OUTPUT_FIGURE = f'SV19_{PROC}_{DREL}_ocean_models.pdf' plt.savefig(grace_dir.joinpath(OUTPUT_FIGURE), format='pdf', dpi=300) plt.clf() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -157,48 +184,111 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, nargs='+', - default=['CSR','GFZ','JPL'], choices=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO processing center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + nargs='+', + default=['CSR', 'GFZ', 'JPL'], + choices=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO processing center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, - default='RL06', choices=['RL04','RL05','RL06'], - help='GRACE/GRACE-FO data release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + choices=['RL04', 'RL05', 'RL06'], + help='GRACE/GRACE-FO data release', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series') - parser.add_argument('--end','-E', - type=int, default=227, - help='Ending GRACE/GRACE-FO month for time series') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167,172, - 177,178,182,200,201] - parser.add_argument('--missing','-M', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months in time series') - parser.add_argument('--ocean','-O', - type=str, nargs='+', - help='Ocean bottom pressure products to use') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=227, + help='Ending GRACE/GRACE-FO month for time series', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-M', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months in time series', + ) + parser.add_argument( + '--ocean', + '-O', + type=str, + nargs='+', + help='Ocean bottom pressure products to use', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run program with parameters for PROC in args.center: - geocenter_ocean_models(args.directory, PROC, args.release, - args.ocean, args.start, args.end, args.missing) + geocenter_ocean_models( + args.directory, + PROC, + args.release, + args.ocean, + args.start, + args.end, + args.missing, + ) + # run main program if __name__ == '__main__': diff --git a/geocenter/geocenter_processing_centers.py b/geocenter/geocenter_processing_centers.py index 9a9b2ef6..950acdce 100644 --- a/geocenter/geocenter_processing_centers.py +++ b/geocenter/geocenter_processing_centers.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" geocenter_processing_centers.py Written by Tyler Sutterley (01/2025) Plots the GRACE/GRACE-FO geocenter time series for different @@ -35,6 +35,7 @@ Updated 11/2019: adjust axes and set directory to full path Updated 09/2019: for public release of time series to references page """ + from __future__ import print_function import pathlib @@ -49,117 +50,141 @@ import matplotlib.font_manager import matplotlib.pyplot as plt import matplotlib.offsetbox + # rebuilt the matplotlib fonts and set parameters matplotlib.font_manager._load_fontmanager() matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) + # PURPOSE: plots the GRACE/GRACE-FO geocenter time series -def geocenter_processing_centers(grace_dir,PROC,DREL,START_MON,END_MON,MISSING): +def geocenter_processing_centers( + grace_dir, PROC, DREL, START_MON, END_MON, MISSING +): # GRACE months - GAP = [187,188,189,190,191,192,193,194,195,196,197] - months = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] + months = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) # labels for each scenario - input_flags = ['','iter','SLF_iter','SLF_iter_wSLR21','SLF_iter_wSLR21_wSLR22'] - input_labels = ['Static','Iterated','Iterated SLF'] + input_flags = [ + '', + 'iter', + 'SLF_iter', + 'SLF_iter_wSLR21', + 'SLF_iter_wSLR21_wSLR22', + ] + input_labels = ['Static', 'Iterated', 'Iterated SLF'] # labels for Release-6 - model_str = 'OMCT' if DREL in ('RL04','RL05') else 'MPIOM' + model_str = 'OMCT' if DREL in ('RL04', 'RL05') else 'MPIOM' # degree one coefficient labels - fig_labels = ['C11','S11','C10'] - axes_labels = dict(C10='c)',C11='a)',S11='b)') - ylabels = dict(C10='z',C11='x',S11='y') + fig_labels = ['C11', 'S11', 'C10'] + axes_labels = dict(C10='c)', C11='a)', S11='b)') + ylabels = dict(C10='z', C11='x', S11='y') # plot colors for each dataset - plot_colors = dict(CSR='darkorange',GFZ='darkorchid',JPL='mediumseagreen') + plot_colors = dict(CSR='darkorange', GFZ='darkorchid', JPL='mediumseagreen') plot_colors['GFZwPT'] = 'dodgerblue' plot_colors['GFZ+CS21'] = 'darkorchid' plot_colors['GFZ+CS21+CS22'] = 'darkorchid' # 3 row plot (C10, C11 and S11) ax = {} - fig,(ax[0],ax[1],ax[2])=plt.subplots(num=1,ncols=3,sharey=True,figsize=(9,4)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, ncols=3, sharey=True, figsize=(9, 4) + ) # plot geocenter estimates for each processing center - for k,pr in enumerate(PROC): + for k, pr in enumerate(PROC): # additionally plot GFZ with SLR replaced pole tide - if pr in ('GFZwPT','GFZ+CS21'): - fargs = ('GFZ',DREL,model_str,input_flags[3]) - elif (pr == 'GFZ+CS21+CS22'): - fargs = ('GFZ',DREL,model_str,input_flags[4]) + if pr in ('GFZwPT', 'GFZ+CS21'): + fargs = ('GFZ', DREL, model_str, input_flags[3]) + elif pr == 'GFZ+CS21+CS22': + fargs = ('GFZ', DREL, model_str, input_flags[4]) else: - fargs = (pr,DREL,model_str,input_flags[2]) + fargs = (pr, DREL, model_str, input_flags[2]) # read geocenter file for processing center and model grace_file = '{0}_{1}_{2}_{3}.txt'.format(*fargs) DEG1 = gravtk.geocenter().from_UCI(grace_dir.joinpath(grace_file)) # indices for mean months - kk, = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) + (kk,) = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) DEG1.mean(apply=True, indices=kk) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) val = getattr(DEG1, ylabels[key].upper()) - for i,m in enumerate(months): + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] data[i] = val[mm] # plot all dates ax[j].plot(tdec, data, color=plot_colors[pr], label=pr) # add axis labels and adjust font sizes for axis ticks - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9,day=3,hour=12,minute=12) - ax[j].axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax[j].axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(8, 4)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(DEG1.month == 186) - kk, = np.flatnonzero(DEG1.month == 198) - vs = ax[j].axvspan(DEG1.time[jj],DEG1.time[kk], - color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (4,2) + (jj,) = np.flatnonzero(DEG1.month == 186) + (kk,) = np.flatnonzero(DEG1.month == 198) + vs = ax[j].axvspan( + DEG1.time[jj], DEG1.time[kk], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (4, 2) # axis label ax[j].set_title(ylabels[key], style='italic', fontsize=14) - artist = matplotlib.offsetbox.AnchoredText(axes_labels[key], pad=0., - prop=dict(size=16,weight='bold'), frameon=False, loc=2) + artist = matplotlib.offsetbox.AnchoredText( + axes_labels[key], + pad=0.0, + prop=dict(size=16, weight='bold'), + frameon=False, + loc=2, + ) ax[j].add_artist(artist) ax[j].set_xlabel('Time [Yr]', fontsize=14) # set ticks - xmin = 2002 + (START_MON + 1.0)//12.0 - xmax = 2002 + (END_MON + 1.0)/12.0 + xmin = 2002 + (START_MON + 1.0) // 12.0 + xmax = 2002 + (END_MON + 1.0) / 12.0 major_ticks = np.arange(2005, xmax, 5) ax[j].xaxis.set_ticks(major_ticks) minor_ticks = sorted(set(np.arange(xmin, xmax, 1)) - set(major_ticks)) ax[j].xaxis.set_ticks(minor_ticks, minor=True) ax[j].set_xlim(xmin, xmax) - ax[j].set_ylim(-9.5,8.5) + ax[j].set_ylim(-9.5, 8.5) # axes tick adjustments - ax[j].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[j].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # add legend - lgd = ax[0].legend(loc=3,frameon=False) + lgd = ax[0].legend(loc=3, frameon=False) lgd.get_frame().set_alpha(1.0) for line in lgd.get_lines(): line.set_linewidth(6) - for i,text in enumerate(lgd.get_texts()): + for i, text in enumerate(lgd.get_texts()): text.set_weight('bold') text.set_color(plot_colors[text.get_text()]) # labels and set limits ax[0].set_ylabel('Geocenter Variation [mm]', fontsize=14) # adjust locations of subplots - fig.subplots_adjust(left=0.06,right=0.98,bottom=0.12,top=0.94,wspace=0.05) + fig.subplots_adjust( + left=0.06, right=0.98, bottom=0.12, top=0.94, wspace=0.05 + ) # save figure to file OUTPUT_FIGURE = f'SV19_{DREL}_centers.pdf' plt.savefig(grace_dir.joinpath(OUTPUT_FIGURE), format='pdf', dpi=300) plt.clf() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -168,44 +193,102 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Data processing center or satellite mission - PROC = ['CSR','GFZ','GFZwPT','JPL'] - parser.add_argument('--center','-c', - metavar='PROC', type=str, nargs='+', default=PROC, - help='GRACE/GRACE-FO Processing Center') + PROC = ['CSR', 'GFZ', 'GFZwPT', 'JPL'] + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + nargs='+', + default=PROC, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, - default='RL06', choices=['RL04','RL05','RL06'], - help='GRACE/GRACE-FO data release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + choices=['RL04', 'RL05', 'RL06'], + help='GRACE/GRACE-FO data release', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series') - parser.add_argument('--end','-E', - type=int, default=230, - help='Ending GRACE/GRACE-FO month for time series') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167,172, - 177,178,182,200,201] - parser.add_argument('--missing','-M', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months in time series') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=230, + help='Ending GRACE/GRACE-FO month for time series', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-M', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months in time series', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run program with parameters - geocenter_processing_centers(args.directory, args.center, args.release, - args.start, args.end, args.missing) + geocenter_processing_centers( + args.directory, + args.center, + args.release, + args.start, + args.end, + args.missing, + ) + # run main program if __name__ == '__main__': diff --git a/geocenter/geocenter_spatial_maps.py b/geocenter/geocenter_spatial_maps.py index ce6b5f55..611a9e80 100644 --- a/geocenter/geocenter_spatial_maps.py +++ b/geocenter/geocenter_spatial_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" geocenter_spatial_maps.py Written by Tyler Sutterley (07/2026) @@ -118,6 +118,7 @@ include 161-day S2 tidal aliasing terms in regression Written 10/2018 """ + from __future__ import print_function import sys @@ -131,6 +132,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -140,9 +142,13 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: import GRACE/GRACE-FO geocenter files for a given months range # Converts the GRACE/GRACE-FO harmonics applying the specified procedures -def geocenter_spatial_maps(base_dir, PROC, DREL, +def geocenter_spatial_maps( + base_dir, + PROC, + DREL, START=None, END=None, MISSING=None, @@ -161,8 +167,8 @@ def geocenter_spatial_maps(base_dir, PROC, DREL, SLR_C50=None, DATAFORM=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # input directory setup base_dir = pathlib.Path(base_dir).expanduser().absolute() grace_dir = base_dir.joinpath('geocenter') @@ -178,10 +184,10 @@ def geocenter_spatial_maps(base_dir, PROC, DREL, suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # GRACE months - GAP = [187,188,189,190,191,192,193,194,195,196,197] - months = sorted(set(np.arange(START,END+1)) - set(MISSING)) + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] + months = sorted(set(np.arange(START, END + 1)) - set(MISSING)) # labels for Release-6 - model_str = 'OMCT' if DREL in ('RL04','RL05') else 'MPIOM' + model_str = 'OMCT' if DREL in ('RL04', 'RL05') else 'MPIOM' # labels for each scenario FLAGS = [] # FLAGS.append('') @@ -191,37 +197,38 @@ def geocenter_spatial_maps(base_dir, PROC, DREL, gia_str = '_AW13_ice6g_GA' ds_str = '_FL' if DESTRIPE else '' # output flag for low-degree harmonic replacements - if SLR_21 in ('CSR','GFZ','GSFC'): + if SLR_21 in ('CSR', 'GFZ', 'GSFC'): C21_str = f'_w{SLR_21}_21' else: C21_str = '' - if SLR_22 in ('CSR','GSFC'): + if SLR_22 in ('CSR', 'GSFC'): C22_str = f'_w{SLR_22}_22' else: C22_str = '' if SLR_C30 in ('GSFC',): # C30 replacement now default for all solutions C30_str = '' - elif SLR_C30 in ('CSR','GFZ','LARES'): + elif SLR_C30 in ('CSR', 'GFZ', 'LARES'): C30_str = f'_w{SLR_C30}_C30' else: C30_str = '' - if SLR_C40 in ('CSR','GSFC','LARES'): + if SLR_C40 in ('CSR', 'GSFC', 'LARES'): C40_str = f'_w{SLR_C40}_C40' else: C40_str = '' - if SLR_C50 in ('CSR','GSFC','LARES'): + if SLR_C50 in ('CSR', 'GSFC', 'LARES'): C50_str = f'_w{SLR_C50}_C50' else: C50_str = '' # combine satellite laser ranging flags - slr_str = ''.join([C21_str,C22_str,C30_str,C40_str,C50_str]) + slr_str = ''.join([C21_str, C22_str, C30_str, C40_str, C50_str]) # degree one coefficient labels - coef_labels = ['C10','C11','S11'] + coef_labels = ['C10', 'C11', 'S11'] # read arrays of kl, hl, and ll Love Numbers - hl,kl,ll = gravtk.load_love_numbers(1, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE) + hl, kl, ll = gravtk.load_love_numbers( + 1, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE + ) # set gravitational load love number to a specific value if LOVE_K1: kl[1] = np.copy(LOVE_K1) @@ -229,10 +236,12 @@ def geocenter_spatial_maps(base_dir, PROC, DREL, # mmwe, millimeters water equivalent unit_label = 'mmwe' unit_name = 'Equivalent_Water_Thickness' - dfactor = gravtk.units(lmax=1).harmonic(hl,kl,ll).mmwe + dfactor = gravtk.units(lmax=1).harmonic(hl, kl, ll).mmwe # attributes for output files attributes = {} - attributes['field_mapping'] = dict(lon='lon', lat='lat', data='z', time='time') + attributes['field_mapping'] = dict( + lon='lon', lat='lat', data='z', time='time' + ) attributes['time_units'] = 'years' attributes['time_longname'] = 'Date_in_Decimal_Years' attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' @@ -243,85 +252,117 @@ def geocenter_spatial_maps(base_dir, PROC, DREL, # Output spatial data object grid = gravtk.spatial() # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output Degree Interval - if (INTERVAL == 1): + 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): + 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) + 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): + 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) + 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 = np.radians(90.0-grid.lat) + theta = np.radians(90.0 - grid.lat) PLM, dPLM = gravtk.plm_holmes(2, np.cos(theta)) # fit coefficients - fits = ['x1','x2','SS','SC','AS','AC','S2SGRC','S2CGRC','S2SGFO','S2CGFO'] + fits = [ + 'x1', + 'x2', + 'SS', + 'SC', + 'AS', + 'AC', + 'S2SGRC', + 'S2CGRC', + 'S2SGFO', + 'S2CGFO', + ] # for each test run - for i,input_flag in enumerate(FLAGS): + for i, input_flag in enumerate(FLAGS): # read geocenter file for processing center and model - fargs = (PROC,DREL,model_str,input_flag,slr_str,gia_str,ds_str) + fargs = (PROC, DREL, model_str, input_flag, slr_str, gia_str, ds_str) grace_file = '{0}_{1}_{2}{3}{4}{5}{6}.txt'.format(*fargs) DEG1 = gravtk.geocenter().from_UCI(grace_dir.joinpath(grace_file)) # indices for months - kk,=np.nonzero((DEG1.month >= START) & (DEG1.month <= END)) + (kk,) = np.nonzero((DEG1.month >= START) & (DEG1.month <= END)) MEAN = DEG1.mean(indices=kk) # if data is Release-5: remove ECMWF jump corrections - if (DREL == 'RL05'): - DEG1.C10[kk] -= atm_corr['clm'][1,0,:] - DEG1.C11[kk] -= atm_corr['clm'][1,1,:] - DEG1.S11[kk] -= atm_corr['slm'][1,1,:] + if DREL == 'RL05': + DEG1.C10[kk] -= atm_corr['clm'][1, 0, :] + DEG1.C11[kk] -= atm_corr['clm'][1, 1, :] + DEG1.S11[kk] -= atm_corr['slm'][1, 1, :] # create dictionary for extracting regressed coefficients Ylm = {} - for k,f in enumerate(fits): + for k, f in enumerate(fits): Ylm[f] = np.zeros((3)) # calculate regression over each coefficient - for j,key in enumerate(coef_labels): + for j, key in enumerate(coef_labels): val = getattr(DEG1, key) # calculate regression coefficients TERMS = gravtk.time_series.aliasing_terms(DEG1.time[kk]) - x1 = gravtk.time_series.regress(DEG1.time[kk], dfactor[1]*val[kk], - ORDER=1, CYCLES=[0.5,1.0], TERMS=TERMS, - CONF=0.95, AICc=True) - x2 = gravtk.time_series.regress(DEG1.time[kk], dfactor[1]*val[kk], - ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS, - CONF=0.95, AICc=True) + x1 = gravtk.time_series.regress( + DEG1.time[kk], + dfactor[1] * val[kk], + ORDER=1, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + CONF=0.95, + AICc=True, + ) + x2 = gravtk.time_series.regress( + DEG1.time[kk], + dfactor[1] * val[kk], + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + CONF=0.95, + AICc=True, + ) # save coefficients - for k,f in enumerate(fits): - Ylm[f][j] = x1['beta'][k+1] + for k, f in enumerate(fits): + Ylm[f][j] = x1['beta'][k + 1] # extract Ylms and convert to spatial var = {} - for k,f in enumerate(fits): - clm = np.zeros((2,2)) - slm = np.zeros((2,2)) - clm[1,0] = Ylm[f][0] - clm[1,1] = Ylm[f][1] - slm[1,1] = Ylm[f][2] - var[f] = gravtk.harmonic_summation(clm, slm, grid.lon, grid.lat, - LMAX=1, MMAX=1, PLM=PLM[:2,:2,:]).T + for k, f in enumerate(fits): + clm = np.zeros((2, 2)) + slm = np.zeros((2, 2)) + clm[1, 0] = Ylm[f][0] + clm[1, 1] = Ylm[f][1] + slm[1, 1] = Ylm[f][2] + var[f] = gravtk.harmonic_summation( + clm, slm, grid.lon, grid.lat, LMAX=1, MMAX=1, PLM=PLM[:2, :2, :] + ).T # amplitude and phase of cyclical components - var['SA'],var['SP'] = gravtk.time_series.amplitude(var['SS'],var['SC']) - var['AA'],var['AP'] = gravtk.time_series.amplitude(var['AS'],var['AC']) - var['S2AGRC'],var['S2PGRC'] = gravtk.time_series.amplitude(var['S2SGRC'],var['S2CGRC']) - var['S2AGFO'],var['S2PGFO'] = gravtk.time_series.amplitude(var['S2SGFO'],var['S2CGFO']) + var['SA'], var['SP'] = gravtk.time_series.amplitude( + var['SS'], var['SC'] + ) + var['AA'], var['AP'] = gravtk.time_series.amplitude( + var['AS'], var['AC'] + ) + var['S2AGRC'], var['S2PGRC'] = gravtk.time_series.amplitude( + var['S2SGRC'], var['S2CGRC'] + ) + var['S2AGFO'], var['S2PGFO'] = gravtk.time_series.amplitude( + var['S2SGFO'], var['S2CGFO'] + ) # out regression coefficients and amplitudes to file unit_suffix = [' yr^-1', ' yr^-2', '', '', ''] - for j,key in enumerate(['x1','x2','SA','AA','S2AGRC','S2AGFO']): + for j, key in enumerate(['x1', 'x2', 'SA', 'AA', 'S2AGRC', 'S2AGFO']): # copy variables to output grid grid.data = np.copy(var[key]) grid.mask = np.zeros_like(grid.data, dtype=bool) @@ -331,8 +372,18 @@ def geocenter_spatial_maps(base_dir, PROC, DREL, attributes['longname'] = copy.copy(unit_name) attributes['title'] = copy.copy(key) # save to file - FILE = file_format.format(PROC,DREL,model_str,input_flag, - slr_str,gia_str,unit_label,key,ds_str,suffix[DATAFORM]) + FILE = file_format.format( + PROC, + DREL, + model_str, + input_flag, + slr_str, + gia_str, + unit_label, + key, + ds_str, + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(FILE) grid.to_file(OUTPUT_FILE, format=DATAFORM, **attributes) # add file to output list @@ -341,7 +392,7 @@ def geocenter_spatial_maps(base_dir, PROC, DREL, OUTPUT_FILE.chmod(mode=MODE) # output phase to file - for key in ['SP','AP','S2PGRC','S2PGFO']: + for key in ['SP', 'AP', 'S2PGRC', 'S2PGFO']: # convert phase from -180:180 to 0:360 var[key] = np.where(var[key] < 0, var[key] + 360.0, var[key]) # copy variables to output grid @@ -352,8 +403,18 @@ def geocenter_spatial_maps(base_dir, PROC, DREL, attributes['longname'] = 'Phase' attributes['title'] = copy.copy(key) # save to file - FILE = file_format.format(PROC,DREL,model_str,input_flag, - slr_str,gia_str,unit_label,key,ds_str,suffix[DATAFORM]) + FILE = file_format.format( + PROC, + DREL, + model_str, + input_flag, + slr_str, + gia_str, + unit_label, + key, + ds_str, + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(FILE) grid.to_file(OUTPUT_FILE, format=DATAFORM, **attributes) # add file to output list @@ -363,10 +424,11 @@ def geocenter_spatial_maps(base_dir, PROC, DREL, # return the list of output files return output_files + # PURPOSE: print a file log for the geocenter analysis def output_log_file(input_arguments, output_files): # format: geocenter_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'geocenter_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -383,10 +445,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the geocenter analysis def output_error_log_file(input_arguments): # format: geocenter_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'geocenter_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -402,6 +465,7 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -409,116 +473,248 @@ def arguments(): and exports trends in the monthly spatial fields in millimeters water equivalent """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') + help='Output directory for spatial files', + ) # Data processing center or satellite mission - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') - parser.add_argument('--kl','-k', - type=float, default=0.021, - help='Degree 1 gravitational Load Love number') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) + parser.add_argument( + '--kl', + '-k', + type=float, + default=0.021, + help='Degree 1 gravitational Load Love number', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2,3], - help=('Output grid interval ' - '(1: global, 2: centered global, 3: non-global)')) - parser.add_argument('--bounds', - type=float, nargs=4, metavar=('lon_min','lon_max','lat_min','lat_max'), - help='Bounding box for non-global grid') + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2, 3], + help=( + 'Output grid interval ' + '(1: global, 2: centered global, 3: non-global)' + ), + ) + parser.add_argument( + '--bounds', + type=float, + nargs=4, + metavar=('lon_min', 'lon_max', 'lat_min', 'lat_max'), + help='Bounding box for non-global grid', + ) # replace low degree harmonics with values from Satellite Laser Ranging - parser.add_argument('--slr-c20', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C20 coefficients with SLR values') - parser.add_argument('--slr-21', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C21 and S21 coefficients with SLR values') - parser.add_argument('--slr-22', - type=str, default=None, choices=['CSR','GSFC'], - help='Replace C22 and S22 coefficients with SLR values') - parser.add_argument('--slr-c30', - type=str, default=None, choices=['CSR','GFZ','GSFC','LARES'], - help='Replace C30 coefficients with SLR values') - parser.add_argument('--slr-c40', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C40 coefficients with SLR values') - parser.add_argument('--slr-c50', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C50 coefficients with SLR values') + parser.add_argument( + '--slr-c20', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C20 coefficients with SLR values', + ) + parser.add_argument( + '--slr-21', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C21 and S21 coefficients with SLR values', + ) + parser.add_argument( + '--slr-22', + type=str, + default=None, + choices=['CSR', 'GSFC'], + help='Replace C22 and S22 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c30', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC', 'LARES'], + help='Replace C30 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c40', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C40 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c50', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C50 coefficients with SLR values', + ) # Output data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Output data format', + ) # Output log file for each job in forms # geocenter_run_2002-04-01_PID-00000.log # geocenter_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -550,18 +746,20 @@ def main(): SLR_C50=args.slr_c50, DATAFORM=args.format, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/geocenter/kernel_degree_one.py b/geocenter/kernel_degree_one.py index 2f881d60..e0dc69eb 100644 --- a/geocenter/kernel_degree_one.py +++ b/geocenter/kernel_degree_one.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" kernel_degree_one.py Written by Tyler Sutterley (07/2026) @@ -125,6 +125,7 @@ can use variable loglevels for verbose output Written 11/2021 """ + from __future__ import print_function import sys @@ -142,6 +143,7 @@ # attempt imports netCDF4 = gravtk.utilities.import_dependency('netCDF4') + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -151,8 +153,12 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate a geocenter time-series -def kernel_degree_one(base_dir, LMAX, RAD, +def kernel_degree_one( + base_dir, + LMAX, + RAD, MMAX=None, LOVE_NUMBERS=0, LOVE_K1=None, @@ -160,8 +166,8 @@ def kernel_degree_one(base_dir, LMAX, RAD, FINGERPRINT=False, EXPANSION=None, LANDMASK=None, - MODE=0o775): - + MODE=0o775, +): # output directory base_dir = pathlib.Path(base_dir).expanduser().absolute() DIRECTORY = base_dir.joinpath('geocenter') @@ -171,9 +177,9 @@ def kernel_degree_one(base_dir, LMAX, RAD, output_files = [] # read load love numbers - LOVE = gravtk.load_love_numbers(EXPANSION, - LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF', - FORMAT='class') + LOVE = gravtk.load_love_numbers( + EXPANSION, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF', FORMAT='class' + ) # set gravitational load love number to a specific value if LOVE_K1: LOVE.kl[1] = np.copy(LOVE_K1) @@ -185,35 +191,39 @@ def kernel_degree_one(base_dir, LMAX, RAD, # 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)) + n_harm = np.int64( + LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX) ** 2 - (LMAX - MMAX) + ) # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) - rho_e = factors.rho_e# Average Density of the Earth [g/cm^3] - rad_e = factors.rad_e# Average Radius of the Earth [cm] + rho_e = factors.rho_e # Average Density of the Earth [g/cm^3] + rad_e = factors.rad_e # Average Radius of the Earth [cm] l = factors.l # Factor for converting to Mass SH dfactor = factors.get('cmwe') # Read Smoothed Ocean and Land Functions # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) # degree spacing and grid dimensions # will create GRACE spatial fields with same dimensions - dlon,dlat = landsea.spacing + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # spatial parameters in radians dphi = np.radians(dlon) dth = np.radians(dlat) # longitude and colatitude in radians - phi = np.radians(landsea.lon[np.newaxis,:]) + phi = np.radians(landsea.lon[np.newaxis, :]) th = np.radians(90.0 - np.squeeze(landsea.lat)) # create land function - land_function = np.zeros((nlon, nlat),dtype=np.float64) + land_function = np.zeros((nlon, nlat), dtype=np.float64) # extract land function from file # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data.T >= 1) & (landsea.data.T <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data.T >= 1) & (landsea.data.T <= 3)) + land_function[indx, indy] = 1.0 # calculate ocean function from land function ocean_function = 1.0 - land_function @@ -224,31 +234,39 @@ def kernel_degree_one(base_dir, LMAX, RAD, # calculate spherical harmonics of ocean function to degree 1 # mass is equivalent to 1 cm ocean height change # eustatic ratio = -land total/ocean total - ocean_Ylms = gravtk.gen_stokes(ocean_function, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=1, LOVE=LOVE, PLM=PLM[:2,:2,:]) + ocean_Ylms = gravtk.gen_stokes( + ocean_function, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + LOVE=LOVE, + PLM=PLM[:2, :2, :], + ) # Gaussian Smoothing (Jekeli, 1981) - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) gw_str = '' # Calculating cos/sin of phi arrays # output [m,phi] - m = np.arange(0,MMAX+1) + m = np.arange(0, MMAX + 1) # Integration factors (solid angle) - int_fact = np.sin(th)*dphi*dth + int_fact = np.sin(th) * dphi * dth # 4-pi normalization - norm = 1.0/(4.0*np.pi) + norm = 1.0 / (4.0 * np.pi) # calculating cos(m*phi) and sin(m*phi) using Euler's formula - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", m, phi)) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', m, phi)) # Legendre polynomials for degree 1 - P10 = np.squeeze(PLM[1,0,:]) - P11 = np.squeeze(PLM[1,1,:]) + P10 = np.squeeze(PLM[1, 0, :]) + P11 = np.squeeze(PLM[1, 1, :]) # Initializing 3x3 I-Parameter matrix # (see equations 12 and 13 of Swenson et al., 2008) @@ -256,21 +274,39 @@ def kernel_degree_one(base_dir, LMAX, RAD, # I-Parameter matrix accounts for the fact that the GRACE data only # includes spherical harmonic degrees greater than or equal to 2 # C10, C11, S11 - PC10 = np.einsum("h...,p...->ph...", P10, m_phi[0,:].real) - PC11 = np.einsum("h...,p...->ph...", P11, m_phi[1,:].real) - PS11 = np.einsum("h...,p...->ph...", P11, m_phi[1,:].imag) + PC10 = np.einsum('h...,p...->ph...', P10, m_phi[0, :].real) + PC11 = np.einsum('h...,p...->ph...', P11, m_phi[1, :].real) + PS11 = np.einsum('h...,p...->ph...', P11, m_phi[1, :].imag) # C10: C10, C11, S11 - IMAT[0,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PC10) - IMAT[1,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PC11) - IMAT[2,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PS11) + IMAT[0, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PC10 + ) + IMAT[1, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PC11 + ) + IMAT[2, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PS11 + ) # C11: C10, C11, S11 - IMAT[0,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PC10) - IMAT[1,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PC11) - IMAT[2,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PS11) + IMAT[0, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PC10 + ) + IMAT[1, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PC11 + ) + IMAT[2, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PS11 + ) # S11: C10, C11, S11 - IMAT[0,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PC10) - IMAT[1,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PC11) - IMAT[2,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PS11) + IMAT[0, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PC10 + ) + IMAT[1, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PC11 + ) + IMAT[2, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PS11 + ) # output flag for using sea level fingerprints slf_str = '_SLF' if FINGERPRINT else '' @@ -286,9 +322,9 @@ def kernel_degree_one(base_dir, LMAX, RAD, # 1-based index of geocenter output['geocenter'] = 1 + np.arange(3) # Legendre polynomials for each degree and order - plm = np.zeros((n_harm,nlat)) + plm = np.zeros((n_harm, nlat)) # cosine and sine factors - mphi = np.zeros((n_harm,nlon)) + mphi = np.zeros((n_harm, nlon)) # factor for converting to coefficients of mass fit_factor = np.zeros((n_harm)) # ii is a counter variable for building the column array @@ -296,28 +332,28 @@ def kernel_degree_one(base_dir, LMAX, RAD, # Creating column array of clm/slm coefficients # Order is [C00...C6060,S11...S6060] # Switching between Cosine and Sine Stokes - for cs,csharm in enumerate(['clm','slm']): + for cs, csharm in enumerate(['clm', 'slm']): # for each spherical harmonic degree # +1 to include LMAX - for l in range(LMIN,LMAX+1): + for l in range(LMIN, LMAX + 1): # for each spherical harmonic order # Sine Stokes for (m=0) = 0 - mm = np.min([MMAX,l]) + mm = np.min([MMAX, l]) # +1 to include l or MMAX (whichever is smaller) - for m in range(cs,mm+1): + for m in range(cs, mm + 1): # copy dimensions output['l'][ii] = l output['m'][ii] = m output['cs'][ii] = cs # legendre polynomials - plm[ii,:] = np.copy(PLM[l,m,:]) + plm[ii, :] = np.copy(PLM[l, m, :]) # degree dependent factor to convert to mass - fit_factor[ii] = wt[l]*(2.0*l + 1.0)/(1.0 + LOVE.kl[l]) + fit_factor[ii] = wt[l] * (2.0 * l + 1.0) / (1.0 + LOVE.kl[l]) # cosine and sine factors - if (csharm == 'clm'): - mphi[ii,:] = m_phi[m,:].real - elif (csharm == 'slm'): - mphi[ii,:] = m_phi[m,:].imag + if csharm == 'clm': + mphi[ii, :] = m_phi[m, :].real + elif csharm == 'slm': + mphi[ii, :] = m_phi[m, :].imag # add 1 to counter ii += 1 @@ -326,9 +362,9 @@ def kernel_degree_one(base_dir, LMAX, RAD, for i in range(n_harm): # setting kern_i equal to 1 for d/o kern_i = np.zeros((n_harm, nlat)) - kern_i[i,:] = 1.0*fit_factor[i] + kern_i[i, :] = 1.0 * fit_factor[i] # calculate land mass maps - lmass = np.dot(mphi.T, plm*kern_i) + lmass = np.dot(mphi.T, plm * kern_i) # Calculating data matrices # GRACE Eustatic degree 1 from land variations eustatic = gravtk.geocenter() @@ -340,41 +376,74 @@ def kernel_degree_one(base_dir, LMAX, RAD, # NOTE: this is an unscaled GRACE estimate that uses the # buffered land function when solving the sea-level equation. # possible improvement using scaled estimate with real coastlines - land_Ylms = gravtk.gen_stokes(lmass*land_function, - landsea.lon, landsea.lat, UNITS=1, LMIN=0, - LMAX=EXPANSION, PLM=PLM, LOVE=LOVE) + land_Ylms = gravtk.gen_stokes( + lmass * land_function, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=EXPANSION, + PLM=PLM, + LOVE=LOVE, + ) # 2) calculate sea level fingerprints of land mass # use maximum of 3 iterations for computational efficiency - sea_level = gravtk.sea_level_equation(land_Ylms.clm, land_Ylms.slm, - landsea.lon, landsea.lat, land_function, LMAX=EXPANSION, - LOVE=LOVE_K1, BODY_TIDE_LOVE=0, FLUID_LOVE=0, ITERATIONS=3, - POLAR=True, PLM=PLM, FILL_VALUE=0) + sea_level = gravtk.sea_level_equation( + land_Ylms.clm, + land_Ylms.slm, + landsea.lon, + landsea.lat, + land_function, + LMAX=EXPANSION, + LOVE=LOVE_K1, + BODY_TIDE_LOVE=0, + FLUID_LOVE=0, + ITERATIONS=3, + POLAR=True, + PLM=PLM, + FILL_VALUE=0, + ) # 3) convert sea level fingerprints into spherical harmonics - slf_Ylms = gravtk.gen_stokes(sea_level, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=1, PLM=PLM[:2,:2,:], LOVE=LOVE) + slf_Ylms = gravtk.gen_stokes( + sea_level, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + PLM=PLM[:2, :2, :], + LOVE=LOVE, + ) # 4) convert the slf degree 1 harmonics to mass with dfactor - eustatic.C10 = slf_Ylms.clm[1,0]*dfactor[1] - eustatic.C11 = slf_Ylms.clm[1,1]*dfactor[1] - eustatic.S11 = slf_Ylms.slm[1,1]*dfactor[1] + eustatic.C10 = slf_Ylms.clm[1, 0] * dfactor[1] + eustatic.C11 = slf_Ylms.clm[1, 1] * dfactor[1] + eustatic.S11 = slf_Ylms.slm[1, 1] * dfactor[1] else: # steps to calculate eustatic component from GRACE land-water change: # 1) calculate total mass of 1 cm of ocean height (calculated above) # 2) calculate total land mass # NOTE: possible improvement using the sea-level equation to solve # for the spatial pattern of sea level from the land water mass - land_Ylms = gravtk.gen_stokes(lmass*land_function, landsea.lon, - landsea.lat, UNITS=1, LMIN=0, LMAX=1, PLM=PLM[:2,:2,:], - LOVE=LOVE) + land_Ylms = gravtk.gen_stokes( + lmass * land_function, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + PLM=PLM[:2, :2, :], + LOVE=LOVE, + ) # 3) calculate ratio between the total land mass and the total mass # of 1 cm of ocean height (negative as positive land = sea level drop) # this converts the total land change to ocean height change - eustatic_ratio = -land_Ylms.clm[0,0]/ocean_Ylms.clm[0,0] + eustatic_ratio = -land_Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0] # 4) scale degree one coefficients of ocean function with ratio # and convert the eustatic degree 1 harmonics to mass with dfactor - scale_factor = eustatic_ratio*dfactor[1] - eustatic.C10 = ocean_Ylms.clm[1,0]*scale_factor - eustatic.C11 = ocean_Ylms.clm[1,1]*scale_factor - eustatic.S11 = ocean_Ylms.slm[1,1]*scale_factor + scale_factor = eustatic_ratio * dfactor[1] + eustatic.C10 = ocean_Ylms.clm[1, 0] * scale_factor + eustatic.C11 = ocean_Ylms.clm[1, 1] * scale_factor + eustatic.S11 = ocean_Ylms.slm[1, 1] * scale_factor # eustatic coefficients of degree 1 CMAT = np.array([eustatic.C10, eustatic.C11, eustatic.S11]) @@ -382,21 +451,39 @@ def kernel_degree_one(base_dir, LMAX, RAD, # calculate ocean mass for each degree and order for j in range(n_harm): # setting kern_j equal to 1 for d/o - kern_j = np.zeros((n_harm,nlat)) + kern_j = np.zeros((n_harm, nlat)) # skipping C10, C11 and S11 for ocean mass - if (j >= 3): - kern_j[j,:] = 1.0*fit_factor[j] + if j >= 3: + kern_j[j, :] = 1.0 * fit_factor[j] # calculate ocean mass maps - rmass = np.dot(mphi.T, plm*kern_j) + rmass = np.dot(mphi.T, plm * kern_j) # Allocate for G matrix parameters # G matrix calculates the GRACE ocean mass variations G = gravtk.geocenter() # calculate G matrix parameters through a summation of each latitude # summation of integration factors, Legendre polynomials, # (convolution of order and harmonics) and the ocean mass at t - G.C10 = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, rmass) - G.C11 = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, rmass) - G.S11 = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, rmass) + G.C10 = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', + int_fact, + PC10, + ocean_function, + rmass, + ) + G.C11 = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', + int_fact, + PC11, + ocean_function, + rmass, + ) + G.S11 = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', + int_fact, + PS11, + ocean_function, + rmass, + ) # G Matrix for time t GMAT = np.array([G.C10, G.C11, G.S11]) @@ -404,18 +491,21 @@ def kernel_degree_one(base_dir, LMAX, RAD, # this is mathematically equivalent to an iterative procedure # whereby the initial degree one coefficients are used to update # the G Matrix until (C10, C11, S11) converge - if (SOLVER == 'inv'): - DMAT = np.dot(np.linalg.inv(IMAT), (CMAT-GMAT)) - elif (SOLVER == 'lstsq'): - DMAT = np.linalg.lstsq(IMAT, (CMAT-GMAT), rcond=-1)[0] + if SOLVER == 'inv': + DMAT = np.dot(np.linalg.inv(IMAT), (CMAT - GMAT)) + elif SOLVER == 'lstsq': + DMAT = np.linalg.lstsq(IMAT, (CMAT - GMAT), rcond=-1)[0] elif SOLVER in ('gelsd', 'gelsy', 'gelss'): - DMAT, res, rnk, s = scipy.linalg.lstsq(IMAT, (CMAT-GMAT), - lapack_driver=SOLVER) + DMAT, res, rnk, s = scipy.linalg.lstsq( + IMAT, (CMAT - GMAT), lapack_driver=SOLVER + ) # normalize covariances and save to matrix - output['covariance'][i,j,:] = rho_e*rad_e*DMAT/(4.0*np.pi*dfactor[1]) + output['covariance'][i, j, :] = ( + rho_e * rad_e * DMAT / (4.0 * np.pi * dfactor[1]) + ) # save to file with all descriptor flags - args = ('COV',LMAX,order_str,gw_str,slf_str,'nc') + args = ('COV', LMAX, order_str, gw_str, slf_str, 'nc') FILE = DIRECTORY.joinpath('{0}_L{1:d}{2}{3}{4}.{5}'.format(*args)) ncdf_covariance(output, FILENAME=FILE) # change the permissions mode @@ -425,6 +515,7 @@ def kernel_degree_one(base_dir, LMAX, RAD, output_files.append(FILE) return output_files + # PURPOSE: Write spherical harmonic covariance coefficients to file def ncdf_covariance(output, **kwargs): """ @@ -443,20 +534,20 @@ def ncdf_covariance(output, **kwargs): DATE: harmonics have date information """ # set default keyword arguments - kwargs.setdefault('FILENAME',None) - kwargs.setdefault('UNITS','Geodesy_Normalization') - kwargs.setdefault('TITLE',None) - kwargs.setdefault('DATE',True) - kwargs.setdefault('CLOBBER',True) + kwargs.setdefault('FILENAME', None) + kwargs.setdefault('UNITS', 'Geodesy_Normalization') + kwargs.setdefault('TITLE', None) + kwargs.setdefault('DATE', True) + kwargs.setdefault('CLOBBER', True) # setting NetCDF clobber attribute clobber = 'w' if kwargs['CLOBBER'] else 'a' # opening netCDF file for writing - fileID = netCDF4.Dataset(kwargs['FILENAME'], clobber, format="NETCDF4") + fileID = netCDF4.Dataset(kwargs['FILENAME'], clobber, format='NETCDF4') # Calculating the number of cos and sin harmonics up to LMAX # taking into account MMAX (if MMAX == LMAX then LMAX-MMAX=0) - n_harm,_,n_geo = output['covariance'].shape + n_harm, _, n_geo = output['covariance'].shape # Defining the netCDF dimensions fileID.createDimension('lm', n_harm) @@ -468,21 +559,21 @@ def ncdf_covariance(output, **kwargs): nc['l'] = fileID.createVariable('l', 'i', ('lm',)) nc['m'] = fileID.createVariable('m', 'i', ('lm',)) nc['cs'] = fileID.createVariable('cs', 'i', ('lm',)) - nc['geocenter'] = fileID.createVariable('geocenter', 'i', - ('geocenter',)) + nc['geocenter'] = fileID.createVariable('geocenter', 'i', ('geocenter',)) # spherical harmonics - nc['covariance'] = fileID.createVariable('covariance', 'd', - ('lm','lm','geocenter')) + nc['covariance'] = fileID.createVariable( + 'covariance', 'd', ('lm', 'lm', 'geocenter') + ) # filling netCDF variables - for key,val in output.items(): + for key, val in output.items(): nc[key][:] = val.copy() # Defining attributes for degree and order - nc['l'].long_name = 'spherical_harmonic_degree'# SH degree long name - nc['l'].units = 'Wavenumber'# SH degree units - nc['m'].long_name = 'spherical_harmonic_order'# SH order long name - nc['m'].units = 'Wavenumber'# SH order units + nc['l'].long_name = 'spherical_harmonic_degree' # SH degree long name + nc['l'].units = 'Wavenumber' # SH degree units + nc['m'].long_name = 'spherical_harmonic_order' # SH order long name + nc['m'].units = 'Wavenumber' # SH order units nc['cs'].long_name = 'cosine/sine harmonics' # Defining attributes for harmonics nc['covariance'].long_name = 'spherical_harmonic_covariance' @@ -491,7 +582,7 @@ def ncdf_covariance(output, **kwargs): nc['geocenter'].units = '1' nc['geocenter'].long_name = 'Geocenter' nc['geocenter'].flag_meanings = '1: C10, 2: C11, 3: S11' - nc['geocenter'].flag_values = [1,2,3] + nc['geocenter'].flag_values = [1, 2, 3] nc['geocenter'].valid_min = 1 nc['geocenter'].valid_max = 3 @@ -501,14 +592,17 @@ def ncdf_covariance(output, **kwargs): fileID.creator_url = 'https://www.ess.uci.edu/~velicogna/index.html' fileID.reference = 'https://doi.org/10.3390/rs11182108' fileID.creator_type = 'group' - fileID.creator_institution = ('University of Washington; ' - 'University of California, Irvine') + fileID.creator_institution = ( + 'University of Washington; University of California, Irvine' + ) fileID.history = 'Created at UC Irvine' fileID.source = 'derived' fileID.title = 'Geocenter covariance' - fileID.summary = ('Geocenter covariance coefficients. Geocenter ' + fileID.summary = ( + 'Geocenter covariance coefficients. Geocenter ' 'coefficients represent the largest-scale variability of ' - 'hydrologic, cryospheric, and solid Earth processes.') + 'hydrologic, cryospheric, and solid Earth processes.' + ) project = [] project.append('NASA Gravity Recovery And Climate Experiment (GRACE)') project.append('GRACE Follow-On (GRACE-FO)') @@ -534,7 +628,7 @@ def ncdf_covariance(output, **kwargs): fileID.software_reference = gravtk.version.project_name fileID.software_version = gravtk.version.full_version # date created - fileID.date_created = time.strftime('%Y-%m-%d',time.localtime()) + fileID.date_created = time.strftime('%Y-%m-%d', time.localtime()) # Output netCDF structure information logging.info(kwargs['FILENAME']) @@ -543,10 +637,11 @@ def ncdf_covariance(output, **kwargs): # Closing the netCDF file fileID.close() + # PURPOSE: print a file log for the GRACE degree one analysis def output_log_file(input_arguments, output_files): # format: kernel_degree_one_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'kernel_degree_one_run_{0}_PID-{1:d}.log'.format(*args) DIRECTORY = pathlib.Path(input_arguments.directory).joinpath('geocenter') # create a unique log and open the log file @@ -563,10 +658,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE degree one analysis def output_error_log_file(input_arguments): # format: kernel_degree_one_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'kernel_degree_one_failed_run_{0}_PID-{1:d}.log'.format(*args) DIRECTORY = pathlib.Path(input_arguments.directory).joinpath('geocenter') # create a unique log and open the log file @@ -582,88 +678,146 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the sensitivity of geocenter calculations for each degree and order """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') - parser.add_argument('--kl','-k', - type=float, default=0.021, - help='Degree 1 gravitational Load Love number') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) + parser.add_argument( + '--kl', + '-k', + type=float, + default=0.021, + help='Degree 1 gravitational Load Love number', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format for ocean models') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format for ocean models', + ) # least squares solver - choices = ('inv','lstsq','gelsd', 'gelsy', 'gelss') - parser.add_argument('--solver','-s', - type=str, default='lstsq', choices=choices, - help='Least squares solver for degree one solutions') + choices = ('inv', 'lstsq', 'gelsd', 'gelsy', 'gelss') + parser.add_argument( + '--solver', + '-s', + type=str, + default='lstsq', + choices=choices, + help='Least squares solver for degree one solutions', + ) # run with sea level fingerprints - parser.add_argument('--fingerprint', - default=False, action='store_true', - help='Redistribute land-water flux using sea level fingerprints') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--fingerprint', + default=False, + action='store_true', + help='Redistribute land-water flux using sea level fingerprints', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # land-sea mask for calculating ocean mass and land water flux - land_mask_file = gravtk.utilities.get_data_path(['data','land_fcn_300km.nc']) - parser.add_argument('--mask', + land_mask_file = gravtk.utilities.get_data_path( + ['data', 'land_fcn_300km.nc'] + ) + parser.add_argument( + '--mask', type=pathlib.Path, default=land_mask_file, - help='Land-sea mask for calculating ocean mass and land water flux') + help='Land-sea mask for calculating ocean mass and land water flux', + ) # Output log file for each job in forms # kernel_degree_one_run_2002-04-01_PID-00000.log # kernel_degree_one_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -684,18 +838,20 @@ def main(): FINGERPRINT=args.fingerprint, EXPANSION=args.expansion, LANDMASK=args.mask, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/geocenter/kernel_degree_one_error.py b/geocenter/kernel_degree_one_error.py index 54011580..bb5e03b8 100644 --- a/geocenter/kernel_degree_one_error.py +++ b/geocenter/kernel_degree_one_error.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" kernel_degree_one_error.py Written by Tyler Sutterley (06/2024) @@ -148,6 +148,7 @@ can use variable loglevels for verbose output Written 11/2021 """ + from __future__ import print_function import sys @@ -163,6 +164,7 @@ # attempt imports netCDF4 = gravtk.utilities.import_dependency('netCDF4') + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -172,8 +174,14 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate the uncertainty in the geocenter time-series -def kernel_degree_one_error(base_dir, PROC, DREL, LMAX, RAD, +def kernel_degree_one_error( + base_dir, + PROC, + DREL, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -196,8 +204,8 @@ def kernel_degree_one_error(base_dir, PROC, DREL, LMAX, RAD, FINGERPRINT=False, EXPANSION=None, ERROR_TYPE='RMS', - MODE=0o775): - + MODE=0o775, +): # output directory base_dir = pathlib.Path(base_dir).expanduser().absolute() DIRECTORY = base_dir.joinpath('geocenter') @@ -226,39 +234,39 @@ def kernel_degree_one_error(base_dir, PROC, DREL, LMAX, RAD, # output flag for using sea level fingerprints slf_str = '_SLF' if FINGERPRINT else '' # output flag for low-degree harmonic replacements - if SLR_21 in ('CSR','GFZ','GSFC'): + if SLR_21 in ('CSR', 'GFZ', 'GSFC'): C21_str = f'_w{SLR_21}_21' else: C21_str = '' - if SLR_22 in ('CSR','GSFC'): + if SLR_22 in ('CSR', 'GSFC'): C22_str = f'_w{SLR_22}_22' else: C22_str = '' if SLR_C30 in ('GSFC',): # C30 replacement now default for all solutions C30_str = '' - elif SLR_C30 in ('CSR','GFZ','LARES'): + elif SLR_C30 in ('CSR', 'GFZ', 'LARES'): C30_str = f'_w{SLR_C30}_C30' else: C30_str = '' - if SLR_C40 in ('CSR','GSFC','LARES'): + if SLR_C40 in ('CSR', 'GSFC', 'LARES'): C40_str = f'_w{SLR_C40}_C40' else: C40_str = '' - if SLR_C50 in ('CSR','GSFC','LARES'): + if SLR_C50 in ('CSR', 'GSFC', 'LARES'): C50_str = f'_w{SLR_C50}_C50' else: C50_str = '' # combine satellite laser ranging flags - slr_str = ''.join([C21_str,C22_str,C30_str,C40_str,C50_str]) + slr_str = ''.join([C21_str, C22_str, C30_str, C40_str, C50_str]) # suffix for input ascii, netcdf and HDF5 files suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # read load love numbers - LOVE = gravtk.load_love_numbers(EXPANSION, - LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF', - FORMAT='class') + LOVE = gravtk.load_love_numbers( + EXPANSION, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF', FORMAT='class' + ) # set gravitational load love number to a specific value if LOVE_K1: LOVE.kl[1] = np.copy(LOVE_K1) @@ -269,33 +277,53 @@ def kernel_degree_one_error(base_dir, PROC, DREL, LMAX, RAD, MMAX = np.copy(LMAX) # Gaussian Smoothing (Jekeli, 1981) - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) gw_str = '' # 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)) + n_harm = np.int64( + LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX) ** 2 - (LMAX - MMAX) + ) # reading GRACE months for input date range # replacing low-degree harmonics with SLR values if specified # correcting for Pole-Tide drift if specified # atmospheric jumps will be corrected externally if specified - Ylms = gravtk.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_C40=SLR_C40, SLR_C50=SLR_C50, - POLE_TIDE=POLE_TIDE, ATM=False, MODEL_DEG1=False) + Ylms = gravtk.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_C40=SLR_C40, + SLR_C50=SLR_C50, + POLE_TIDE=POLE_TIDE, + ATM=False, + MODEL_DEG1=False, + ) # create harmonics object from GRACE/GRACE-FO data GSM_Ylms = gravtk.harmonics().from_dict(Ylms) # 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 = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GSM_Ylms.subtract(mean_Ylms) else: @@ -322,8 +350,8 @@ def kernel_degree_one_error(base_dir, PROC, DREL, LMAX, RAD, ATM_Ylms.month[:] = np.copy(GSM_Ylms.month) if ATM: atm_corr = gravtk.read_ecmwf_corrections(base_dir, LMAX, ATM_Ylms.month) - ATM_Ylms.clm[:,:,:] = np.copy(atm_corr['clm']) - ATM_Ylms.slm[:,:,:] = np.copy(atm_corr['slm']) + ATM_Ylms.clm[:, :, :] = np.copy(atm_corr['clm']) + ATM_Ylms.slm[:, :, :] = np.copy(atm_corr['slm']) # removing the mean of the atmospheric jump correction coefficients ATM_Ylms.mean(apply=True) # truncate to degree and order LMAX/MMAX @@ -340,8 +368,18 @@ def kernel_degree_one_error(base_dir, PROC, DREL, LMAX, RAD, # calculating GRACE/GRACE-FO error (Wahr et al. 2006) # output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) - fargs = (PROC,DREL,DSET,LMAX,order_str,ds_str,atm_str,GSM_Ylms.month[0], - GSM_Ylms.month[-1], suffix[DATAFORM]) + fargs = ( + PROC, + DREL, + DSET, + LMAX, + order_str, + ds_str, + atm_str, + GSM_Ylms.month[0], + GSM_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 = GSM_Ylms.directory.joinpath(delta_format.format(*fargs)) # check full path of the GRACE directory for delta file @@ -353,38 +391,41 @@ def kernel_degree_one_error(base_dir, PROC, DREL, LMAX, RAD, # Delta coefficients of GRACE time series (Error components) delta_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - delta_Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - delta_Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + 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'))): + 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 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 + 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']): + for cs, csharm in enumerate(['clm', 'slm']): # calculate GRACE Error (Noise of smoothed time-series) # With Annual and Semi-Annual Terms val1 = getattr(GSM_Ylms, csharm) - smth = gravtk.time_series.smooth(tdec, val1[l,m,:], - HFWTH=HFWTH) + smth = gravtk.time_series.smooth( + tdec, val1[l, m, :], HFWTH=HFWTH + ) # number of smoothed points nsmth = len(smth['data']) tsmth = np.mean(smth['time']) # GRACE/GRACE-FO delta Ylms # variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) - val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth) + val2[l, m] = np.sqrt(np.sum(smth['noise'] ** 2) / nsmth) # attributes for output files attributes = {} attributes['title'] = 'GRACE/GRACE-FO Spherical Harmonic Errors' - attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # save GRACE/GRACE-FO delta harmonics to file delta_Ylms.time = np.copy(tsmth) delta_Ylms.month = np.int64(nsmth) @@ -395,25 +436,24 @@ def kernel_degree_one_error(base_dir, PROC, DREL, LMAX, RAD, output_files.append(DELTA_FILE) else: # read GRACE/GRACE-FO delta harmonics from file - delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, - format=DATAFORM) + delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, format=DATAFORM) # 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.int64(delta_Ylms.month) # read covariance file with all descriptor flags - args = ('COV',LMAX,order_str,gw_str,slf_str,'nc') + args = ('COV', LMAX, order_str, gw_str, slf_str, 'nc') FILE = DIRECTORY.joinpath('{0}_L{1:d}{2}{3}{4}.{5}'.format(*args)) COV = {} # opening netCDF covariance file for reading with netCDF4.Dataset(FILE, mode='r') as fileID: # copy each netCDF variables - for key,val in fileID.variables.items(): + for key, val in fileID.variables.items(): COV[key] = val[:].copy() # calculate total uncertainty - if (ERROR_TYPE == 'RMS'): + if ERROR_TYPE == 'RMS': # add all the uncertainties in quadrature Ylms = delta_Ylms.power(2) for eYlms in error_Ylms: @@ -435,49 +475,52 @@ def kernel_degree_one_error(base_dir, PROC, DREL, LMAX, RAD, # ii is a counter variable for building the column array ii = 0 - for cs,csharm in enumerate(['clm','slm']): + for cs, csharm in enumerate(['clm', 'slm']): # get harmonics temp = getattr(Ylms, csharm) # for each spherical harmonic degree # +1 to include LMAX - for l in range(LMIN,LMAX+1): + for l in range(LMIN, LMAX + 1): # for each spherical harmonic order # Sine Stokes for (m=0) = 0 - mm = np.min([MMAX,l]) + mm = np.min([MMAX, l]) # +1 to include l or MMAX (whichever is smaller) - for m in range(cs,mm+1): + for m in range(cs, mm + 1): # cosine and sine harmonics - YLM[ii] = temp[l,m] + YLM[ii] = temp[l, m] # add 1 to counter ii += 1 # output geocenter errors DEG1 = gravtk.geocenter() # create a meshgrid for multiplying each harmonic - rows,cols = np.meshgrid(YLM, YLM) + rows, cols = np.meshgrid(YLM, YLM) # reduce to unique harmonic pairs - ii,jj = np.tril_indices(n_harm) + ii, jj = np.tril_indices(n_harm) # for each geocenter coefficient - for d,gc in enumerate(['C10','C11','S11']): - tmp = np.sum(rows[ii,jj]*cols[ii,jj]*COV['covariance'][ii,jj,d]**2) - setattr(DEG1,gc,np.sqrt(tmp)) + for d, gc in enumerate(['C10', 'C11', 'S11']): + tmp = np.sum( + rows[ii, jj] * cols[ii, jj] * COV['covariance'][ii, jj, d] ** 2 + ) + setattr(DEG1, gc, np.sqrt(tmp)) # output degree 1 coefficient errors file_format = '{0}_{1}_{2}{3}{4}{5}{6}.{7}' output_format = '{0:11.4f}{1:14.6e}{2:14.6e}{3:14.6e} {4:03d}\n' # local version with all descriptor flags - a1 = (PROC,DREL,model_str,slf_str,slr_str,delta_str,ds_str,'txt') + a1 = (PROC, DREL, model_str, slf_str, slr_str, delta_str, ds_str, 'txt') FILE1 = DIRECTORY.joinpath(file_format.format(*a1)) fid1 = FILE1.open(mode='w', encoding='utf8') logging.info(str(FILE1)) # print headers print_header(fid1) - print_harmonic(fid1,LOVE.kl[1]) - print_global(fid1,PROC,DREL,model_str.replace('_',' '), - SLR_C20,SLR_21,months) - print_variables(fid1,'single precision','fully normalized') + print_harmonic(fid1, LOVE.kl[1]) + print_global( + fid1, PROC, DREL, model_str.replace('_', ' '), SLR_C20, SLR_21, months + ) + print_variables(fid1, 'single precision', 'fully normalized') # output geocenter coefficients to file - args=(tdec.mean(),DEG1.C10,DEG1.C11,DEG1.S11,n_files) + args = (tdec.mean(), DEG1.C10, DEG1.C11, DEG1.S11, n_files) fid1.write(output_format.format(*args)) # close the output file fid1.close() @@ -488,49 +531,60 @@ def kernel_degree_one_error(base_dir, PROC, DREL, LMAX, RAD, # return the list of output files return output_files + # PURPOSE: print YAML header to top of file def print_header(fid): # print header fid.write('{0}:\n'.format('header')) # data dimensions fid.write(' {0}:\n'.format('dimensions')) - fid.write(' {0:22}: {1:d}\n'.format('degree',1)) - fid.write(' {0:22}: {1:d}\n'.format('order',1)) + fid.write(' {0:22}: {1:d}\n'.format('degree', 1)) + fid.write(' {0:22}: {1:d}\n'.format('order', 1)) fid.write('\n') + # PURPOSE: print spherical harmonic attributes to YAML header -def print_harmonic(fid,kl): +def print_harmonic(fid, kl): # non-standard attributes fid.write(' {0}:\n'.format('non-standard_attributes')) # load love number fid.write(' {0:22}:\n'.format('love_number')) long_name = 'Gravitational Load Love Number of Degree 1 (k1)' - fid.write(' {0:20}: {1}\n'.format('long_name',long_name)) - fid.write(' {0:20}: {1:0.3f}\n'.format('value',kl)) + fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) + fid.write(' {0:20}: {1:0.3f}\n'.format('value', kl)) # data format data_format = '(f11.4,3e14.6,i4)' - fid.write(' {0:22}: {1}\n'.format('formatting_string',data_format)) + fid.write(' {0:22}: {1}\n'.format('formatting_string', data_format)) fid.write('\n') + # PURPOSE: print global attributes to YAML header -def print_global(fid,PROC,DREL,MODEL,SLR,S21,month): +def print_global(fid, PROC, DREL, MODEL, SLR, S21, month): fid.write(' {0}:\n'.format('global_attributes')) MISSION = 'GRACE/GRACE-FO' - title = '{0} Geocenter Coefficients {1} {2}'.format(MISSION,PROC,DREL) - fid.write(' {0:22}: {1}\n'.format('title',title)) + title = '{0} Geocenter Coefficients {1} {2}'.format(MISSION, PROC, DREL) + fid.write(' {0:22}: {1}\n'.format('title', title)) summary = [] - summary.append(('Geocenter error coefficients derived from {0} mission ' - 'measurements and {1} ocean model outputs.').format(MISSION,MODEL)) - summary.append((' These coefficients represent the largest-scale ' - 'variability of hydrologic, cryospheric, and solid Earth ' - 'processes. In addition, the coefficients represent the ' - 'atmospheric and oceanic processes not captured in the {0} {1} ' - 'de-aliasing product.').format(MISSION,DREL)) - fid.write(' {0:22}: {1}\n'.format('summary',''.join(summary))) + summary.append( + ( + 'Geocenter error coefficients derived from {0} mission ' + 'measurements and {1} ocean model outputs.' + ).format(MISSION, MODEL) + ) + summary.append( + ( + ' These coefficients represent the largest-scale ' + 'variability of hydrologic, cryospheric, and solid Earth ' + 'processes. In addition, the coefficients represent the ' + 'atmospheric and oceanic processes not captured in the {0} {1} ' + 'de-aliasing product.' + ).format(MISSION, DREL) + ) + fid.write(' {0:22}: {1}\n'.format('summary', ''.join(summary))) project = [] project.append('NASA Gravity Recovery And Climate Experiment (GRACE)') project.append('GRACE Follow-On (GRACE-FO)') if (DREL == 'RL06') else None - fid.write(' {0:22}: {1}\n'.format('project',', '.join(project))) + fid.write(' {0:22}: {1}\n'.format('project', ', '.join(project))) keywords = [] keywords.append('GRACE') keywords.append('GRACE-FO') if (DREL == 'RL06') else None @@ -541,80 +595,122 @@ def print_global(fid,PROC,DREL,MODEL,SLR,S21,month): keywords.append('Time Variable Gravity') keywords.append('Mass Transport') keywords.append('Satellite Geodesy') - fid.write(' {0:22}: {1}\n'.format('keywords',', '.join(keywords))) + fid.write(' {0:22}: {1}\n'.format('keywords', ', '.join(keywords))) vocabulary = 'NASA Global Change Master Directory (GCMD) Science Keywords' - fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary',vocabulary)) + fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary', vocabulary)) hist = '{0} Level-3 Data created at UC Irvine'.format(MISSION) - fid.write(' {0:22}: {1}\n'.format('history',hist)) + fid.write(' {0:22}: {1}\n'.format('history', hist)) src = 'An inversion using {0} measurements and {1} ocean model outputs.' - args = (MISSION,MODEL,DREL) - fid.write(' {0:22}: {1}\n'.format('source',src.format(*args))) + args = (MISSION, MODEL, DREL) + fid.write(' {0:22}: {1}\n'.format('source', src.format(*args))) # fid.write(' {0:22}: {1}\n'.format('platform','GRACE-A, GRACE-B')) # vocabulary = 'NASA Global Change Master Directory platform keywords' # fid.write(' {0:22}: {1}\n'.format('platform_vocabulary',vocabulary)) # fid.write(' {0:22}: {1}\n'.format('instrument','ACC,KBR,GPS,SCA')) # vocabulary = 'NASA Global Change Master Directory instrument keywords' # fid.write(' {0:22}: {1}\n'.format('instrument_vocabulary',vocabulary)) - fid.write(' {0:22}: {1:d}\n'.format('processing_level',3)) + fid.write(' {0:22}: {1:d}\n'.format('processing_level', 3)) ack = [] - ack.append(('Work was supported by an appointment to the NASA Postdoctoral ' - 'Program at NASA Goddard Space Flight Center, administered by ' - 'Universities Space Research Association under contract with NASA')) + ack.append( + ( + 'Work was supported by an appointment to the NASA Postdoctoral ' + 'Program at NASA Goddard Space Flight Center, administered by ' + 'Universities Space Research Association under contract with NASA' + ) + ) ack.append('GRACE is a joint mission of NASA (USA) and DLR (Germany)') - if (DREL == 'RL06'): - ack.append('GRACE-FO is a joint mission of NASA (USA) and GFZ (Germany)') - fid.write(' {0:22}: {1}\n'.format('acknowledgement','. '.join(ack))) + if DREL == 'RL06': + ack.append( + 'GRACE-FO is a joint mission of NASA (USA) and GFZ (Germany)' + ) + fid.write(' {0:22}: {1}\n'.format('acknowledgement', '. '.join(ack))) PRODUCT_VERSION = f'Release-{DREL[2:]}' - fid.write(' {0:22}: {1}\n'.format('product_version',PRODUCT_VERSION)) + fid.write(' {0:22}: {1}\n'.format('product_version', PRODUCT_VERSION)) fid.write(' {0:22}:\n'.format('references')) reference = [] # geocenter citations - reference.append(('T. C. Sutterley, and I. Velicogna, "Improved estimates ' - 'of geocenter variability from time-variable gravity and ocean model ' - 'outputs", Remote Sensing, 11(18), 2108, (2019). ' - 'https://doi.org/10.3390/rs11182108')) - reference.append(('S. C. Swenson, D. P. Chambers, and J. Wahr, "Estimating ' - 'geocenter variations from a combination of GRACE and ocean model ' - 'output", Journal of Geophysical Research - Solid Earth, 113(B08410), ' - '(2008). https://doi.org/10.1029/2007JB005338')) + reference.append( + ( + 'T. C. Sutterley, and I. Velicogna, "Improved estimates ' + 'of geocenter variability from time-variable gravity and ocean model ' + 'outputs", Remote Sensing, 11(18), 2108, (2019). ' + 'https://doi.org/10.3390/rs11182108' + ) + ) + reference.append( + ( + 'S. C. Swenson, D. P. Chambers, and J. Wahr, "Estimating ' + 'geocenter variations from a combination of GRACE and ocean model ' + 'output", Journal of Geophysical Research - Solid Earth, 113(B08410), ' + '(2008). https://doi.org/10.1029/2007JB005338' + ) + ) # ECMWF jump corrections citation - if (DREL == 'RL05'): - reference.append(('E. Fagiolini, F. Flechtner, M. Horwath, H. Dobslaw, ' - '''"Correction of inconsistencies in ECMWF's operational ''' - '''analysis data during de-aliasing of GRACE gravity models", ''' - 'Geophysical Journal International, 202(3), 2150, (2015). ' - 'https://doi.org/10.1093/gji/ggv276')) + if DREL == 'RL05': + reference.append( + ( + 'E. Fagiolini, F. Flechtner, M. Horwath, H. Dobslaw, ' + """"Correction of inconsistencies in ECMWF's operational """ + """analysis data during de-aliasing of GRACE gravity models", """ + 'Geophysical Journal International, 202(3), 2150, (2015). ' + 'https://doi.org/10.1093/gji/ggv276' + ) + ) # SLR citation for a given solution - if (SLR == 'CSR'): - reference.append(('M. Cheng, B. D. Tapley, and J. C. Ries, ' - '''"Deceleration in the Earth's oblateness", Journal of ''' - 'Geophysical Research: Solid Earth, 118(2), 740-747, (2013). ' - 'https://doi.org/10.1002/jgrb.50058')) - elif (SLR == 'GSFC'): - reference.append(('B. D. Loomis, K. E. Rachlin, and S. B. Luthcke, ' - '"Improved Earth Oblateness Rate Reveals Increased Ice Sheet Losses ' - 'and Mass-Driven Sea Level Rise", Geophysical Research Letters, ' - '46(12), 6910-6917, (2019). https://doi.org/10.1029/2019GL082929')) - reference.append(('B. D. Loomis, K. E. Rachlin, D. N. Wiese, ' - 'F. W. Landerer, and S. B. Luthcke, "Replacing GRACE/GRACE-FO C30 ' - 'with satellite laser ranging: Impacts on Antarctic Ice Sheet mass ' - 'change", Geophysical Research Letters, 47(3), (2020). ' - 'https://doi.org/10.1029/2019GL085488')) - elif (SLR == 'GFZ'): - reference.append(('R. Koenig, P. Schreiner, and C. Dahle, "Monthly ' - 'estimates of C(2,0) generated by GFZ from SLR satellites based ' - 'on GFZ GRACE/GRACE-FO RL06 background models." V. 1.0. GFZ Data ' - 'Services, (2019). http://doi.org/10.5880/GFZ.GRAVIS_06_C20_SLR')) - if (S21 == 'CSR'): - reference.append(('M. Cheng, J. C. Ries, and B. D. Tapley, ' - '''"Variations of the Earth's figure axis from satellite laser ''' - 'ranging and GRACE", Journal of Geophysical Research: Solid Earth, ' - '116, B01409, (2011). https://doi.org/10.1029/2010JB000850')) - elif (S21 == 'GFZ'): - reference.append(('C. Dahle and M. Murboeck, "Post-processed ' - 'GRACE/GRACE-FO Geopotential GSM Coefficients GFZ RL06 ' - '(Level-2B Product)." V. 0002. GFZ Data Services, (2019). ' - 'http://doi.org/10.5880/GFZ.GRAVIS_06_L2B')) + if SLR == 'CSR': + reference.append( + ( + 'M. Cheng, B. D. Tapley, and J. C. Ries, ' + """"Deceleration in the Earth's oblateness", Journal of """ + 'Geophysical Research: Solid Earth, 118(2), 740-747, (2013). ' + 'https://doi.org/10.1002/jgrb.50058' + ) + ) + elif SLR == 'GSFC': + reference.append( + ( + 'B. D. Loomis, K. E. Rachlin, and S. B. Luthcke, ' + '"Improved Earth Oblateness Rate Reveals Increased Ice Sheet Losses ' + 'and Mass-Driven Sea Level Rise", Geophysical Research Letters, ' + '46(12), 6910-6917, (2019). https://doi.org/10.1029/2019GL082929' + ) + ) + reference.append( + ( + 'B. D. Loomis, K. E. Rachlin, D. N. Wiese, ' + 'F. W. Landerer, and S. B. Luthcke, "Replacing GRACE/GRACE-FO C30 ' + 'with satellite laser ranging: Impacts on Antarctic Ice Sheet mass ' + 'change", Geophysical Research Letters, 47(3), (2020). ' + 'https://doi.org/10.1029/2019GL085488' + ) + ) + elif SLR == 'GFZ': + reference.append( + ( + 'R. Koenig, P. Schreiner, and C. Dahle, "Monthly ' + 'estimates of C(2,0) generated by GFZ from SLR satellites based ' + 'on GFZ GRACE/GRACE-FO RL06 background models." V. 1.0. GFZ Data ' + 'Services, (2019). http://doi.org/10.5880/GFZ.GRAVIS_06_C20_SLR' + ) + ) + if S21 == 'CSR': + reference.append( + ( + 'M. Cheng, J. C. Ries, and B. D. Tapley, ' + """"Variations of the Earth's figure axis from satellite laser """ + 'ranging and GRACE", Journal of Geophysical Research: Solid Earth, ' + '116, B01409, (2011). https://doi.org/10.1029/2010JB000850' + ) + ) + elif S21 == 'GFZ': + reference.append( + ( + 'C. Dahle and M. Murboeck, "Post-processed ' + 'GRACE/GRACE-FO Geopotential GSM Coefficients GFZ RL06 ' + '(Level-2B Product)." V. 0002. GFZ Data Services, (2019). ' + 'http://doi.org/10.5880/GFZ.GRAVIS_06_L2B' + ) + ) # print list of references for ref in reference: fid.write(' - {0}\n'.format(ref)) @@ -626,19 +722,24 @@ def print_global(fid,PROC,DREL,MODEL,SLR,S21,month): fid.write(' {0:22}: {1}\n'.format('creator_url', url)) fid.write(' {0:22}: {1}\n'.format('creator_type', 'group')) inst = 'University of Washington; University of California, Irvine' - fid.write(' {0:22}: {1}\n'.format('creator_institution',inst)) + fid.write(' {0:22}: {1}\n'.format('creator_institution', inst)) # date range and date created - calendar_year,calendar_month = gravtk.time.grace_to_calendar(month) - start_time = '{0:4.0f}-{1:02.0f}'.format(calendar_year[0],calendar_month[0]) + calendar_year, calendar_month = gravtk.time.grace_to_calendar(month) + start_time = '{0:4.0f}-{1:02.0f}'.format( + calendar_year[0], calendar_month[0] + ) fid.write(' {0:22}: {1}\n'.format('time_coverage_start', start_time)) - end_time = '{0:4.0f}-{1:02.0f}'.format(calendar_year[-1],calendar_month[-1]) + end_time = '{0:4.0f}-{1:02.0f}'.format( + calendar_year[-1], calendar_month[-1] + ) fid.write(' {0:22}: {1}\n'.format('time_coverage_end', end_time)) - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) fid.write(' {0:22}: {1}\n'.format('date_created', today)) fid.write('\n') + # PURPOSE: print variable descriptions to YAML header -def print_variables(fid,data_precision,data_units): +def print_variables(fid, data_precision, data_units): # variables fid.write(' {0}:\n'.format('variables')) # time @@ -681,10 +782,11 @@ def print_variables(fid,data_precision,data_units): # end of header fid.write('\n\n# End of YAML header\n') + # PURPOSE: print a file log for the GRACE degree one analysis def output_log_file(input_arguments, output_files): # format: kernel_degree_one_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'kernel_degree_one_run_{0}_PID-{1:d}.log'.format(*args) DIRECTORY = pathlib.Path(input_arguments.directory).joinpath('geocenter') # create a unique log and open the log file @@ -701,10 +803,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE degree one analysis def output_error_log_file(input_arguments): # format: kernel_degree_one_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'kernel_degree_one_failed_run_{0}_PID-{1:d}.log'.format(*args) DIRECTORY = pathlib.Path(input_arguments.directory).joinpath('geocenter') # create a unique log and open the log file @@ -720,6 +823,7 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -727,132 +831,272 @@ def arguments(): coefficients of degree 2 and greater, and ocean bottom pressure variations from ECCO and OMCT/MPIOM """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') - parser.add_argument('--kl','-k', - type=float, default=0.021, - help='Degree 1 gravitational Load Love number') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) + parser.add_argument( + '--kl', + '-k', + type=float, + default=0.021, + help='Degree 1 gravitational Load Love number', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # correct for pole tide drift follow Wahr et al. (2015) - parser.add_argument('--pole-tide', - default=False, action='store_true', - help='Correct for pole tide drift') + parser.add_argument( + '--pole-tide', + default=False, + action='store_true', + help='Correct for pole tide drift', + ) # replace low degree harmonics with values from Satellite Laser Ranging - parser.add_argument('--slr-c20', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C20 coefficients with SLR values') - parser.add_argument('--slr-21', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C21 and S21 coefficients with SLR values') - parser.add_argument('--slr-22', - type=str, default=None, choices=['CSR','GSFC'], - help='Replace C22 and S22 coefficients with SLR values') - parser.add_argument('--slr-c30', - type=str, default=None, choices=['CSR','GFZ','GSFC','LARES'], - help='Replace C30 coefficients with SLR values') - parser.add_argument('--slr-c40', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C40 coefficients with SLR values') - parser.add_argument('--slr-c50', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C50 coefficients with SLR values') + parser.add_argument( + '--slr-c20', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C20 coefficients with SLR values', + ) + parser.add_argument( + '--slr-21', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C21 and S21 coefficients with SLR values', + ) + parser.add_argument( + '--slr-22', + type=str, + default=None, + choices=['CSR', 'GSFC'], + help='Replace C22 and S22 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c30', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC', 'LARES'], + help='Replace C30 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c40', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C40 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c50', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C50 coefficients with SLR values', + ) # input/output data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # mean file to remove - parser.add_argument('--mean-file', + parser.add_argument( + '--mean-file', type=pathlib.Path, - help='GRACE/GRACE-FO mean file to remove from the harmonic data') + help='GRACE/GRACE-FO mean file to remove from the harmonic data', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--mean-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5','gfc'], - help='Input data format for GRACE/GRACE-FO mean file') + parser.add_argument( + '--mean-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5', 'gfc'], + help='Input data format for GRACE/GRACE-FO mean file', + ) # additional error files to include - parser.add_argument('--error-file', + parser.add_argument( + '--error-file', type=pathlib.Path, - nargs='+', default=[], - help='Additional error files to include in total uncertainty') + nargs='+', + default=[], + help='Additional error files to include in total uncertainty', + ) # run with sea level fingerprints - parser.add_argument('--fingerprint', - default=False, action='store_true', - help='Redistribute land-water flux using sea level fingerprints') + parser.add_argument( + '--fingerprint', + default=False, + action='store_true', + help='Redistribute land-water flux using sea level fingerprints', + ) # Output log file for each job in forms # kernel_degree_one_run_2002-04-01_PID-00000.log # kernel_degree_one_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -888,18 +1132,20 @@ def main(): MEANFORM=args.mean_format, ERROR_FILES=args.error_file, FINGERPRINT=args.fingerprint, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/geocenter/model_degree_one.py b/geocenter/model_degree_one.py index a80c76a3..6e5042a9 100755 --- a/geocenter/model_degree_one.py +++ b/geocenter/model_degree_one.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" model_degree_one.py Written by Tyler Sutterley (07/2026) @@ -153,6 +153,7 @@ Updated 06/2019: added parameter LANDMASK for setting the land-sea mask Written 10/2018 """ + from __future__ import print_function import sys @@ -175,7 +176,8 @@ import matplotlib.offsetbox from matplotlib.ticker import MultipleLocator except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) + # PURPOSE: keep track of threads def info(args): @@ -186,6 +188,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: model the seasonal component of an initial degree 1 model # using preliminary estimates of annual and semi-annual variations from LWM # as calculated in Chen et al. (1999), doi:10.1029/1998JB900019 @@ -210,17 +213,24 @@ def model_seasonal_geocenter(grace_date): SAPz = 75.0 # calculate each geocenter component from the amplitude and phase # converting the phase from degrees to radians - X = AAx*np.sin(2.0*np.pi*grace_date + np.radians(APx)) + \ - SAAx*np.sin(4.0*np.pi*grace_date + np.radians(SAPx)) - Y = AAy*np.sin(2.0*np.pi*grace_date + np.radians(APy)) + \ - SAAy*np.sin(4.0*np.pi*grace_date + np.radians(SAPy)) - Z = AAz*np.sin(2.0*np.pi*grace_date + np.radians(APz)) + \ - SAAz*np.sin(4.0*np.pi*grace_date + np.radians(SAPz)) - DEG1 = gravtk.geocenter(X=X-X.mean(), Y=Y-Y.mean(), Z=Z-Z.mean()) + X = AAx * np.sin( + 2.0 * np.pi * grace_date + np.radians(APx) + ) + SAAx * np.sin(4.0 * np.pi * grace_date + np.radians(SAPx)) + Y = AAy * np.sin( + 2.0 * np.pi * grace_date + np.radians(APy) + ) + SAAy * np.sin(4.0 * np.pi * grace_date + np.radians(SAPy)) + Z = AAz * np.sin( + 2.0 * np.pi * grace_date + np.radians(APz) + ) + SAAz * np.sin(4.0 * np.pi * grace_date + np.radians(SAPz)) + DEG1 = gravtk.geocenter(X=X - X.mean(), Y=Y - Y.mean(), Z=Z - Z.mean()) return DEG1.from_cartesian() + # PURPOSE: calculate a geocenter time-series -def model_degree_one(input_file, LMAX, RAD, +def model_degree_one( + input_file, + LMAX, + RAD, MMAX=None, DESTRIPE=False, LOVE_NUMBERS=0, @@ -235,8 +245,8 @@ def model_degree_one(input_file, LMAX, RAD, EXPANSION=None, LANDMASK=None, PLOT=False, - MODE=0o775): - + MODE=0o775, +): # create output directory if currently non-existent OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -247,8 +257,9 @@ def model_degree_one(input_file, LMAX, RAD, slf_str = 'SLF_' if FINGERPRINT else '' # read load love numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE='CF', FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF', FORMAT='class' + ) # set gravitational load love number to a specific value if LOVE_K1: LOVE.kl[1] = np.copy(LOVE_K1) @@ -258,8 +269,8 @@ def model_degree_one(input_file, LMAX, RAD, # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) - rho_e = factors.rho_e# Average Density of the Earth [g/cm^3] - rad_e = factors.rad_e# Average Radius of the Earth [cm] + rho_e = factors.rho_e # Average Density of the Earth [g/cm^3] + rad_e = factors.rad_e # Average Radius of the Earth [cm] l = factors.l # Factor for converting to Mass SH dfactor = factors.get('cmwe') @@ -267,23 +278,25 @@ def model_degree_one(input_file, LMAX, RAD, # Read Smoothed Ocean and Land Functions # smoothed functions are from the read_ocean_function.py program # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) # degree spacing and grid dimensions # will create spatial fields with same dimensions - dlon,dlat = landsea.spacing + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # spatial parameters in radians dphi = np.radians(dlon) dth = np.radians(dlat) # longitude and colatitude in radians - phi = np.radians(landsea.lon[np.newaxis,:]) + phi = np.radians(landsea.lon[np.newaxis, :]) th = np.radians(90.0 - np.squeeze(landsea.lat)) # create land function - land_function = np.zeros((nlon, nlat),dtype=np.float64) + land_function = np.zeros((nlon, nlat), dtype=np.float64) # extract land function from file # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data.T >= 1) & (landsea.data.T <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data.T >= 1) & (landsea.data.T <= 3)) + land_function[indx, indy] = 1.0 # calculate ocean function from land function ocean_function = 1.0 - land_function @@ -293,15 +306,23 @@ def model_degree_one(input_file, LMAX, RAD, # calculate spherical harmonics of ocean function to degree 1 # mass is equivalent to 1 cm ocean height change # eustatic ratio = -land total/ocean total - ocean_Ylms = gravtk.gen_stokes(ocean_function, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=1, LOVE=LOVE, PLM=PLM[:2,:2,:]) + ocean_Ylms = gravtk.gen_stokes( + ocean_function, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + LOVE=LOVE, + PLM=PLM[:2, :2, :], + ) # Gaussian Smoothing (Jekeli, 1981) - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) # input spherical harmonic datafiles # input spherical harmonic datafile index @@ -311,9 +332,13 @@ def model_degree_one(input_file, LMAX, RAD, # number of files within the index n_files = data_Ylms.shape[-1] # truncate to degree and order - data_Ylms.truncate(lmax=LMAX,mmax=MMAX) + data_Ylms.truncate(lmax=LMAX, mmax=MMAX) # save original geocenter for file in mm w.e. - DATA = gravtk.geocenter().from_harmonics(data_Ylms).scale(10.0*(rho_e*rad_e)) + DATA = ( + gravtk.geocenter() + .from_harmonics(data_Ylms) + .scale(10.0 * (rho_e * rad_e)) + ) DATA.mean(apply=True) # extract date variables if DATE: @@ -330,21 +355,23 @@ def model_degree_one(input_file, LMAX, RAD, # output [m,phi] m = data_Ylms.m # Integration factors (solid angle) - int_fact = np.sin(th)*dphi*dth + int_fact = np.sin(th) * dphi * dth # 4-pi normalization - norm = 1.0/(4.0*np.pi) + norm = 1.0 / (4.0 * np.pi) # calculating cos(m*phi) and sin(m*phi) using Euler's formula - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", m, phi)) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', m, phi)) # Legendre polynomials for degree 1 - P10 = np.squeeze(PLM[1,0,:]) - P11 = np.squeeze(PLM[1,1,:]) + P10 = np.squeeze(PLM[1, 0, :]) + P11 = np.squeeze(PLM[1, 1, :]) # PLM for spherical harmonic degrees 2+ up to LMAX # converted into mass and smoothed if specified - plmout = np.zeros((LMAX+1, MMAX+1, nlat)) + plmout = np.zeros((LMAX + 1, MMAX + 1, nlat)) # convert to smoothed coefficients of mass # Convolving plms with degree dependent factor and smoothing - plmout[:] = np.einsum("l,l,lmh->lmh", dfactor, wt, PLM[:LMAX+1,:MMAX+1,:]) + plmout[:] = np.einsum( + 'l,l,lmh->lmh', dfactor, wt, PLM[: LMAX + 1, : MMAX + 1, :] + ) # Initializing 3x3 I-Parameter matrix # (see equations 12 and 13 of Swenson et al., 2008) @@ -352,21 +379,39 @@ def model_degree_one(input_file, LMAX, RAD, # I-Parameter matrix accounts for the fact that the GRACE data only # includes spherical harmonic degrees greater than or equal to 2 # C10, C11, S11 - PC10 = np.einsum("h...,p...->ph...", P10, m_phi[0,:].real) - PC11 = np.einsum("h...,p...->ph...", P11, m_phi[1,:].real) - PS11 = np.einsum("h...,p...->ph...", P11, m_phi[1,:].imag) + PC10 = np.einsum('h...,p...->ph...', P10, m_phi[0, :].real) + PC11 = np.einsum('h...,p...->ph...', P11, m_phi[1, :].real) + PS11 = np.einsum('h...,p...->ph...', P11, m_phi[1, :].imag) # C10: C10, C11, S11 - IMAT[0,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PC10) - IMAT[1,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PC11) - IMAT[2,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PS11) + IMAT[0, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PC10 + ) + IMAT[1, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PC11 + ) + IMAT[2, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PS11 + ) # C11: C10, C11, S11 - IMAT[0,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PC10) - IMAT[1,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PC11) - IMAT[2,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PS11) + IMAT[0, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PC10 + ) + IMAT[1, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PC11 + ) + IMAT[2, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PS11 + ) # S11: C10, C11, S11 - IMAT[0,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PC10) - IMAT[1,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PC11) - IMAT[2,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PS11) + IMAT[0, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PC10 + ) + IMAT[1, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PC11 + ) + IMAT[2, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PS11 + ) # get seasonal variations of an initial geocenter correction # for use in the land water mass calculation @@ -391,7 +436,7 @@ def model_degree_one(input_file, LMAX, RAD, G.C11 = np.zeros((n_files)) G.S11 = np.zeros((n_files)) # DMAT is the degree one matrix ((C10,C11,S11) x Time) in terms of mass - DMAT = np.zeros((3,n_files)) + DMAT = np.zeros((3, n_files)) # degree 1 iterations iteration = gravtk.geocenter() iteration.C10 = np.zeros((n_files, max_iter)) @@ -403,46 +448,56 @@ def model_degree_one(input_file, LMAX, RAD, # Summing product of plms and c/slms over all SH degrees >= 2 Ylms = data_Ylms.index(t) # subset GRACE to degrees 2+ for calculating ocean mass - l2 = slice(2, LMAX+1) - pconv = np.einsum("lmh...,lm...->mh...", plmout[l2, :, :], Ylms.ilm[l2, :]) + l2 = slice(2, LMAX + 1) + pconv = np.einsum( + 'lmh...,lm...->mh...', plmout[l2, :, :], Ylms.ilm[l2, :] + ) # Multiplying by c/s(phi#m) to get surface density in cmwe (lon,lat) # ccos/ssin are mXphi, pcos/psin are mXtheta: resultant matrices are phiXtheta # The summation over spherical harmonic order is in this multiplication - rmass = np.einsum("mp...,mh...->ph...", m_phi, pconv).real + rmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # calculate G matrix parameters through a summation of each latitude # summation of integration factors, Legendre polynomials, # (convolution of order and harmonics) and the ocean mass at t - G.C10[t] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, rmass) - G.C11[t] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, rmass) - G.S11[t] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, rmass) + G.C10[t] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, rmass + ) + G.C11[t] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, rmass + ) + G.S11[t] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, rmass + ) # calculate degree one solution for each iteration (or single if not) while (eps > eps_max) and (n_iter < max_iter): # for each file for t in range(n_files): # calculate eustatic component (can iterate) - if (n_iter == 0): + if n_iter == 0: # for first iteration (will be only iteration if not ITERATIVE): # seasonal component of geocenter variation for land water - data_Ylms.clm[1,0,t] = seasonal_geocenter.C10[t] - data_Ylms.clm[1,1,t] = seasonal_geocenter.C11[t] - data_Ylms.slm[1,1,t] = seasonal_geocenter.S11[t] + data_Ylms.clm[1, 0, t] = seasonal_geocenter.C10[t] + data_Ylms.clm[1, 1, t] = seasonal_geocenter.C11[t] + data_Ylms.slm[1, 1, t] = seasonal_geocenter.S11[t] else: # for all others: use previous iteration of inversion # for each of the geocenter solutions (C10, C11, S11) - data_Ylms.clm[1,0,t] = iteration.C10[t,n_iter-1] - data_Ylms.clm[1,1,t] = iteration.C11[t,n_iter-1] - data_Ylms.slm[1,1,t] = iteration.S11[t,n_iter-1] + data_Ylms.clm[1, 0, t] = iteration.C10[t, n_iter - 1] + data_Ylms.clm[1, 1, t] = iteration.C11[t, n_iter - 1] + data_Ylms.slm[1, 1, t] = iteration.S11[t, n_iter - 1] # Summing product of plms and c/slms over all SH degrees Ylms = data_Ylms.index(t) - l1 = slice(1, LMAX+1) - pconv = np.einsum("lmh...,lm...->mh...", plmout[l1, :, :], Ylms.ilm[l1, :]) + l1 = slice(1, LMAX + 1) + pconv = np.einsum( + 'lmh...,lm...->mh...', plmout[l1, :, :], Ylms.ilm[l1, :] + ) # Multiplying by c/s(phi#m) to get surface density in cm w.e. (lonxlat) # ccos/ssin are mXphi, pcos/psin are mXtheta: resultant matrices are phiXtheta # The summation over spherical harmonic order is in this multiplication - lmass = np.einsum("mp...,mh...->ph...", m_phi, pconv).real + lmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # use sea level fingerprints or eustatic from land components if FINGERPRINT: @@ -452,40 +507,81 @@ def model_degree_one(input_file, LMAX, RAD, # NOTE: this is an unscaled estimate that uses the # buffered land function when solving the sea-level equation. # possible improvement using scaled estimate with real coastlines - land_Ylms = gravtk.gen_stokes(land_function*lmass, - landsea.lon, landsea.lat, UNITS=1, LMIN=0, LMAX=EXPANSION, - PLM=PLM, LOVE=LOVE) + land_Ylms = gravtk.gen_stokes( + land_function * lmass, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=EXPANSION, + PLM=PLM, + LOVE=LOVE, + ) # 2) calculate sea level fingerprints of land mass at time t # use maximum of 3 iterations for computational efficiency - sea_level = gravtk.sea_level_equation(land_Ylms.clm, land_Ylms.slm, - landsea.lon, landsea.lat, land_function, LMAX=EXPANSION, - LOVE=LOVE, BODY_TIDE_LOVE=0, FLUID_LOVE=0, ITERATIONS=3, - POLAR=True, PLM=PLM, FILL_VALUE=0) + sea_level = gravtk.sea_level_equation( + land_Ylms.clm, + land_Ylms.slm, + landsea.lon, + landsea.lat, + land_function, + LMAX=EXPANSION, + LOVE=LOVE, + BODY_TIDE_LOVE=0, + FLUID_LOVE=0, + ITERATIONS=3, + POLAR=True, + PLM=PLM, + FILL_VALUE=0, + ) # 3) convert sea level fingerprints into spherical harmonics - slf_Ylms = gravtk.gen_stokes(sea_level, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=1, PLM=PLM[:2,:2,:], LOVE=LOVE) + slf_Ylms = gravtk.gen_stokes( + sea_level, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + PLM=PLM[:2, :2, :], + LOVE=LOVE, + ) # 4) convert the slf degree 1 harmonics to mass with dfactor - eustatic = gravtk.geocenter().from_harmonics(slf_Ylms).scale(dfactor[1]) + eustatic = ( + gravtk.geocenter() + .from_harmonics(slf_Ylms) + .scale(dfactor[1]) + ) else: # steps to calculate eustatic component from land-water change: # 1) calculate total mass of 1 cm of ocean height (calculated above) # 2) calculate total land mass at time t (data*land function) # NOTE: possible improvement using the sea-level equation to solve # for the spatial pattern of sea level from the land water mass - land_Ylms = gravtk.gen_stokes(land_function*lmass, - landsea.lon, landsea.lat, UNITS=1, LMIN=0, LMAX=1, - PLM=PLM[:2,:2,:], LOVE=LOVE) + land_Ylms = gravtk.gen_stokes( + land_function * lmass, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + PLM=PLM[:2, :2, :], + LOVE=LOVE, + ) # 3) calculate ratio between the total land mass and the total mass # of 1 cm of ocean height (negative as positive land = sea level drop) # this converts the total land change to ocean height change - eustatic_ratio = -land_Ylms.clm[0,0]/ocean_Ylms.clm[0,0] + eustatic_ratio = -land_Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0] # 4) scale degree one coefficients of ocean function with ratio # and convert the eustatic degree 1 harmonics to mass with dfactor - scale_factor = eustatic_ratio*dfactor[1] - eustatic = gravtk.geocenter().from_harmonics(ocean_Ylms).scale(scale_factor) + scale_factor = eustatic_ratio * dfactor[1] + eustatic = ( + gravtk.geocenter() + .from_harmonics(ocean_Ylms) + .scale(scale_factor) + ) # eustatic coefficients of degree 1 - CMAT = np.array([eustatic.C10,eustatic.C11,eustatic.S11]) + CMAT = np.array([eustatic.C10, eustatic.C11, eustatic.S11]) # G Matrix for time t GMAT = np.array([G.C10[t], G.C11[t], G.S11[t]]) # calculate degree 1 solution for iteration @@ -493,28 +589,41 @@ def model_degree_one(input_file, LMAX, RAD, # whereby the initial degree one coefficients are used to update # the G Matrix until (C10, C11, S11) converge # calculates min(eustatic from land - measured ocean) - if (SOLVER == 'inv'): - DMAT[:,t] = np.dot(np.linalg.inv(IMAT), (CMAT-GMAT)) - elif (SOLVER == 'lstsq'): - DMAT[:,t] = np.linalg.lstsq(IMAT, (CMAT-GMAT), rcond=-1)[0] + if SOLVER == 'inv': + DMAT[:, t] = np.dot(np.linalg.inv(IMAT), (CMAT - GMAT)) + elif SOLVER == 'lstsq': + DMAT[:, t] = np.linalg.lstsq(IMAT, (CMAT - GMAT), rcond=-1)[0] elif SOLVER in ('gelsd', 'gelsy', 'gelss'): - DMAT[:,t], res, rnk, s = scipy.linalg.lstsq(IMAT, (CMAT-GMAT), - lapack_driver=SOLVER) + DMAT[:, t], res, rnk, s = scipy.linalg.lstsq( + IMAT, (CMAT - GMAT), lapack_driver=SOLVER + ) # save geocenter for iteration and time t - iteration.C10[t,n_iter] = DMAT[0,t]/dfactor[1] - iteration.C11[t,n_iter] = DMAT[1,t]/dfactor[1] - iteration.S11[t,n_iter] = DMAT[2,t]/dfactor[1] + iteration.C10[t, n_iter] = DMAT[0, t] / dfactor[1] + iteration.C11[t, n_iter] = DMAT[1, t] / dfactor[1] + iteration.S11[t, n_iter] = DMAT[2, t] / dfactor[1] # remove mean of each solution for iteration - iteration.C10[:,n_iter] -= iteration.C10[:,n_iter].mean() - iteration.C11[:,n_iter] -= iteration.C11[:,n_iter].mean() - iteration.S11[:,n_iter] -= iteration.S11[:,n_iter].mean() + iteration.C10[:, n_iter] -= iteration.C10[:, n_iter].mean() + iteration.C11[:, n_iter] -= iteration.C11[:, n_iter].mean() + iteration.S11[:, n_iter] -= iteration.S11[:, n_iter].mean() # calculate difference between original geocenter coefficients and the # calculated coefficients for each of the geocenter solutions - sigma_C10 = np.sum((data_Ylms.clm[1,0,:] - iteration.C10[:,n_iter])**2) - sigma_C11 = np.sum((data_Ylms.clm[1,1,:] - iteration.C11[:,n_iter])**2) - sigma_S11 = np.sum((data_Ylms.slm[1,1,:] - iteration.S11[:,n_iter])**2) - power = data_Ylms.clm[1,0,t]**2 + data_Ylms.clm[1,1,t]**2 + data_Ylms.slm[1,1,t]**2 - eps = np.sqrt(sigma_C10 + sigma_C11 + sigma_S11)/np.sqrt(np.sum(power)) + sigma_C10 = np.sum( + (data_Ylms.clm[1, 0, :] - iteration.C10[:, n_iter]) ** 2 + ) + sigma_C11 = np.sum( + (data_Ylms.clm[1, 1, :] - iteration.C11[:, n_iter]) ** 2 + ) + sigma_S11 = np.sum( + (data_Ylms.slm[1, 1, :] - iteration.S11[:, n_iter]) ** 2 + ) + power = ( + data_Ylms.clm[1, 0, t] ** 2 + + data_Ylms.clm[1, 1, t] ** 2 + + data_Ylms.slm[1, 1, t] ** 2 + ) + eps = np.sqrt(sigma_C10 + sigma_C11 + sigma_S11) / np.sqrt( + np.sum(power) + ) # add 1 to n_iter counter n_iter += 1 @@ -522,15 +631,15 @@ def model_degree_one(input_file, LMAX, RAD, # for each of the geocenter solutions (C10, C11, S11) # for the iterative case this will be the final iteration DEG1 = gravtk.geocenter() - DEG1.C10,DEG1.C11,DEG1.S11 = DMAT/dfactor[1] + DEG1.C10, DEG1.C11, DEG1.S11 = DMAT / dfactor[1] DEG1.mean(apply=True) # calculate harmonics in mm w.e. - mmwe = DEG1.scale(10.0*(rho_e*rad_e)) + mmwe = DEG1.scale(10.0 * (rho_e * rad_e)) # output degree 1 coefficients # 1: mm water equivalent of recovered # 2: mm water equivalent of actual - args = (FILE_PREFIX,slf_str,iter_str,ds_str) + args = (FILE_PREFIX, slf_str, iter_str, ds_str) FILE1 = OUTPUT_DIRECTORY.joinpath('{0}{1}{2}mmwe{3}.txt'.format(*args)) fid1 = FILE1.open(mode='w', encoding='utf8') output_files.append(FILE1) @@ -538,20 +647,28 @@ def model_degree_one(input_file, LMAX, RAD, fid2 = FILE2.open(mode='w', encoding='utf8') output_files.append(FILE2) # print Swenson file headers - print(" Degree 1 coefficients, mm equivalent water thickness", file=fid1) - print(" Degree 1 coefficients, mm equivalent water thickness", file=fid2) + print(' Degree 1 coefficients, mm equivalent water thickness', file=fid1) + print(' Degree 1 coefficients, mm equivalent water thickness', file=fid2) print(" format='(4f9.2,i9)'", file=fid1) print(" format='(4f9.2,i9)'", file=fid2) - args = ['Time','C10','C11','S11','Month'] + args = ['Time', 'C10', 'C11', 'S11', 'Month'] print(''.join('{:>9}'.format(s) for s in args), file=fid1) print(''.join('{:>9}'.format(s) for s in args), file=fid2) # for each file for t in range(n_files): # output geocenter coefficients to file - print('{0:9.2f}{1:9.2f}{2:9.2f}{3:9.2f} {4:03d}'.format(tdec[t], - mmwe.C10[t], mmwe.C11[t], mmwe.S11[t], mon[t]), file=fid1) - print('{0:9.2f}{1:9.2f}{2:9.2f}{3:9.2f} {4:03d}'.format(tdec[t], - DATA.C10[t], DATA.C11[t], DATA.S11[t], mon[t]), file=fid2) + print( + '{0:9.2f}{1:9.2f}{2:9.2f}{3:9.2f} {4:03d}'.format( + tdec[t], mmwe.C10[t], mmwe.C11[t], mmwe.S11[t], mon[t] + ), + file=fid1, + ) + print( + '{0:9.2f}{1:9.2f}{2:9.2f}{3:9.2f} {4:03d}'.format( + tdec[t], DATA.C10[t], DATA.C11[t], DATA.S11[t], mon[t] + ), + file=fid2, + ) # close the output files fid1.close() fid2.close() @@ -563,8 +680,9 @@ def model_degree_one(input_file, LMAX, RAD, if PLOT: # 3 row plot (C10, C11 and S11) ax = {} - fig, (ax[0], ax[1], ax[2]) = plt.subplots(num=1, nrows=3, sharex=True, - sharey=True, figsize=(6,9)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, nrows=3, sharex=True, sharey=True, figsize=(6, 9) + ) # plot original geocenter ax[0].plot(tdec, DATA.C10, 'b', lw=2) ax[1].plot(tdec, DATA.C11, 'b', lw=2) @@ -578,24 +696,32 @@ def model_degree_one(input_file, LMAX, RAD, ax[1].set_ylabel('[mm]', fontsize=14) ax[2].set_ylabel('[mm]', fontsize=14) ax[2].set_xlabel('Time [Yr]', fontsize=14) - #ax[2].set_xlim(2003,2007) - #ax[2].set_ylim(-6,6) - #ax[2].xaxis.set_ticks(np.arange(2003,2008,1)) - #ax[2].xaxis.set_minor_locator(MultipleLocator(0.25)) - #ax[2].yaxis.set_ticks(np.arange(-6,8,2)) + # ax[2].set_xlim(2003,2007) + # ax[2].set_ylim(-6,6) + # ax[2].xaxis.set_ticks(np.arange(2003,2008,1)) + # ax[2].xaxis.set_minor_locator(MultipleLocator(0.25)) + # ax[2].yaxis.set_ticks(np.arange(-6,8,2)) ax[2].xaxis.get_major_formatter().set_useOffset(False) # add axis labels and adjust font sizes for axis ticks - for i,lbl in enumerate(['C10','C11','S11']): + for i, lbl in enumerate(['C10', 'C11', 'S11']): # axis label - artist = matplotlib.offsetbox.AnchoredText(lbl, pad=0.0, - frameon=False, loc=2, prop=dict(size=16,weight='bold')) + artist = matplotlib.offsetbox.AnchoredText( + lbl, + pad=0.0, + frameon=False, + loc=2, + prop=dict(size=16, weight='bold'), + ) ax[i].add_artist(artist) # axes tick adjustments - ax[i].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[i].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # adjust locations of subplots and save to file - fig.subplots_adjust(left=0.1,right=0.96,bottom=0.06,top=0.98,hspace=0.1) - args = (FILE_PREFIX,slf_str,iter_str,ds_str) + fig.subplots_adjust( + left=0.1, right=0.96, bottom=0.06, top=0.98, hspace=0.1 + ) + args = (FILE_PREFIX, slf_str, iter_str, ds_str) FILE = '{0}{1}{2}Comparison{3}.pdf'.format(*args) PLOT1 = OUTPUT_DIRECTORY.joinpath(FILE) plt.savefig(PLOT1, format='pdf') @@ -608,37 +734,54 @@ def model_degree_one(input_file, LMAX, RAD, if PLOT and ITERATIVE: # 3 row plot (C10, C11 and S11) ax = {} - fig, (ax[0],ax[1],ax[2]) = plt.subplots(num=1, nrows=3, sharex=True, - figsize=(6,9)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, nrows=3, sharex=True, figsize=(6, 9) + ) # show solutions for each iteration - plot_colors = iter(cm.rainbow(np.linspace(0,1,n_iter))) + plot_colors = iter(cm.rainbow(np.linspace(0, 1, n_iter))) for j in range(n_iter): color_j = next(plot_colors) # C10, C11 and S11 - ax[0].plot(mon,10.*(iteration.C10[:,j]*dfactor[1]),color=color_j) - ax[1].plot(mon,10.*(iteration.C11[:,j]*dfactor[1]),color=color_j) - ax[2].plot(mon,10.*(iteration.S11[:,j]*dfactor[1]),color=color_j) + ax[0].plot( + mon, 10.0 * (iteration.C10[:, j] * dfactor[1]), color=color_j + ) + ax[1].plot( + mon, 10.0 * (iteration.C11[:, j] * dfactor[1]), color=color_j + ) + ax[2].plot( + mon, 10.0 * (iteration.S11[:, j] * dfactor[1]), color=color_j + ) # labels and set limits ax[0].set_ylabel('mm', fontsize=14) ax[1].set_ylabel('mm', fontsize=14) ax[2].set_ylabel('mm', fontsize=14) ax[2].set_xlabel('Grace Month', fontsize=14) - ax[2].set_xlim(np.floor(mon[0]/10.)*10.,np.ceil(mon[-1]/10.)*10.) + ax[2].set_xlim( + np.floor(mon[0] / 10.0) * 10.0, np.ceil(mon[-1] / 10.0) * 10.0 + ) ax[2].xaxis.set_minor_locator(MultipleLocator(5)) ax[2].xaxis.get_major_formatter().set_useOffset(False) # add axis labels and adjust font sizes for axis ticks - fig_labels = ['C10','C11','S11'] + fig_labels = ['C10', 'C11', 'S11'] for i in range(3): # axis label - artist = matplotlib.offsetbox.AnchoredText(fig_labels[i], pad=0., - frameon=False, loc=2, prop=dict(size=16,weight='bold')) + artist = matplotlib.offsetbox.AnchoredText( + fig_labels[i], + pad=0.0, + frameon=False, + loc=2, + prop=dict(size=16, weight='bold'), + ) ax[i].add_artist(artist) # axes tick adjustments - ax[i].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[i].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # adjust locations of subplots and save to file - fig.subplots_adjust(left=0.12,right=0.94,bottom=0.06,top=0.98,hspace=0.1) - args = (FILE_PREFIX,slf_str,ds_str) + fig.subplots_adjust( + left=0.12, right=0.94, bottom=0.06, top=0.98, hspace=0.1 + ) + args = (FILE_PREFIX, slf_str, ds_str) FILE = '{0}{1}Geocenter_Iterative{2}.pdf'.format(*args) PLOT2 = OUTPUT_DIRECTORY.joinpath(FILE) plt.savefig(PLOT2, format='pdf') @@ -650,10 +793,11 @@ def model_degree_one(input_file, LMAX, RAD, # return the list of output files and the number of iterations return (output_files, n_iter) + # PURPOSE: print a file log for the model degree one analysis def output_log_file(input_arguments, output_files, n_iter): # format: model_degree_one_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'model_degree_one_run_{0}_PID-{1:d}.log'.format(*args) DIRECTORY = pathlib.Path(input_arguments.output_directory) # create a unique log and open the log file @@ -673,10 +817,11 @@ def output_log_file(input_arguments, output_files, n_iter): # close the log file fid.close() + # PURPOSE: print a error file log for the model degree one analysis def output_error_log_file(input_arguments): # format: model_degree_one_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'model_degree_one_failed_run_{0}_PID-{1:d}.log'.format(*args) DIRECTORY = pathlib.Path(input_arguments.output_directory) # create a unique log and open the log file @@ -692,6 +837,7 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -699,96 +845,162 @@ def arguments(): coefficients of degree 2 and greater for testing the reliability of the algorithm """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', + parser.add_argument( + 'infile', type=pathlib.Path, - help='Input index file with spherical harmonic data files') + help='Input index file with spherical harmonic data files', + ) # output working data directory - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for files') - parser.add_argument('--file-prefix','-P', - type=str, - help='Prefix string for output files') - parser.add_argument('--date','-D', - default=False, action='store_true', - help='Model harmonics are a time series') + help='Output directory for files', + ) + parser.add_argument( + '--file-prefix', '-P', type=str, help='Prefix string for output files' + ) + parser.add_argument( + '--date', + '-D', + default=False, + action='store_true', + help='Model harmonics are a time series', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') - parser.add_argument('--kl','-k', - type=float, default=0.021, - help='Degree 1 gravitational Load Love number') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) + parser.add_argument( + '--kl', + '-k', + type=float, + default=0.021, + help='Degree 1 gravitational Load Love number', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format for ocean models') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format for ocean models', + ) # run with iterative scheme - parser.add_argument('--iterative', - default=False, action='store_true', - help='Iterate degree one solutions') + parser.add_argument( + '--iterative', + default=False, + action='store_true', + help='Iterate degree one solutions', + ) # least squares solver - choices = ('inv','lstsq','gelsd', 'gelsy', 'gelss') - parser.add_argument('--solver','-s', - type=str, default='lstsq', choices=choices, - help='Least squares solver for degree one solutions') + choices = ('inv', 'lstsq', 'gelsd', 'gelsy', 'gelss') + parser.add_argument( + '--solver', + '-s', + type=str, + default='lstsq', + choices=choices, + help='Least squares solver for degree one solutions', + ) # run with sea level fingerprints - parser.add_argument('--fingerprint', - default=False, action='store_true', - help='Redistribute land-water flux using sea level fingerprints') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--fingerprint', + default=False, + action='store_true', + help='Redistribute land-water flux using sea level fingerprints', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # land-sea mask for calculating ocean mass and land water flux - land_mask_file = gravtk.utilities.get_data_path(['data','land_fcn_300km.nc']) - parser.add_argument('--mask', + land_mask_file = gravtk.utilities.get_data_path( + ['data', 'land_fcn_300km.nc'] + ) + parser.add_argument( + '--mask', type=pathlib.Path, default=land_mask_file, - help='Land-sea mask for calculating ocean mass and land water flux') + help='Land-sea mask for calculating ocean mass and land water flux', + ) # create output plots - parser.add_argument('--plot','-p', - default=False, action='store_true', - help='Create output plots for components and iterations') + parser.add_argument( + '--plot', + '-p', + default=False, + action='store_true', + help='Create output plots for components and iterations', + ) # Output log file for each job in forms # model_degree_one_run_2002-04-01_PID-00000.log # model_degree_one_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -798,7 +1010,7 @@ def main(): try: info(args) # run model_degree_one algorithm with parameters - output_files,n_iter = model_degree_one( + output_files, n_iter = model_degree_one( args.infile, args.lmax, args.radius, @@ -816,18 +1028,20 @@ def main(): EXPANSION=args.expansion, LANDMASK=args.mask, PLOT=args.plot, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files,n_iter) + if args.log: # write successful job completion log file + output_log_file(args, output_files, n_iter) + # run main program if __name__ == '__main__': diff --git a/geocenter/monte_carlo_degree_one.py b/geocenter/monte_carlo_degree_one.py index 7e6501fc..b87f2f22 100644 --- a/geocenter/monte_carlo_degree_one.py +++ b/geocenter/monte_carlo_degree_one.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" monte_carlo_degree_one.py Written by Tyler Sutterley (07/2026) @@ -211,6 +211,7 @@ output all monte carlo iterations to a single netCDF4 file Written 11/2018 """ + from __future__ import print_function import sys @@ -233,6 +234,7 @@ ticker = gravtk.utilities.import_dependency('matplotlib.ticker') netCDF4 = gravtk.utilities.import_dependency('netCDF4') + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -242,6 +244,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: model the seasonal component of an initial degree 1 model # using preliminary estimates of annual and semi-annual variations from LWM # as calculated in Chen et al. (1999), doi:10.1029/1998JB900019 @@ -266,17 +269,26 @@ def model_seasonal_geocenter(grace_date): SAPz = 75.0 # calculate each geocenter component from the amplitude and phase # converting the phase from degrees to radians - X = AAx*np.sin(2.0*np.pi*grace_date + np.radians(APx)) + \ - SAAx*np.sin(4.0*np.pi*grace_date + np.radians(SAPx)) - Y = AAy*np.sin(2.0*np.pi*grace_date + np.radians(APy)) + \ - SAAy*np.sin(4.0*np.pi*grace_date + np.radians(SAPy)) - Z = AAz*np.sin(2.0*np.pi*grace_date + np.radians(APz)) + \ - SAAz*np.sin(4.0*np.pi*grace_date + np.radians(SAPz)) - DEG1 = gravtk.geocenter(X=X-X.mean(), Y=Y-Y.mean(), Z=Z-Z.mean()) + X = AAx * np.sin( + 2.0 * np.pi * grace_date + np.radians(APx) + ) + SAAx * np.sin(4.0 * np.pi * grace_date + np.radians(SAPx)) + Y = AAy * np.sin( + 2.0 * np.pi * grace_date + np.radians(APy) + ) + SAAy * np.sin(4.0 * np.pi * grace_date + np.radians(SAPy)) + Z = AAz * np.sin( + 2.0 * np.pi * grace_date + np.radians(APz) + ) + SAAz * np.sin(4.0 * np.pi * grace_date + np.radians(SAPz)) + DEG1 = gravtk.geocenter(X=X - X.mean(), Y=Y - Y.mean(), Z=Z - Z.mean()) return DEG1.from_cartesian() + # PURPOSE: calculate the satellite error for a geocenter time-series -def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, +def monte_carlo_degree_one( + base_dir, + PROC, + DREL, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -307,8 +319,8 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, EXPANSION=None, LANDMASK=None, PLOT=False, - MODE=0o775): - + MODE=0o775, +): # GRACE/GRACE-FO dataset DSET = 'GSM' # do not import degree 1 coefficients @@ -324,7 +336,6 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, attributes['title'] = f'{MISSION} Geocenter Coefficients' attributes['solver'] = SOLVER - # delta coefficients flag for monte carlo run delta_str = '_monte_carlo' # output string for both LMAX==MMAX and LMAX != MMAX cases @@ -336,31 +347,31 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # output flag for using sea level fingerprints slf_str = '_SLF' if FINGERPRINT else '' # output flag for low-degree harmonic replacements - if SLR_21 in ('CSR','GFZ','GSFC'): + if SLR_21 in ('CSR', 'GFZ', 'GSFC'): C21_str = f'_w{SLR_21}_21' else: C21_str = '' - if SLR_22 in ('CSR','GSFC'): + if SLR_22 in ('CSR', 'GSFC'): C22_str = f'_w{SLR_22}_22' else: C22_str = '' if SLR_C30 in ('GSFC',): # C30 replacement now default for all solutions C30_str = '' - elif SLR_C30 in ('CSR','GFZ','LARES'): + elif SLR_C30 in ('CSR', 'GFZ', 'LARES'): C30_str = f'_w{SLR_C30}_C30' else: C30_str = '' - if SLR_C40 in ('CSR','GSFC','LARES'): + if SLR_C40 in ('CSR', 'GSFC', 'LARES'): C40_str = f'_w{SLR_C40}_C40' else: C40_str = '' - if SLR_C50 in ('CSR','GSFC','LARES'): + if SLR_C50 in ('CSR', 'GSFC', 'LARES'): C50_str = f'_w{SLR_C50}_C50' else: C50_str = '' # combine satellite laser ranging flags - slr_str = ''.join([C21_str,C22_str,C30_str,C40_str,C50_str]) + slr_str = ''.join([C21_str, C22_str, C30_str, C40_str, C50_str]) # suffix for input ascii, netcdf and HDF5 files suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') @@ -370,9 +381,9 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, output_files = [] # read load love numbers - LOVE = gravtk.load_love_numbers(EXPANSION, - LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF', - FORMAT='class') + LOVE = gravtk.load_love_numbers( + EXPANSION, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE='CF', FORMAT='class' + ) # add attributes for earth model and love numbers attributes['earth_model'] = LOVE.model attributes['earth_love_numbers'] = LOVE.citation @@ -390,8 +401,8 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) - rho_e = factors.rho_e# Average Density of the Earth [g/cm^3] - rad_e = factors.rad_e# Average Radius of the Earth [cm] + rho_e = factors.rho_e # Average Density of the Earth [g/cm^3] + rad_e = factors.rad_e # Average Radius of the Earth [cm] l = factors.l # Factor for converting to Mass SH dfactor = factors.get('cmwe') @@ -403,24 +414,25 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # Read Smoothed Ocean and Land Functions # smoothed functions are from the read_ocean_function.py program # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) # degree spacing and grid dimensions # will create GRACE spatial fields with same dimensions - dlon,dlat = landsea.spacing + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # spatial parameters in radians dphi = np.radians(dlon) dth = np.radians(dlat) # longitude and colatitude in radians - phi = np.radians(landsea.lon[np.newaxis,:]) + phi = np.radians(landsea.lon[np.newaxis, :]) th = np.radians(90.0 - np.squeeze(landsea.lat)) # create land function - land_function = np.zeros((nlon, nlat),dtype=np.float64) + land_function = np.zeros((nlon, nlat), dtype=np.float64) # extract land function from file # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data.T >= 1) & (landsea.data.T <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data.T >= 1) & (landsea.data.T <= 3)) + land_function[indx, indy] = 1.0 # calculate ocean function from land function ocean_function = 1.0 - land_function @@ -430,25 +442,50 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # calculate spherical harmonics of ocean function to degree 1 # mass is equivalent to 1 cm ocean height change # eustatic ratio = -land total/ocean total - ocean_Ylms = gravtk.gen_stokes(ocean_function, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=1, LOVE=LOVE, PLM=PLM[:2,:2,:]) + ocean_Ylms = gravtk.gen_stokes( + ocean_function, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + LOVE=LOVE, + PLM=PLM[:2, :2, :], + ) # Gaussian Smoothing (Jekeli, 1981) - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) attributes['smoothing_radius'] = f'{RAD:0.0f} km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) # reading GRACE months for input date range # replacing low-degree harmonics with SLR values if specified # correcting for Pole-Tide drift if specified # atmospheric jumps will be corrected externally if specified - Ylms = gravtk.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_C40=SLR_C40, SLR_C50=SLR_C50, - POLE_TIDE=POLE_TIDE, ATM=False, MODEL_DEG1=False) + Ylms = gravtk.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_C40=SLR_C40, + SLR_C50=SLR_C50, + POLE_TIDE=POLE_TIDE, + ATM=False, + MODEL_DEG1=False, + ) # create harmonics object from GRACE/GRACE-FO data GSM_Ylms = gravtk.harmonics().from_dict(Ylms) # add attributes for input GRACE/GRACE-FO spherical harmonics @@ -457,8 +494,9 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # 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 = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GSM_Ylms.subtract(mean_Ylms) attributes['lineage'].append(MEAN_FILE.name) @@ -501,9 +539,9 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, ATM_Ylms.time[:] = np.copy(GSM_Ylms.time) ATM_Ylms.month[:] = np.copy(GSM_Ylms.month) if ATM: - atm_corr = gravtk.read_ecmwf_corrections(base_dir,LMAX,ATM_Ylms.month) - ATM_Ylms.clm[:,:,:] = np.copy(atm_corr['clm']) - ATM_Ylms.slm[:,:,:] = np.copy(atm_corr['slm']) + atm_corr = gravtk.read_ecmwf_corrections(base_dir, LMAX, ATM_Ylms.month) + ATM_Ylms.clm[:, :, :] = np.copy(atm_corr['clm']) + ATM_Ylms.slm[:, :, :] = np.copy(atm_corr['slm']) # removing the mean of the atmospheric jump correction coefficients ATM_Ylms.mean(apply=True) # truncate to degree and order LMAX/MMAX @@ -519,22 +557,24 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, 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) + 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'): + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - Ylms = gravtk.harmonics().from_file(REMOVE_FILE, - format=REMOVEFORM) + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) attributes['lineage'].append(Ylms.filename) - elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'): + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,removeform = REMOVEFORM.split('-') + _, removeform = REMOVEFORM.split('-') # index containing files in data format - Ylms = gravtk.harmonics().from_index(REMOVE_FILE, - format=removeform) + Ylms = gravtk.harmonics().from_index( + REMOVE_FILE, format=removeform + ) attributes['lineage'].extend([f.name for f in Ylms.filename]) # reduce to GRACE/GRACE-FO months and truncate to degree and order Ylms = Ylms.subset(GSM_Ylms.month).truncate(lmax=LMAX, mmax=MMAX) @@ -544,14 +584,14 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, 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] + 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 + 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] + 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() @@ -573,8 +613,18 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # calculating GRACE/GRACE-FO error (Wahr et al. 2006) # output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) - fargs = (PROC,DREL,DSET,LMAX,order_str,ds_str,atm_str,GSM_Ylms.month[0], - GSM_Ylms.month[-1], suffix[DATAFORM]) + fargs = ( + PROC, + DREL, + DSET, + LMAX, + order_str, + ds_str, + atm_str, + GSM_Ylms.month[0], + GSM_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 = GSM_Ylms.directory.joinpath(delta_format.format(*fargs)) # check full path of the GRACE directory for delta file @@ -586,33 +636,34 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # Delta coefficients of GRACE time series (Error components) delta_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - delta_Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - delta_Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + 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'))): + 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 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 + 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']): + for cs, csharm in enumerate(['clm', 'slm']): # calculate GRACE Error (Noise of smoothed time-series) # With Annual and Semi-Annual Terms val1 = getattr(GSM_Ylms, csharm) - smth = gravtk.time_series.smooth(tdec, val1[l,m,:], - HFWTH=HFWTH) + smth = gravtk.time_series.smooth( + tdec, val1[l, m, :], HFWTH=HFWTH + ) # number of smoothed points nsmth = len(smth['data']) tsmth = np.mean(smth['time']) # GRACE/GRACE-FO delta Ylms # variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) - val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth) + val2[l, m] = np.sqrt(np.sum(smth['noise'] ** 2) / nsmth) # attributes for output files attrs = {} @@ -628,8 +679,7 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, output_files.append(DELTA_FILE) else: # read GRACE/GRACE-FO delta harmonics from file - delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, - format=DATAFORM) + delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, format=DATAFORM) # 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) @@ -639,21 +689,23 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # output [m,phi] m = GSM_Ylms.m # Integration factors (solid angle) - int_fact = np.sin(th)*dphi*dth + int_fact = np.sin(th) * dphi * dth # 4-pi normalization - norm = 1.0/(4.0*np.pi) + norm = 1.0 / (4.0 * np.pi) # calculating cos(m*phi) and sin(m*phi) using Euler's formula - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", m, phi)) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', m, phi)) # Legendre polynomials for degree 1 - P10 = np.squeeze(PLM[1,0,:]) - P11 = np.squeeze(PLM[1,1,:]) + P10 = np.squeeze(PLM[1, 0, :]) + P11 = np.squeeze(PLM[1, 1, :]) # PLM for spherical harmonic degrees 2+ up to LMAX # converted into mass and smoothed if specified - plmout = np.zeros((LMAX+1, MMAX+1, nlat)) + plmout = np.zeros((LMAX + 1, MMAX + 1, nlat)) # convert to smoothed coefficients of mass # Convolving plms with degree dependent factor and smoothing - plmout[:] = np.einsum("l,l,lmh->lmh", dfactor, wt, PLM[:LMAX+1,:MMAX+1,:]) + plmout[:] = np.einsum( + 'l,l,lmh->lmh', dfactor, wt, PLM[: LMAX + 1, : MMAX + 1, :] + ) # Initializing 3x3 I-Parameter matrix # (see equations 12 and 13 of Swenson et al., 2008) @@ -661,21 +713,39 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # I-Parameter matrix accounts for the fact that the GRACE data only # includes spherical harmonic degrees greater than or equal to 2 # C10, C11, S11 - PC10 = np.einsum("h...,p...->ph...", P10, m_phi[0,:].real) - PC11 = np.einsum("h...,p...->ph...", P11, m_phi[1,:].real) - PS11 = np.einsum("h...,p...->ph...", P11, m_phi[1,:].imag) + PC10 = np.einsum('h...,p...->ph...', P10, m_phi[0, :].real) + PC11 = np.einsum('h...,p...->ph...', P11, m_phi[1, :].real) + PS11 = np.einsum('h...,p...->ph...', P11, m_phi[1, :].imag) # C10: C10, C11, S11 - IMAT[0,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PC10) - IMAT[1,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PC11) - IMAT[2,0] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, PS11) + IMAT[0, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PC10 + ) + IMAT[1, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PC11 + ) + IMAT[2, 0] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC10, ocean_function, PS11 + ) # C11: C10, C11, S11 - IMAT[0,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PC10) - IMAT[1,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PC11) - IMAT[2,1] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, PS11) + IMAT[0, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PC10 + ) + IMAT[1, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PC11 + ) + IMAT[2, 1] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PC11, ocean_function, PS11 + ) # S11: C10, C11, S11 - IMAT[0,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PC10) - IMAT[1,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PC11) - IMAT[2,2] = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, PS11) + IMAT[0, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PC10 + ) + IMAT[1, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PC11 + ) + IMAT[2, 2] = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', int_fact, PS11, ocean_function, PS11 + ) # get seasonal variations of an initial geocenter correction # for use in the land water mass calculation @@ -683,21 +753,29 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # degree 1 iterations for each monte carlo run iteration = gravtk.geocenter() - iteration.C10 = np.zeros((n_files,RUNS)) - iteration.C11 = np.zeros((n_files,RUNS)) - iteration.S11 = np.zeros((n_files,RUNS)) + iteration.C10 = np.zeros((n_files, RUNS)) + iteration.C11 = np.zeros((n_files, RUNS)) + iteration.S11 = np.zeros((n_files, RUNS)) # for each monte carlo iteration for n_iter in range(0, RUNS): # calculate non-iterated terms for each file (G-matrix parameters) for t in range(n_files): # calculate uncertainty for time t and each degree/order Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - Ylms.clm = (1.0-2.0*np.random.rand(LMAX+1,MMAX+1))*delta_Ylms.clm - Ylms.slm = (1.0-2.0*np.random.rand(LMAX+1,MMAX+1))*delta_Ylms.slm + Ylms.clm = ( + 1.0 - 2.0 * np.random.rand(LMAX + 1, MMAX + 1) + ) * delta_Ylms.clm + Ylms.slm = ( + 1.0 - 2.0 * np.random.rand(LMAX + 1, MMAX + 1) + ) * delta_Ylms.slm # add additional uncertainty terms for eYlms in error_Ylms: - Ylms.clm += (1.0-2.0*np.random.rand(LMAX+1,MMAX+1))*eYlms.clm - Ylms.slm += (1.0-2.0*np.random.rand(LMAX+1,MMAX+1))*eYlms.slm + Ylms.clm += ( + 1.0 - 2.0 * np.random.rand(LMAX + 1, MMAX + 1) + ) * eYlms.clm + Ylms.slm += ( + 1.0 - 2.0 * np.random.rand(LMAX + 1, MMAX + 1) + ) * eYlms.slm # Removing monthly GIA signal, atmospheric correction # and the auxiliary coefficients @@ -714,23 +792,43 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, G.C11 = 0.0 G.S11 = 0.0 # subset GRACE to degrees 2+ for calculating ocean mass - l2 = slice(2, LMAX+1) - pconv = np.einsum("lmh...,lm...->mh...", plmout[l2, :, :], GRACE_Ylms.ilm[l2, :]) + l2 = slice(2, LMAX + 1) + pconv = np.einsum( + 'lmh...,lm...->mh...', plmout[l2, :, :], GRACE_Ylms.ilm[l2, :] + ) # Multiplying by c/s(phi#m) to get surface density in cmwe (lon,lat) # ccos/ssin are mXphi, pcos/psin are mXtheta: resultant matrices are phiXtheta # The summation over spherical harmonic order is in this multiplication - rmass = np.einsum("mp...,mh...->ph...", m_phi, pconv).real + rmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # calculate G matrix parameters through a summation of each latitude # summation of integration factors, Legendre polynomials, # (convolution of order and harmonics) and the ocean mass at t - G.C10 = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC10, ocean_function, rmass) - G.C11 = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PC11, ocean_function, rmass) - G.S11 = norm*np.einsum("h...,ph...,ph...,ph...->...", int_fact, PS11, ocean_function, rmass) + G.C10 = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', + int_fact, + PC10, + ocean_function, + rmass, + ) + G.C11 = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', + int_fact, + PC11, + ocean_function, + rmass, + ) + G.S11 = norm * np.einsum( + 'h...,ph...,ph...,ph...->...', + int_fact, + PS11, + ocean_function, + rmass, + ) # seasonal component of geocenter variation for land water - GSM_Ylms.clm[1,0,t] = seasonal_geocenter.C10[t] - GSM_Ylms.clm[1,1,t] = seasonal_geocenter.C11[t] - GSM_Ylms.slm[1,1,t] = seasonal_geocenter.S11[t] + GSM_Ylms.clm[1, 0, t] = seasonal_geocenter.C10[t] + GSM_Ylms.clm[1, 1, t] = seasonal_geocenter.C11[t] + GSM_Ylms.slm[1, 1, t] = seasonal_geocenter.S11[t] # Removing monthly GIA signal, atmospheric correction # and the auxiliary coefficients GRACE_Ylms = GSM_Ylms.index(t) @@ -742,13 +840,15 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # for land water: use an initial seasonal geocenter estimate # from Chen et al. (1999) then the iterative if specified - l1 = slice(1, LMAX+1) - pconv = np.einsum("lmh...,lm...->mh...", plmout[l1, :, :], GRACE_Ylms.ilm[l1, :]) + l1 = slice(1, LMAX + 1) + pconv = np.einsum( + 'lmh...,lm...->mh...', plmout[l1, :, :], GRACE_Ylms.ilm[l1, :] + ) # Multiplying by c/s(phi#m) to get surface density in cm w.e. (lonxlat) # ccos/ssin are mXphi, pcos/psin are mXtheta: resultant matrices are phiXtheta # The summation over spherical harmonic order is in this multiplication - lmass = np.einsum("mp...,mh...->ph...", m_phi, pconv).real + lmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # use sea level fingerprints or eustatic from GRACE land components if FINGERPRINT: @@ -758,70 +858,113 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # NOTE: this is an unscaled GRACE estimate that uses the # buffered land function when solving the sea-level equation. # possible improvement using scaled estimate with real coastlines - land_Ylms = gravtk.gen_stokes(land_function*lmass, - landsea.lon, landsea.lat, UNITS=1, LMIN=0, - LMAX=EXPANSION, LOVE=LOVE) + land_Ylms = gravtk.gen_stokes( + land_function * lmass, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=EXPANSION, + LOVE=LOVE, + ) # 2) calculate sea level fingerprints of land mass at time t # use maximum of 3 iterations for computational efficiency - sea_level = gravtk.sea_level_equation(land_Ylms.clm, land_Ylms.slm, - landsea.lon, landsea.lat, land_function, LMAX=EXPANSION, - LOVE=LOVE, BODY_TIDE_LOVE=0, FLUID_LOVE=0, ITERATIONS=3, - POLAR=True, FILL_VALUE=0) + sea_level = gravtk.sea_level_equation( + land_Ylms.clm, + land_Ylms.slm, + landsea.lon, + landsea.lat, + land_function, + LMAX=EXPANSION, + LOVE=LOVE, + BODY_TIDE_LOVE=0, + FLUID_LOVE=0, + ITERATIONS=3, + POLAR=True, + FILL_VALUE=0, + ) # 3) convert sea level fingerprints into spherical harmonics - slf_Ylms = gravtk.gen_stokes(sea_level, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=1, PLM=PLM[:2,:2,:], LOVE=LOVE) + slf_Ylms = gravtk.gen_stokes( + sea_level, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + PLM=PLM[:2, :2, :], + LOVE=LOVE, + ) # 4) convert the slf degree 1 harmonics to mass with dfactor - eustatic = gravtk.geocenter().from_harmonics(slf_Ylms).scale(dfactor[1]) + eustatic = ( + gravtk.geocenter() + .from_harmonics(slf_Ylms) + .scale(dfactor[1]) + ) else: # steps to calculate eustatic component from GRACE land-water change: # 1) calculate total mass of 1 cm of ocean height (calculated above) # 2) calculate total land mass at time t (GRACE*land function) # NOTE: possible improvement using the sea-level equation to solve # for the spatial pattern of sea level from the land water mass - land_Ylms = gravtk.gen_stokes(lmass*land_function, - landsea.lon, landsea.lat, UNITS=1, LMIN=0, LMAX=1, - PLM=PLM[:2,:2,:], LOVE=LOVE) + land_Ylms = gravtk.gen_stokes( + lmass * land_function, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=1, + PLM=PLM[:2, :2, :], + LOVE=LOVE, + ) # 3) calculate ratio between the total land mass and the total mass # of 1 cm of ocean height (negative as positive land = sea level drop) # this converts the total land change to ocean height change - eustatic_ratio = -land_Ylms.clm[0,0]/ocean_Ylms.clm[0,0] + eustatic_ratio = -land_Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0] # 4) scale degree one coefficients of ocean function with ratio # and convert the eustatic degree 1 harmonics to mass with dfactor - scale_factor = eustatic_ratio*dfactor[1] - eustatic = gravtk.geocenter().from_harmonics(ocean_Ylms).scale(scale_factor) + scale_factor = eustatic_ratio * dfactor[1] + eustatic = ( + gravtk.geocenter() + .from_harmonics(ocean_Ylms) + .scale(scale_factor) + ) # eustatic coefficients of degree 1 - CMAT = np.array([eustatic.C10,eustatic.C11,eustatic.S11]) + CMAT = np.array([eustatic.C10, eustatic.C11, eustatic.S11]) # G Matrix for time t GMAT = np.array([G.C10, G.C11, G.S11]) # calculate degree 1 solution for iteration # this is mathematically equivalent to an iterative procedure # whereby the initial degree one coefficients are used to update # the G Matrix until (C10, C11, S11) converge - if (SOLVER == 'inv'): - DMAT = np.dot(np.linalg.inv(IMAT), (CMAT-GMAT)) - elif (SOLVER == 'lstsq'): - DMAT = np.linalg.lstsq(IMAT, (CMAT-GMAT), rcond=-1)[0] + if SOLVER == 'inv': + DMAT = np.dot(np.linalg.inv(IMAT), (CMAT - GMAT)) + elif SOLVER == 'lstsq': + DMAT = np.linalg.lstsq(IMAT, (CMAT - GMAT), rcond=-1)[0] elif SOLVER in ('gelsd', 'gelsy', 'gelss'): - DMAT, res, rnk, s = scipy.linalg.lstsq(IMAT, (CMAT-GMAT), - lapack_driver=SOLVER) + DMAT, res, rnk, s = scipy.linalg.lstsq( + IMAT, (CMAT - GMAT), lapack_driver=SOLVER + ) # save geocenter for iteration and time t after restoring fields - iteration.C10[t,n_iter] = DMAT[0]/dfactor[1] + \ - gia.C10[t] + atm.C10[t] + remove.C10[t] - iteration.C11[t,n_iter] = DMAT[1]/dfactor[1] + \ - gia.C11[t] + atm.C11[t] + remove.C11[t] - iteration.S11[t,n_iter] = DMAT[2]/dfactor[1] + \ - gia.S11[t] + atm.S11[t] + remove.S11[t] + iteration.C10[t, n_iter] = ( + DMAT[0] / dfactor[1] + gia.C10[t] + atm.C10[t] + remove.C10[t] + ) + iteration.C11[t, n_iter] = ( + DMAT[1] / dfactor[1] + gia.C11[t] + atm.C11[t] + remove.C11[t] + ) + iteration.S11[t, n_iter] = ( + DMAT[2] / dfactor[1] + gia.S11[t] + atm.S11[t] + remove.S11[t] + ) # remove mean of each solution for iteration - iteration.C10[:,n_iter] -= iteration.C10[:,n_iter].mean() - iteration.C11[:,n_iter] -= iteration.C11[:,n_iter].mean() - iteration.S11[:,n_iter] -= iteration.S11[:,n_iter].mean() + iteration.C10[:, n_iter] -= iteration.C10[:, n_iter].mean() + iteration.C11[:, n_iter] -= iteration.C11[:, n_iter].mean() + iteration.S11[:, n_iter] -= iteration.S11[:, n_iter].mean() # calculate mean degree one time series through all iterations MEAN = gravtk.geocenter() - MEAN.C10 = np.mean(iteration.C10,axis=1) - MEAN.C11 = np.mean(iteration.C11,axis=1) - MEAN.S11 = np.mean(iteration.S11,axis=1) + MEAN.C10 = np.mean(iteration.C10, axis=1) + MEAN.C11 = np.mean(iteration.C11, axis=1) + MEAN.S11 = np.mean(iteration.S11, axis=1) # calculate RMS off of mean time series RMS = gravtk.geocenter() @@ -829,37 +972,62 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, RMS.C11 = np.zeros((n_files)) RMS.S11 = np.zeros((n_files)) for t in range(n_files): - RMS.C10[t] = np.sqrt(np.sum((iteration.C10[t,:]-MEAN.C10[t])**2)/RUNS) - RMS.C11[t] = np.sqrt(np.sum((iteration.C11[t,:]-MEAN.C11[t])**2)/RUNS) - RMS.S11[t] = np.sqrt(np.sum((iteration.S11[t,:]-MEAN.S11[t])**2)/RUNS) + RMS.C10[t] = np.sqrt( + np.sum((iteration.C10[t, :] - MEAN.C10[t]) ** 2) / RUNS + ) + RMS.C11[t] = np.sqrt( + np.sum((iteration.C11[t, :] - MEAN.C11[t]) ** 2) / RUNS + ) + RMS.S11[t] = np.sqrt( + np.sum((iteration.S11[t, :] - MEAN.S11[t]) ** 2) / RUNS + ) # Convert inverted solutions into fully normalized spherical harmonics # for each of the geocenter solutions (C10, C11, S11) - DEG1 = MEAN.scale(1.0/dfactor[1]) + DEG1 = MEAN.scale(1.0 / dfactor[1]) # convert estimated monte carlo errors into fully normalized harmonics - ERROR = RMS.scale(1.0/dfactor[1]) + ERROR = RMS.scale(1.0 / dfactor[1]) # output degree 1 coefficients file_format = '{0}_{1}_{2}{3}{4}{5}{6}{7}.{8}' - output_format = ('{0:11.4f}{1:14.6e}{2:14.6e}{3:14.6e}' - '{4:14.6e}{5:14.6e}{6:14.6e} {7:03d}\n') + output_format = ( + '{0:11.4f}{1:14.6e}{2:14.6e}{3:14.6e}' + '{4:14.6e}{5:14.6e}{6:14.6e} {7:03d}\n' + ) # public file format in fully normalized spherical harmonics # local version with all descriptor flags - a1=(PROC,DREL,model_str,slf_str,'',gia_str,delta_str,ds_str,'txt') + a1 = (PROC, DREL, model_str, slf_str, '', gia_str, delta_str, ds_str, 'txt') FILE1 = DIRECTORY.joinpath(file_format.format(*a1)) fid1 = FILE1.open(mode='w', encoding='utf8') # print headers for cases with and without dealiasing print_header(fid1) - print_harmonic(fid1,LOVE.kl[1]) - print_global(fid1,PROC,DREL,model_str.replace('_',' '),GIA_Ylms_rate, - SLR_C20,SLR_21,months) - print_variables(fid1,'single precision','fully normalized') + print_harmonic(fid1, LOVE.kl[1]) + print_global( + fid1, + PROC, + DREL, + model_str.replace('_', ' '), + GIA_Ylms_rate, + SLR_C20, + SLR_21, + months, + ) + print_variables(fid1, 'single precision', 'fully normalized') # for each GRACE/GRACE-FO month - for t,mon in enumerate(months): + for t, mon in enumerate(months): # output geocenter coefficients to file - fid1.write(output_format.format(tdec[t], - DEG1.C10[t],DEG1.C11[t],DEG1.S11[t], - ERROR.C10[t],ERROR.C11[t],ERROR.S11[t],mon)) + fid1.write( + output_format.format( + tdec[t], + DEG1.C10[t], + DEG1.C11[t], + DEG1.S11[t], + ERROR.C10[t], + ERROR.C11[t], + ERROR.S11[t], + mon, + ) + ) # close the output file fid1.close() # set the permissions mode of the output file @@ -867,9 +1035,9 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, output_files.append(FILE1) # output all degree 1 coefficients as a netCDF4 file - a2=(PROC,DREL,model_str,slf_str,'',gia_str,delta_str,ds_str,'nc') + a2 = (PROC, DREL, model_str, slf_str, '', gia_str, delta_str, ds_str, 'nc') FILE2 = DIRECTORY.joinpath(file_format.format(*a2)) - fileID = netCDF4.Dataset(FILE2, mode='w', format="NETCDF4") + fileID = netCDF4.Dataset(FILE2, mode='w', format='NETCDF4') # Defining the NetCDF4 dimensions fileID.createDimension('run', RUNS) fileID.createDimension('time', n_files) @@ -899,23 +1067,30 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, nc['time'][:] = tdec[:].copy() nc['month'][:] = months[:].copy() # set attributes for time and month - for key in ('time','month'): + for key in ('time', 'month'): for att_name, att_val in attrs[key].items(): nc[key].setncattr(att_name, att_val) # degree 1 coefficients from the monte carlo solution for key in iteration.fields: var = iteration.get(key) - nc[key] = fileID.createVariable(key, var.dtype, - ('time','run',), zlib=True) - nc[key][:] = var[:,:]/dfactor[1] + nc[key] = fileID.createVariable( + key, + var.dtype, + ( + 'time', + 'run', + ), + zlib=True, + ) + nc[key][:] = var[:, :] / dfactor[1] for att_name, att_val in attrs[key].items(): nc[key].setncattr(att_name, att_val) # define global attributes for att_name, att_val in attributes.items(): fileID.setncattr(att_name, att_val) - fileID.date_created = time.strftime('%Y-%m-%d',time.localtime()) + fileID.date_created = time.strftime('%Y-%m-%d', time.localtime()) # close the output file fileID.close() # set the permissions mode of the output file @@ -926,39 +1101,51 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, if PLOT: # 3 row plot (C10, C11 and S11) ax = {} - fig,(ax[0],ax[1],ax[2])=plt.subplots(nrows=3,sharex=True,figsize=(6,9)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + nrows=3, sharex=True, figsize=(6, 9) + ) # show solutions for each iteration - plot_colors = iter(cm.rainbow(np.linspace(0,1,RUNS))) + plot_colors = iter(cm.rainbow(np.linspace(0, 1, RUNS))) for j in range(n_iter): color_j = next(plot_colors) # C10, C11 and S11 - ax[0].plot(months,10.0*iteration.C10[:,j],color=color_j) - ax[1].plot(months,10.0*iteration.C11[:,j],color=color_j) - ax[2].plot(months,10.0*iteration.S11[:,j],color=color_j) + ax[0].plot(months, 10.0 * iteration.C10[:, j], color=color_j) + ax[1].plot(months, 10.0 * iteration.C11[:, j], color=color_j) + ax[2].plot(months, 10.0 * iteration.S11[:, j], color=color_j) # mean C10, C11 and S11 - ax[0].plot(months,10.0*MEAN.C10,color='k',lw=1.5) - ax[1].plot(months,10.0*MEAN.C11,color='k',lw=1.5) - ax[2].plot(months,10.0*MEAN.S11,color='k',lw=1.5) + ax[0].plot(months, 10.0 * MEAN.C10, color='k', lw=1.5) + ax[1].plot(months, 10.0 * MEAN.C11, color='k', lw=1.5) + ax[2].plot(months, 10.0 * MEAN.S11, color='k', lw=1.5) # labels and set limits ax[0].set_ylabel('mm', fontsize=14) ax[1].set_ylabel('mm', fontsize=14) ax[2].set_ylabel('mm', fontsize=14) ax[2].set_xlabel('Grace Month', fontsize=14) - ax[2].set_xlim(np.floor(months[0]/10.)*10.,np.ceil(months[-1]/10.)*10.) + ax[2].set_xlim( + np.floor(months[0] / 10.0) * 10.0, np.ceil(months[-1] / 10.0) * 10.0 + ) ax[2].xaxis.set_minor_locator(ticker.MultipleLocator(5)) ax[2].xaxis.get_major_formatter().set_useOffset(False) # add axis labels and adjust font sizes for axis ticks - for i,lbl in enumerate(['C10','C11','S11']): + for i, lbl in enumerate(['C10', 'C11', 'S11']): # axis label - artist = offsetbox.AnchoredText(lbl, pad=0.0, - frameon=False, loc=2, prop=dict(size=16,weight='bold')) + artist = offsetbox.AnchoredText( + lbl, + pad=0.0, + frameon=False, + loc=2, + prop=dict(size=16, weight='bold'), + ) ax[i].add_artist(artist) # axes tick adjustments - ax[i].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[i].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # adjust locations of subplots and save to file - fig.subplots_adjust(left=0.12,right=0.94,bottom=0.06,top=0.98,hspace=0.1) - args = (PROC,DREL,model_str,ds_str) + fig.subplots_adjust( + left=0.12, right=0.94, bottom=0.06, top=0.98, hspace=0.1 + ) + args = (PROC, DREL, model_str, ds_str) FILE = 'Geocenter_Monte_Carlo_{0}_{1}_{2}{3}.pdf'.format(*args) PLOT1 = DIRECTORY.joinpath(FILE) plt.savefig(PLOT1, format='pdf') @@ -970,55 +1157,74 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # return the list of output files return output_files + # PURPOSE: print YAML header to top of file def print_header(fid): # print header fid.write('{0}:\n'.format('header')) # data dimensions fid.write(' {0}:\n'.format('dimensions')) - fid.write(' {0:22}: {1:d}\n'.format('degree',1)) - fid.write(' {0:22}: {1:d}\n'.format('order',1)) + fid.write(' {0:22}: {1:d}\n'.format('degree', 1)) + fid.write(' {0:22}: {1:d}\n'.format('order', 1)) fid.write('\n') + # PURPOSE: print spherical harmonic attributes to YAML header -def print_harmonic(fid,kl): +def print_harmonic(fid, kl): # non-standard attributes fid.write(' {0}:\n'.format('non-standard_attributes')) # load love number fid.write(' {0:22}:\n'.format('love_number')) long_name = 'Gravitational Load Love Number of Degree 1 (k1)' - fid.write(' {0:20}: {1}\n'.format('long_name',long_name)) - fid.write(' {0:20}: {1:0.3f}\n'.format('value',kl)) + fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) + fid.write(' {0:20}: {1:0.3f}\n'.format('value', kl)) # data format data_format = '(f11.4,3e14.6,i4)' - fid.write(' {0:22}: {1}\n'.format('formatting_string',data_format)) + fid.write(' {0:22}: {1}\n'.format('formatting_string', data_format)) fid.write('\n') + # PURPOSE: print global attributes to YAML header -def print_global(fid,PROC,DREL,MODEL,GIA,SLR,S21,month): +def print_global(fid, PROC, DREL, MODEL, GIA, SLR, S21, month): fid.write(' {0}:\n'.format('global_attributes')) MISSION = 'GRACE/GRACE-FO' - title = '{0} Geocenter Coefficients {1} {2}'.format(MISSION,PROC,DREL) - fid.write(' {0:22}: {1}\n'.format('title',title)) + title = '{0} Geocenter Coefficients {1} {2}'.format(MISSION, PROC, DREL) + fid.write(' {0:22}: {1}\n'.format('title', title)) summary = [] - summary.append(('Geocenter coefficients derived from {0} mission ' - 'measurements and {1} ocean model outputs.').format(MISSION,MODEL)) - summary.append((' These coefficients represent the largest-scale ' - 'variability of hydrologic, cryospheric, and solid Earth ' - 'processes. In addition, the coefficients represent the ' - 'atmospheric and oceanic processes not captured in the {0} {1} ' - 'de-aliasing product.').format(MISSION,DREL)) + summary.append( + ( + 'Geocenter coefficients derived from {0} mission ' + 'measurements and {1} ocean model outputs.' + ).format(MISSION, MODEL) + ) + summary.append( + ( + ' These coefficients represent the largest-scale ' + 'variability of hydrologic, cryospheric, and solid Earth ' + 'processes. In addition, the coefficients represent the ' + 'atmospheric and oceanic processes not captured in the {0} {1} ' + 'de-aliasing product.' + ).format(MISSION, DREL) + ) # get GIA parameters - summary.append((' Glacial Isostatic Adjustment (GIA) estimates from ' - '{0} have been restored.').format(GIA.citation)) - if (DREL == 'RL05'): - summary.append((' ECMWF corrections from Fagiolini et al. (2015) have ' - 'been restored.')) - fid.write(' {0:22}: {1}\n'.format('summary',''.join(summary))) + summary.append( + ( + ' Glacial Isostatic Adjustment (GIA) estimates from ' + '{0} have been restored.' + ).format(GIA.citation) + ) + if DREL == 'RL05': + summary.append( + ( + ' ECMWF corrections from Fagiolini et al. (2015) have ' + 'been restored.' + ) + ) + fid.write(' {0:22}: {1}\n'.format('summary', ''.join(summary))) project = [] project.append('NASA Gravity Recovery And Climate Experiment (GRACE)') project.append('GRACE Follow-On (GRACE-FO)') if (DREL == 'RL06') else None - fid.write(' {0:22}: {1}\n'.format('project',', '.join(project))) + fid.write(' {0:22}: {1}\n'.format('project', ', '.join(project))) keywords = [] keywords.append('GRACE') keywords.append('GRACE-FO') if (DREL == 'RL06') else None @@ -1029,82 +1235,124 @@ def print_global(fid,PROC,DREL,MODEL,GIA,SLR,S21,month): keywords.append('Time Variable Gravity') keywords.append('Mass Transport') keywords.append('Satellite Geodesy') - fid.write(' {0:22}: {1}\n'.format('keywords',', '.join(keywords))) + fid.write(' {0:22}: {1}\n'.format('keywords', ', '.join(keywords))) vocabulary = 'NASA Global Change Master Directory (GCMD) Science Keywords' - fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary',vocabulary)) + fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary', vocabulary)) hist = '{0} Level-3 Data created at UC Irvine'.format(MISSION) - fid.write(' {0:22}: {1}\n'.format('history',hist)) + fid.write(' {0:22}: {1}\n'.format('history', hist)) src = 'An inversion using {0} measurements and {1} ocean model outputs.' - args = (MISSION,MODEL,DREL) - fid.write(' {0:22}: {1}\n'.format('source',src.format(*args))) + args = (MISSION, MODEL, DREL) + fid.write(' {0:22}: {1}\n'.format('source', src.format(*args))) # fid.write(' {0:22}: {1}\n'.format('platform','GRACE-A, GRACE-B')) # vocabulary = 'NASA Global Change Master Directory platform keywords' # fid.write(' {0:22}: {1}\n'.format('platform_vocabulary',vocabulary)) # fid.write(' {0:22}: {1}\n'.format('instrument','ACC,KBR,GPS,SCA')) # vocabulary = 'NASA Global Change Master Directory instrument keywords' # fid.write(' {0:22}: {1}\n'.format('instrument_vocabulary',vocabulary)) - fid.write(' {0:22}: {1:d}\n'.format('processing_level',3)) + fid.write(' {0:22}: {1:d}\n'.format('processing_level', 3)) ack = [] - ack.append(('Work was supported by an appointment to the NASA Postdoctoral ' - 'Program at NASA Goddard Space Flight Center, administered by ' - 'Universities Space Research Association under contract with NASA')) + ack.append( + ( + 'Work was supported by an appointment to the NASA Postdoctoral ' + 'Program at NASA Goddard Space Flight Center, administered by ' + 'Universities Space Research Association under contract with NASA' + ) + ) ack.append('GRACE is a joint mission of NASA (USA) and DLR (Germany)') - if (DREL == 'RL06'): - ack.append('GRACE-FO is a joint mission of NASA (USA) and GFZ (Germany)') - fid.write(' {0:22}: {1}\n'.format('acknowledgement','. '.join(ack))) + if DREL == 'RL06': + ack.append( + 'GRACE-FO is a joint mission of NASA (USA) and GFZ (Germany)' + ) + fid.write(' {0:22}: {1}\n'.format('acknowledgement', '. '.join(ack))) PRODUCT_VERSION = f'Release-{DREL[2:]}' - fid.write(' {0:22}: {1}\n'.format('product_version',PRODUCT_VERSION)) + fid.write(' {0:22}: {1}\n'.format('product_version', PRODUCT_VERSION)) fid.write(' {0:22}:\n'.format('references')) reference = [] # geocenter citations - reference.append(('T. C. Sutterley, and I. Velicogna, "Improved estimates ' - 'of geocenter variability from time-variable gravity and ocean model ' - 'outputs", Remote Sensing, 11(18), 2108, (2019). ' - 'https://doi.org/10.3390/rs11182108')) - reference.append(('S. C. Swenson, D. P. Chambers, and J. Wahr, "Estimating ' - 'geocenter variations from a combination of GRACE and ocean model ' - 'output", Journal of Geophysical Research - Solid Earth, 113(B08410), ' - '(2008). https://doi.org/10.1029/2007JB005338')) + reference.append( + ( + 'T. C. Sutterley, and I. Velicogna, "Improved estimates ' + 'of geocenter variability from time-variable gravity and ocean model ' + 'outputs", Remote Sensing, 11(18), 2108, (2019). ' + 'https://doi.org/10.3390/rs11182108' + ) + ) + reference.append( + ( + 'S. C. Swenson, D. P. Chambers, and J. Wahr, "Estimating ' + 'geocenter variations from a combination of GRACE and ocean model ' + 'output", Journal of Geophysical Research - Solid Earth, 113(B08410), ' + '(2008). https://doi.org/10.1029/2007JB005338' + ) + ) # GIA citation reference.append(GIA.reference) # ECMWF jump corrections citation - if (DREL == 'RL05'): - reference.append(('E. Fagiolini, F. Flechtner, M. Horwath, H. Dobslaw, ' - '''"Correction of inconsistencies in ECMWF's operational ''' - '''analysis data during de-aliasing of GRACE gravity models", ''' - 'Geophysical Journal International, 202(3), 2150, (2015). ' - 'https://doi.org/10.1093/gji/ggv276')) + if DREL == 'RL05': + reference.append( + ( + 'E. Fagiolini, F. Flechtner, M. Horwath, H. Dobslaw, ' + """"Correction of inconsistencies in ECMWF's operational """ + """analysis data during de-aliasing of GRACE gravity models", """ + 'Geophysical Journal International, 202(3), 2150, (2015). ' + 'https://doi.org/10.1093/gji/ggv276' + ) + ) # SLR citation for a given solution - if (SLR == 'CSR'): - reference.append(('M. Cheng, B. D. Tapley, and J. C. Ries, ' - '''"Deceleration in the Earth's oblateness", Journal of ''' - 'Geophysical Research: Solid Earth, 118(2), 740-747, (2013). ' - 'https://doi.org/10.1002/jgrb.50058')) - elif (SLR == 'GSFC'): - reference.append(('B. D. Loomis, K. E. Rachlin, and S. B. Luthcke, ' - '"Improved Earth Oblateness Rate Reveals Increased Ice Sheet Losses ' - 'and Mass-Driven Sea Level Rise", Geophysical Research Letters, ' - '46(12), 6910-6917, (2019). https://doi.org/10.1029/2019GL082929')) - reference.append(('B. D. Loomis, K. E. Rachlin, D. N. Wiese, ' - 'F. W. Landerer, and S. B. Luthcke, "Replacing GRACE/GRACE-FO C30 ' - 'with satellite laser ranging: Impacts on Antarctic Ice Sheet mass ' - 'change", Geophysical Research Letters, 47(3), (2020). ' - 'https://doi.org/10.1029/2019GL085488')) - elif (SLR == 'GFZ'): - reference.append(('R. Koenig, P. Schreiner, and C. Dahle, "Monthly ' - 'estimates of C(2,0) generated by GFZ from SLR satellites based ' - 'on GFZ GRACE/GRACE-FO RL06 background models." V. 1.0. GFZ Data ' - 'Services, (2019). http://doi.org/10.5880/GFZ.GRAVIS_06_C20_SLR')) - if (S21 == 'CSR'): - reference.append(('M. Cheng, J. C. Ries, and B. D. Tapley, ' - '''"Variations of the Earth's figure axis from satellite laser ''' - 'ranging and GRACE", Journal of Geophysical Research: Solid Earth, ' - '116, B01409, (2011). https://doi.org/10.1029/2010JB000850')) - elif (S21 == 'GFZ'): - reference.append(('C. Dahle and M. Murboeck, "Post-processed ' - 'GRACE/GRACE-FO Geopotential GSM Coefficients GFZ RL06 ' - '(Level-2B Product)." V. 0002. GFZ Data Services, (2019). ' - 'http://doi.org/10.5880/GFZ.GRAVIS_06_L2B')) + if SLR == 'CSR': + reference.append( + ( + 'M. Cheng, B. D. Tapley, and J. C. Ries, ' + """"Deceleration in the Earth's oblateness", Journal of """ + 'Geophysical Research: Solid Earth, 118(2), 740-747, (2013). ' + 'https://doi.org/10.1002/jgrb.50058' + ) + ) + elif SLR == 'GSFC': + reference.append( + ( + 'B. D. Loomis, K. E. Rachlin, and S. B. Luthcke, ' + '"Improved Earth Oblateness Rate Reveals Increased Ice Sheet Losses ' + 'and Mass-Driven Sea Level Rise", Geophysical Research Letters, ' + '46(12), 6910-6917, (2019). https://doi.org/10.1029/2019GL082929' + ) + ) + reference.append( + ( + 'B. D. Loomis, K. E. Rachlin, D. N. Wiese, ' + 'F. W. Landerer, and S. B. Luthcke, "Replacing GRACE/GRACE-FO C30 ' + 'with satellite laser ranging: Impacts on Antarctic Ice Sheet mass ' + 'change", Geophysical Research Letters, 47(3), (2020). ' + 'https://doi.org/10.1029/2019GL085488' + ) + ) + elif SLR == 'GFZ': + reference.append( + ( + 'R. Koenig, P. Schreiner, and C. Dahle, "Monthly ' + 'estimates of C(2,0) generated by GFZ from SLR satellites based ' + 'on GFZ GRACE/GRACE-FO RL06 background models." V. 1.0. GFZ Data ' + 'Services, (2019). http://doi.org/10.5880/GFZ.GRAVIS_06_C20_SLR' + ) + ) + if S21 == 'CSR': + reference.append( + ( + 'M. Cheng, J. C. Ries, and B. D. Tapley, ' + """"Variations of the Earth's figure axis from satellite laser """ + 'ranging and GRACE", Journal of Geophysical Research: Solid Earth, ' + '116, B01409, (2011). https://doi.org/10.1029/2010JB000850' + ) + ) + elif S21 == 'GFZ': + reference.append( + ( + 'C. Dahle and M. Murboeck, "Post-processed ' + 'GRACE/GRACE-FO Geopotential GSM Coefficients GFZ RL06 ' + '(Level-2B Product)." V. 0002. GFZ Data Services, (2019). ' + 'http://doi.org/10.5880/GFZ.GRAVIS_06_L2B' + ) + ) # print list of references for ref in reference: fid.write(' - {0}\n'.format(ref)) @@ -1116,19 +1364,24 @@ def print_global(fid,PROC,DREL,MODEL,GIA,SLR,S21,month): fid.write(' {0:22}: {1}\n'.format('creator_url', url)) fid.write(' {0:22}: {1}\n'.format('creator_type', 'group')) inst = 'University of Washington; University of California, Irvine' - fid.write(' {0:22}: {1}\n'.format('creator_institution',inst)) + fid.write(' {0:22}: {1}\n'.format('creator_institution', inst)) # date range and date created - calendar_year,calendar_month = gravtk.time.grace_to_calendar(month) - start_time = '{0:4.0f}-{1:02.0f}'.format(calendar_year[0],calendar_month[0]) + calendar_year, calendar_month = gravtk.time.grace_to_calendar(month) + start_time = '{0:4.0f}-{1:02.0f}'.format( + calendar_year[0], calendar_month[0] + ) fid.write(' {0:22}: {1}\n'.format('time_coverage_start', start_time)) - end_time = '{0:4.0f}-{1:02.0f}'.format(calendar_year[-1],calendar_month[-1]) + end_time = '{0:4.0f}-{1:02.0f}'.format( + calendar_year[-1], calendar_month[-1] + ) fid.write(' {0:22}: {1}\n'.format('time_coverage_end', end_time)) - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) fid.write(' {0:22}: {1}\n'.format('date_created', today)) fid.write('\n') + # PURPOSE: print variable descriptions to YAML header -def print_variables(fid,data_precision,data_units): +def print_variables(fid, data_precision, data_units): # variables fid.write(' {0}:\n'.format('variables')) # time @@ -1192,10 +1445,11 @@ def print_variables(fid,data_precision,data_units): # end of header fid.write('\n\n# End of YAML header\n') + # PURPOSE: print a file log for the GRACE degree one analysis def output_log_file(input_arguments, output_files): # format: monte_carlo_degree_one_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'monte_carlo_degree_one_run_{0}_PID-{1:d}.log'.format(*args) DIRECTORY = pathlib.Path(input_arguments.directory).joinpath('geocenter') # create a unique log and open the log file @@ -1214,11 +1468,14 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE degree one analysis def output_error_log_file(input_arguments): # format: monte_carlo_degree_one_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) - LOGFILE = 'monte_carlo_degree_one_failed_run_{0}_PID-{1:d}.log'.format(*args) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) + LOGFILE = 'monte_carlo_degree_one_failed_run_{0}_PID-{1:d}.log'.format( + *args + ) DIRECTORY = pathlib.Path(input_arguments.directory).joinpath('geocenter') # create a unique log and open the log file fid = gravtk.utilities.create_unique_file(DIRECTORY.joinpath(LOGFILE)) @@ -1233,6 +1490,7 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -1240,66 +1498,150 @@ def arguments(): coefficients of degree 2 and greater, and ocean bottom pressure variations from OMCT/MPIOM in a Monte Carlo scheme """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # number of monte carlo iterations - parser.add_argument('--runs', - type=int, default=10000, - help='Number of Monte Carlo iterations') + parser.add_argument( + '--runs', + type=int, + default=10000, + help='Number of Monte Carlo iterations', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') - parser.add_argument('--kl','-k', - type=float, default=0.021, - help='Degree 1 gravitational Load Love number') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) + parser.add_argument( + '--kl', + '-k', + type=float, + default=0.021, + help='Degree 1 gravitational Load Love number', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -1315,114 +1657,205 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # correct for pole tide drift follow Wahr et al. (2015) - parser.add_argument('--pole-tide', - default=False, action='store_true', - help='Correct for pole tide drift') + parser.add_argument( + '--pole-tide', + default=False, + action='store_true', + help='Correct for pole tide drift', + ) # replace low degree harmonics with values from Satellite Laser Ranging - parser.add_argument('--slr-c20', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C20 coefficients with SLR values') - parser.add_argument('--slr-21', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C21 and S21 coefficients with SLR values') - parser.add_argument('--slr-22', - type=str, default=None, choices=['CSR','GSFC'], - help='Replace C22 and S22 coefficients with SLR values') - parser.add_argument('--slr-c30', - type=str, default=None, choices=['CSR','GFZ','GSFC','LARES'], - help='Replace C30 coefficients with SLR values') - parser.add_argument('--slr-c40', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C40 coefficients with SLR values') - parser.add_argument('--slr-c50', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C50 coefficients with SLR values') + parser.add_argument( + '--slr-c20', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C20 coefficients with SLR values', + ) + parser.add_argument( + '--slr-21', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C21 and S21 coefficients with SLR values', + ) + parser.add_argument( + '--slr-22', + type=str, + default=None, + choices=['CSR', 'GSFC'], + help='Replace C22 and S22 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c30', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC', 'LARES'], + help='Replace C30 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c40', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C40 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c50', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C50 coefficients with SLR values', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/Output data format for delta harmonics file') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/Output data format for delta harmonics file', + ) # mean file to remove - parser.add_argument('--mean-file', + parser.add_argument( + '--mean-file', type=pathlib.Path, - help='GRACE/GRACE-FO mean file to remove from the harmonic data') + help='GRACE/GRACE-FO mean file to remove from the harmonic data', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--mean-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5','gfc'], - help='Input data format for GRACE/GRACE-FO mean file') + parser.add_argument( + '--mean-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5', 'gfc'], + help='Input data format for GRACE/GRACE-FO mean file', + ) # monthly files to be removed from the GRACE/GRACE-FO data - parser.add_argument('--remove-file', - type=pathlib.Path, nargs='+', - help='Monthly files to be removed from the GRACE/GRACE-FO data') + parser.add_argument( + '--remove-file', + type=pathlib.Path, + nargs='+', + help='Monthly files to be removed from the GRACE/GRACE-FO data', + ) choices = [] - choices.extend(['ascii','netCDF4','HDF5']) - choices.extend(['index-ascii','index-netCDF4','index-HDF5']) - parser.add_argument('--remove-format', - type=str, nargs='+', choices=choices, - help='Input data format for files to be removed') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + choices.extend(['ascii', 'netCDF4', 'HDF5']) + choices.extend(['index-ascii', 'index-netCDF4', 'index-HDF5']) + parser.add_argument( + '--remove-format', + type=str, + nargs='+', + choices=choices, + help='Input data format for files to be removed', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # additional error files to be used in the monte carlo run - parser.add_argument('--error-file', + parser.add_argument( + '--error-file', type=pathlib.Path, - nargs='+', default=[], - help='Additional error files to use in Monte Carlo analysis') + nargs='+', + default=[], + help='Additional error files to use in Monte Carlo analysis', + ) # least squares solver - choices = ('inv','lstsq','gelsd', 'gelsy', 'gelss') - parser.add_argument('--solver','-s', - type=str, default='lstsq', choices=choices, - help='Least squares solver for degree one solutions') + choices = ('inv', 'lstsq', 'gelsd', 'gelsy', 'gelss') + parser.add_argument( + '--solver', + '-s', + type=str, + default='lstsq', + choices=choices, + help='Least squares solver for degree one solutions', + ) # run with sea level fingerprints - parser.add_argument('--fingerprint', - default=False, action='store_true', - help='Redistribute land-water flux using sea level fingerprints') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--fingerprint', + default=False, + action='store_true', + help='Redistribute land-water flux using sea level fingerprints', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # land-sea mask for calculating ocean mass and land water flux - land_mask_file = gravtk.utilities.get_data_path(['data','land_fcn_300km.nc']) - parser.add_argument('--mask', + land_mask_file = gravtk.utilities.get_data_path( + ['data', 'land_fcn_300km.nc'] + ) + parser.add_argument( + '--mask', type=pathlib.Path, default=land_mask_file, - help='Land-sea mask for calculating ocean mass and land water flux') + help='Land-sea mask for calculating ocean mass and land water flux', + ) # create output plots - parser.add_argument('--plot','-p', - default=False, action='store_true', - help='Create output plots for Monte Carlo iterations') + parser.add_argument( + '--plot', + '-p', + default=False, + action='store_true', + help='Create output plots for Monte Carlo iterations', + ) # Output log file for each job in forms # monte_carlo_degree_one_run_2002-04-01_PID-00000.log # monte_carlo_degree_one_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -1468,18 +1901,20 @@ def main(): EXPANSION=args.expansion, LANDMASK=args.mask, PLOT=args.plot, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/gravity_toolkit/SLR/C20.py b/gravity_toolkit/SLR/C20.py index 48369361..2f56ffed 100644 --- a/gravity_toolkit/SLR/C20.py +++ b/gravity_toolkit/SLR/C20.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" C20.py Written by Tyler Sutterley (05/2023) @@ -105,11 +105,13 @@ Will accommodate upcoming GRACE RL05, which will use different SLR files Written 12/2011 """ + import re import pathlib import numpy as np import gravity_toolkit.time + # PURPOSE: read oblateness data from Satellite Laser Ranging (SLR) def C20(SLR_file, AOD=True, HEADER=True): r""" @@ -145,7 +147,7 @@ def C20(SLR_file, AOD=True, HEADER=True): # output dictionary with data variables dinput = {} # determine if imported file is from PO.DAAC or CSR - if bool(re.search(r'C20_RL\d+', SLR_file.name ,re.I)): + if bool(re.search(r'C20_RL\d+', SLR_file.name, re.I)): # SLR C20 file from CSR # Just for checking new months when TN series isn't up to date as the # SLR estimates always use the full set of days in each calendar month. @@ -157,8 +159,16 @@ def C20(SLR_file, AOD=True, HEADER=True): # Column 5: Mean value of Atmosphere-Ocean De-aliasing model (1E-10) # Columns 6-7: Start and end dates of data used in solution dtype = {} - dtype['names'] = ('time','C20','delta','sigma','AOD','start','end') - dtype['formats'] = ('f','f8','f','f','f','f','f') + dtype['names'] = ( + 'time', + 'C20', + 'delta', + 'sigma', + 'AOD', + 'start', + 'end', + ) + dtype['formats'] = ('f', 'f8', 'f', 'f', 'f', 'f', 'f') # header text is commented and won't be read file_input = np.loadtxt(SLR_file, dtype=dtype) # date and GRACE/GRACE-FO month @@ -167,13 +177,13 @@ def C20(SLR_file, AOD=True, HEADER=True): # monthly spherical harmonic replacement solutions dinput['data'] = file_input['C20'].copy() # monthly spherical harmonic formal standard deviations - dinput['error'] = file_input['sigma']*1e-10 + dinput['error'] = file_input['sigma'] * 1e-10 # Background gravity model includes solid earth and ocean tides, solid # earth and ocean pole tides, and the Atmosphere-Ocean De-aliasing # product. The monthly mean of the AOD model has been restored. if AOD: # Removing AOD product that was restored in the solution - dinput['data'] -= file_input['AOD']*1e-10 + dinput['data'] -= file_input['AOD'] * 1e-10 elif bool(re.search(r'GFZ_(RL\d+)_C20_SLR', SLR_file.name, re.I)): # SLR C20 file from GFZ # Column 1: MJD of BEGINNING of solution span @@ -192,7 +202,7 @@ def C20(SLR_file, AOD=True, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line)) + HEADER = not bool(re.match(r'PRODUCT:+', line)) # add 1 to counter count += 1 @@ -200,7 +210,7 @@ def C20(SLR_file, AOD=True, HEADER=True): n_mon = file_lines - count # date and GRACE/GRACE-FO month dinput['time'] = np.zeros((n_mon)) - dinput['month'] = np.zeros((n_mon),dtype=np.int64) + dinput['month'] = np.zeros((n_mon), dtype=np.int64) # monthly spherical harmonic replacement solutions dinput['data'] = np.zeros((n_mon)) # monthly spherical harmonic formal standard deviations @@ -211,21 +221,22 @@ def C20(SLR_file, AOD=True, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) # check if line has G* or Gm flags - if bool(re.search(r'(G\*|Gm)',line)): + if bool(re.search(r'(G\*|Gm)', line)): # reading decimal year for start of span dinput['time'][t] = np.float64(line_contents[1]) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[2]) - dinput['error'][t] = np.float64(line_contents[4])*1e-10 + dinput['error'][t] = np.float64(line_contents[4]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] elif bool(re.search(r'GRAVIS-2B_GFZOP', SLR_file.name, re.I)): @@ -247,7 +258,7 @@ def C20(SLR_file, AOD=True, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line)) + HEADER = not bool(re.match(r'PRODUCT:+', line)) # add 1 to counter count += 1 @@ -266,22 +277,23 @@ def C20(SLR_file, AOD=True, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) count = len(line_contents) # check for empty lines - if (count > 0): + if count > 0: # reading decimal year for start of span dinput['time'][t] = np.float64(line_contents[1]) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[2]) - dinput['error'][t] = np.float64(line_contents[4])*1e-10 + dinput['error'][t] = np.float64(line_contents[4]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] elif bool(re.search(r'TN-(11|14)', SLR_file.name, re.I)): @@ -298,7 +310,7 @@ def C20(SLR_file, AOD=True, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line,re.IGNORECASE)) + HEADER = not bool(re.match(r'PRODUCT:+', line, re.IGNORECASE)) # add 1 to counter count += 1 @@ -306,7 +318,7 @@ def C20(SLR_file, AOD=True, HEADER=True): n_mon = file_lines - count # date and GRACE/GRACE-FO month dinput['time'] = np.zeros((n_mon)) - dinput['month'] = np.zeros((n_mon),dtype=np.int64) + dinput['month'] = np.zeros((n_mon), dtype=np.int64) # monthly spherical harmonic replacement solutions dinput['data'] = np.zeros((n_mon)) # monthly spherical harmonic formal standard deviations @@ -317,31 +329,36 @@ def C20(SLR_file, AOD=True, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) # check for empty lines as there are # slight differences in RL04 TN-05_C20_SLR.txt # with blanks between the PRODUCT: line and the data count = len(line_contents) # if count is greater than 0 - if (count > 0): + if count > 0: # modified julian date for line MJD = np.float64(line_contents[0]) # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - MJD+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + MJD + 2400000.5, format='tuple' + ) # converting from month, day, year into decimal year - dinput['time'][t], = gravity_toolkit.time.convert_calendar_decimal( - YY, MM, day=DD, hour=hh) + (dinput['time'][t],) = ( + gravity_toolkit.time.convert_calendar_decimal( + YY, MM, day=DD, hour=hh + ) + ) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[2]) - dinput['error'][t] = np.float64(line_contents[4])*1e-10 + dinput['error'][t] = np.float64(line_contents[4]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] else: # SLR C20 file from PO.DAAC @@ -357,7 +374,7 @@ def C20(SLR_file, AOD=True, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line)) + HEADER = not bool(re.match(r'PRODUCT:+', line)) # add 1 to counter count += 1 @@ -370,33 +387,35 @@ def C20(SLR_file, AOD=True, HEADER=True): # monthly spherical harmonic formal standard deviations eC20_input = np.zeros((n_mon)) # flag denoting if replacement solution - slr_flag = np.zeros((n_mon),dtype=bool) + slr_flag = np.zeros((n_mon), dtype=bool) # time count t = 0 # for every other line: for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) # check for empty lines as there are # slight differences in RL04 TN-05_C20_SLR.txt # with blanks between the PRODUCT: line and the data count = len(line_contents) # if count is greater than 0 - if (count > 0): + if count > 0: # modified julian date for line MJD = np.float64(line_contents[0]) # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - MJD+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + MJD + 2400000.5, format='tuple' + ) # converting from month, day, year into decimal year - date_conv[t], = gravity_toolkit.time.convert_calendar_decimal( - YY, MM, day=DD, hour=hh) + (date_conv[t],) = gravity_toolkit.time.convert_calendar_decimal( + YY, MM, day=DD, hour=hh + ) # Spherical Harmonic data for line C20_input[t] = np.float64(line_contents[2]) - eC20_input[t] = np.float64(line_contents[4])*1e-10 + eC20_input[t] = np.float64(line_contents[4]) * 1e-10 # line has * flag - if bool(re.search(r'\*',line)): + if bool(re.search(r'\*', line)): slr_flag[t] = True # add to t count t += 1 @@ -408,7 +427,7 @@ def C20(SLR_file, AOD=True, HEADER=True): slr_flag = slr_flag[:t] # GRACE/GRACE-FO month of SLR solutions - mon = gravity_toolkit.time.calendar_to_grace(date_conv,around=np.round) + mon = gravity_toolkit.time.calendar_to_grace(date_conv, around=np.round) # number of unique months dinput['month'] = np.unique(mon) n_uniq = len(dinput['month']) @@ -423,14 +442,14 @@ def C20(SLR_file, AOD=True, HEADER=True): for t in range(n_uniq): count = np.count_nonzero(mon == dinput['month'][t]) # there is only one solution for the month - if (count == 1): + if count == 1: i = np.nonzero(mon == dinput['month'][t]) dinput['time'][t] = date_conv[i] dinput['data'][t] = C20_input[i] dinput['error'][t] = eC20_input[i] # there is a special solution for the month # will the solution flagged with slr_flag - elif (count == 2): + elif count == 2: i = np.nonzero((mon == dinput['month'][t]) & slr_flag) dinput['time'][t] = date_conv[i] dinput['data'][t] = C20_input[i] @@ -446,4 +465,4 @@ def C20(SLR_file, AOD=True, HEADER=True): dinput['month'] = gravity_toolkit.time.adjust_months(dinput['month']) # return the SLR-derived oblateness solutions - return dinput \ No newline at end of file + return dinput diff --git a/gravity_toolkit/SLR/C30.py b/gravity_toolkit/SLR/C30.py index 3cd9d3b7..d55c50af 100644 --- a/gravity_toolkit/SLR/C30.py +++ b/gravity_toolkit/SLR/C30.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" C30.py Written by Yara Mohajerani and Tyler Sutterley (05/2023) @@ -77,12 +77,14 @@ read CSR monthly 5x5 file and extract C3,0 coefficients Written 05/2019 """ + import re import pathlib import numpy as np import gravity_toolkit.time import gravity_toolkit.read_SLR_harmonics + # PURPOSE: read Degree 3 zonal data from Satellite Laser Ranging (SLR) def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): r""" @@ -118,7 +120,6 @@ def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): dinput = {} # determine source of input file if bool(re.search(r'TN-(14)', SLR_file.name, re.I)): - # SLR C30 RL06 file from PO.DAAC produced by GSFC with SLR_file.open(mode='r', encoding='utf8') as f: file_contents = f.read().splitlines() @@ -132,7 +133,7 @@ def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'Product:+',line)) + HEADER = not bool(re.match(r'Product:+', line)) # add 1 to counter count += 1 @@ -151,41 +152,46 @@ def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) count = len(line_contents) # only read lines where C30 data exists (don't read NaN lines) - if (count > 7): + if count > 7: # modified julian date for line MJD = np.float64(line_contents[0]) # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - MJD+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + MJD + 2400000.5, format='tuple' + ) # converting from month, day, year into decimal year - dinput['time'][t], = gravity_toolkit.time.convert_calendar_decimal( - YY, MM, day=DD, hour=hh) + (dinput['time'][t],) = ( + gravity_toolkit.time.convert_calendar_decimal( + YY, MM, day=DD, hour=hh + ) + ) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[5]) - dinput['error'][t] = np.float64(line_contents[7])*1e-10 + dinput['error'][t] = np.float64(line_contents[7]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # verify that there imported C30 solutions # (TN-14 data format has changed in the past) - if (t == 0): + if t == 0: raise Exception('No GSFC C30 data imported') # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] elif bool(re.search(r'C30_LARES', SLR_file.name, re.I)): # read LARES filtered values - LARES_input = np.loadtxt(SLR_file,skiprows=1) - dinput['time'] = LARES_input[:,0].copy() + LARES_input = np.loadtxt(SLR_file, skiprows=1) + dinput['time'] = LARES_input[:, 0].copy() # convert C30 from anomalies to absolute - dinput['data'] = 1e-10*LARES_input[:,1] + C30_MEAN + dinput['data'] = 1e-10 * LARES_input[:, 1] + C30_MEAN # filtered data does not have errors - dinput['error'] = np.zeros_like(LARES_input[:,1]) + dinput['error'] = np.zeros_like(LARES_input[:, 1]) # calculate GRACE/GRACE-FO month dinput['month'] = gravity_toolkit.time.calendar_to_grace(dinput['time']) elif bool(re.search(r'GRAVIS-2B_GFZOP', SLR_file.name, re.I)): @@ -207,7 +213,7 @@ def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line)) + HEADER = not bool(re.match(r'PRODUCT:+', line)) # add 1 to counter count += 1 @@ -226,35 +232,37 @@ def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) count = len(line_contents) # check for empty lines - if (count > 0): + if count > 0: # reading decimal year for start of span dinput['time'][t] = np.float64(line_contents[1]) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[5]) - dinput['error'][t] = np.float64(line_contents[7])*1e-10 + dinput['error'][t] = np.float64(line_contents[7]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] else: # CSR 5x5 + 6,1 file from CSR and extract C3,0 coefficients Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # extract dates, C30 harmonics and errors dinput['time'] = Ylms['time'].copy() - dinput['data'] = Ylms['clm'][3,0,:].copy() - dinput['error'] = Ylms['error']['clm'][3,0,:].copy() + dinput['data'] = Ylms['clm'][3, 0, :].copy() + dinput['error'] = Ylms['error']['clm'][3, 0, :].copy() # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - Ylms['MJD']+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + Ylms['MJD'] + 2400000.5, format='tuple' + ) # calculate GRACE/GRACE-FO month - dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY,MM) + dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY, MM) # The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with # Accelerometer shutoffs make the relation between month number diff --git a/gravity_toolkit/SLR/C40.py b/gravity_toolkit/SLR/C40.py index b1dd7ab6..f75171a7 100644 --- a/gravity_toolkit/SLR/C40.py +++ b/gravity_toolkit/SLR/C40.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" C40.py Written by Tyler Sutterley (05/2023) @@ -47,12 +47,14 @@ Updated 01/2023: refactored satellite laser ranging read functions Written 09/2022 """ + import re import pathlib import numpy as np import gravity_toolkit.time import gravity_toolkit.read_SLR_harmonics + # PURPOSE: read Degree 4 zonal data from Satellite Laser Ranging (SLR) def C40(SLR_file, C40_MEAN=0.0, DATE=None, **kwargs): r""" @@ -91,18 +93,21 @@ def C40(SLR_file, C40_MEAN=0.0, DATE=None, **kwargs): # read 5x5 + 6,1 file from GSFC and extract coefficients Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # calculate 28-day moving-average solution from 7-day arcs - dinput.update(gravity_toolkit.convert_weekly(Ylms['time'], - Ylms['clm'][4,0,:], DATE=DATE, NEIGHBORS=28)) + dinput.update( + gravity_toolkit.convert_weekly( + Ylms['time'], Ylms['clm'][4, 0, :], DATE=DATE, NEIGHBORS=28 + ) + ) # no estimated spherical harmonic errors - dinput['error'] = np.zeros_like(DATE,dtype='f8') + dinput['error'] = np.zeros_like(DATE, dtype='f8') elif bool(re.search(r'C40_LARES', SLR_file.name, re.I)): # read LARES filtered values LARES_input = np.loadtxt(SLR_file, skiprows=1) - dinput['time'] = LARES_input[:,0].copy() + dinput['time'] = LARES_input[:, 0].copy() # convert C40 from anomalies to absolute - dinput['data'] = 1e-10*LARES_input[:,1] + C40_MEAN + dinput['data'] = 1e-10 * LARES_input[:, 1] + C40_MEAN # filtered data does not have errors - dinput['error'] = np.zeros_like(LARES_input[:,1]) + dinput['error'] = np.zeros_like(LARES_input[:, 1]) # calculate GRACE/GRACE-FO month dinput['month'] = gravity_toolkit.time.calendar_to_grace(dinput['time']) else: @@ -110,13 +115,14 @@ def C40(SLR_file, C40_MEAN=0.0, DATE=None, **kwargs): Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # extract dates, C40 harmonics and errors dinput['time'] = Ylms['time'].copy() - dinput['data'] = Ylms['clm'][4,0,:].copy() - dinput['error'] = Ylms['error']['clm'][4,0,:].copy() + dinput['data'] = Ylms['clm'][4, 0, :].copy() + dinput['error'] = Ylms['error']['clm'][4, 0, :].copy() # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - Ylms['MJD']+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + Ylms['MJD'] + 2400000.5, format='tuple' + ) # calculate GRACE/GRACE-FO month - dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY,MM) + dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY, MM) # The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with # Accelerometer shutoffs make the relation between month number diff --git a/gravity_toolkit/SLR/C50.py b/gravity_toolkit/SLR/C50.py index 53dff904..2f9e75bd 100644 --- a/gravity_toolkit/SLR/C50.py +++ b/gravity_toolkit/SLR/C50.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" C50.py Written by Yara Mohajerani and Tyler Sutterley (05/2023) @@ -57,12 +57,14 @@ Updated 07/2020: added function docstrings Written 11/2019 """ + import re import pathlib import numpy as np import gravity_toolkit.time import gravity_toolkit.read_SLR_harmonics + # PURPOSE: read Degree 5 zonal data from Satellite Laser Ranging (SLR) def C50(SLR_file, C50_MEAN=0.0, DATE=None, HEADER=True): r""" @@ -100,7 +102,6 @@ def C50(SLR_file, C50_MEAN=0.0, DATE=None, HEADER=True): dinput = {} # determine source of input file if bool(re.search(r'GSFC_SLR_C(20)_C(30)_C(50)', SLR_file.name, re.I)): - # SLR C50 RL06 file from GSFC with SLR_file.open(mode='r', encoding='utf8') as f: file_contents = f.read().splitlines() @@ -114,7 +115,7 @@ def C50(SLR_file, C50_MEAN=0.0, DATE=None, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'Product:+',line)) + HEADER = not bool(re.match(r'Product:+', line)) # add 1 to counter count += 1 @@ -133,48 +134,56 @@ def C50(SLR_file, C50_MEAN=0.0, DATE=None, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) count = len(line_contents) # only read lines where C50 data exists (don't read NaN lines) - if (count > 7): + if count > 7: # modified julian date for line MJD = np.float64(line_contents[0]) # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - MJD+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + MJD + 2400000.5, format='tuple' + ) # converting from month, day, year into decimal year - dinput['time'][t], = gravity_toolkit.time.convert_calendar_decimal( - YY, MM, day=DD, hour=hh) + (dinput['time'][t],) = ( + gravity_toolkit.time.convert_calendar_decimal( + YY, MM, day=DD, hour=hh + ) + ) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[10]) - dinput['error'][t] = np.float64(line_contents[12])*1e-10 + dinput['error'][t] = np.float64(line_contents[12]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # verify that there imported C50 solutions - if (t == 0): + if t == 0: raise Exception('No GSFC C50 data imported') # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] elif bool(re.search(r'gsfc_slr_5x5c61s61', SLR_file.name, re.I)): # read 5x5 + 6,1 file from GSFC and extract coefficients Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # calculate 28-day moving-average solution from 7-day arcs - dinput.update(gravity_toolkit.convert_weekly(Ylms['time'], - Ylms['clm'][5,0,:], DATE=DATE, NEIGHBORS=28)) + dinput.update( + gravity_toolkit.convert_weekly( + Ylms['time'], Ylms['clm'][5, 0, :], DATE=DATE, NEIGHBORS=28 + ) + ) # no estimated spherical harmonic errors - dinput['error'] = np.zeros_like(DATE,dtype='f8') + dinput['error'] = np.zeros_like(DATE, dtype='f8') elif bool(re.search(r'C50_LARES', SLR_file.name, re.I)): # read LARES filtered values LARES_input = np.loadtxt(SLR_file, skiprows=1) - dinput['time'] = LARES_input[:,0].copy() + dinput['time'] = LARES_input[:, 0].copy() # convert C50 from anomalies to absolute - dinput['data'] = 1e-10*LARES_input[:,1] + C50_MEAN + dinput['data'] = 1e-10 * LARES_input[:, 1] + C50_MEAN # filtered data does not have errors - dinput['error'] = np.zeros_like(LARES_input[:,1]) + dinput['error'] = np.zeros_like(LARES_input[:, 1]) # calculate GRACE/GRACE-FO month dinput['month'] = gravity_toolkit.time.calendar_to_grace(dinput['time']) else: @@ -182,13 +191,14 @@ def C50(SLR_file, C50_MEAN=0.0, DATE=None, HEADER=True): Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # extract dates, C50 harmonics and errors dinput['time'] = Ylms['time'].copy() - dinput['data'] = Ylms['clm'][5,0,:].copy() - dinput['error'] = Ylms['error']['clm'][5,0,:].copy() + dinput['data'] = Ylms['clm'][5, 0, :].copy() + dinput['error'] = Ylms['error']['clm'][5, 0, :].copy() # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - Ylms['MJD']+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + Ylms['MJD'] + 2400000.5, format='tuple' + ) # calculate GRACE/GRACE-FO month - dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY,MM) + dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY, MM) # The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with # Accelerometer shutoffs make the relation between month number diff --git a/gravity_toolkit/SLR/CS2.py b/gravity_toolkit/SLR/CS2.py index 372fad2a..4c037813 100644 --- a/gravity_toolkit/SLR/CS2.py +++ b/gravity_toolkit/SLR/CS2.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" CS2.py Written by Hugo Lecomte and Tyler Sutterley (05/2023) @@ -77,12 +77,14 @@ Updated 04/2021: use adjust_months function to fix special months cases Written 11/2020 """ + import re import pathlib import numpy as np import gravity_toolkit.time import gravity_toolkit.read_SLR_harmonics + # PURPOSE: read Degree 2,m data from Satellite Laser Ranging (SLR) def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): r""" @@ -128,30 +130,30 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): # 7-day arc SLR file produced by GSFC # input variable names and types dtype = {} - dtype['names'] = ('time','C2','S2') - dtype['formats'] = ('f','f8','f8') + dtype['names'] = ('time', 'C2', 'S2') + dtype['formats'] = ('f', 'f8', 'f8') # read SLR 2,1 file from GSFC # Column 1: Approximate mid-point of 7-day solution (years) # Column 2: Solution from SLR (normalized) # Column 3: Solution from SLR (normalized) content = np.loadtxt(SLR_file, dtype=dtype) # duplicate time and harmonics - tdec = np.repeat(content['time'],7) - c2m = np.repeat(content['C2'],7) - s2m = np.repeat(content['S2'],7) + tdec = np.repeat(content['time'], 7) + c2m = np.repeat(content['C2'], 7) + s2m = np.repeat(content['S2'], 7) # calculate daily dates to use in centered moving average - tdec += (np.mod(np.arange(len(tdec)),7) - 3.5)/365.25 + tdec += (np.mod(np.arange(len(tdec)), 7) - 3.5) / 365.25 # number of dates to use in average n_neighbors = 28 # calculate 28-day moving-average solution from 7-day arcs dinput['time'] = np.zeros_like(DATE) - dinput['C2m'] = np.zeros_like(DATE,dtype='f8') - dinput['S2m'] = np.zeros_like(DATE,dtype='f8') + dinput['C2m'] = np.zeros_like(DATE, dtype='f8') + dinput['S2m'] = np.zeros_like(DATE, dtype='f8') # no estimated spherical harmonic errors - dinput['eC2m'] = np.zeros_like(DATE,dtype='f8') - dinput['eS2m'] = np.zeros_like(DATE,dtype='f8') - for i,D in enumerate(DATE): - isort = np.argsort((tdec - D)**2)[:n_neighbors] + dinput['eC2m'] = np.zeros_like(DATE, dtype='f8') + dinput['eS2m'] = np.zeros_like(DATE, dtype='f8') + for i, D in enumerate(DATE): + isort = np.argsort((tdec - D) ** 2)[:n_neighbors] dinput['time'][i] = np.mean(tdec[isort]) dinput['C2m'][i] = np.mean(c2m[isort]) dinput['S2m'][i] = np.mean(s2m[isort]) @@ -161,22 +163,22 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): # read 5x5 + 6,1 file from GSFC and extract coefficients Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # duplicate time and harmonics - tdec = np.repeat(Ylms['time'],7) - c2m = np.repeat(Ylms['clm'][2,ORDER],7) - s2m = np.repeat(Ylms['slm'][2,ORDER],7) + tdec = np.repeat(Ylms['time'], 7) + c2m = np.repeat(Ylms['clm'][2, ORDER], 7) + s2m = np.repeat(Ylms['slm'][2, ORDER], 7) # calculate daily dates to use in centered moving average - tdec += (np.mod(np.arange(len(tdec)),7) - 3.5)/365.25 + tdec += (np.mod(np.arange(len(tdec)), 7) - 3.5) / 365.25 # number of dates to use in average n_neighbors = 28 # calculate 28-day moving-average solution from 7-day arcs dinput['time'] = np.zeros_like(DATE) - dinput['C2m'] = np.zeros_like(DATE,dtype='f8') - dinput['S2m'] = np.zeros_like(DATE,dtype='f8') + dinput['C2m'] = np.zeros_like(DATE, dtype='f8') + dinput['S2m'] = np.zeros_like(DATE, dtype='f8') # no estimated spherical harmonic errors - dinput['eC2m'] = np.zeros_like(DATE,dtype='f8') - dinput['eS2m'] = np.zeros_like(DATE,dtype='f8') - for i,D in enumerate(DATE): - isort = np.argsort((tdec - D)**2)[:n_neighbors] + dinput['eC2m'] = np.zeros_like(DATE, dtype='f8') + dinput['eS2m'] = np.zeros_like(DATE, dtype='f8') + for i, D in enumerate(DATE): + isort = np.argsort((tdec - D) ** 2)[:n_neighbors] dinput['time'][i] = np.mean(tdec[isort]) dinput['C2m'][i] = np.mean(c2m[isort]) dinput['S2m'][i] = np.mean(s2m[isort]) @@ -186,9 +188,18 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): # SLR RL06 file produced by CSR # input variable names and types dtype = {} - dtype['names'] = ('time','C2','S2','eC2','eS2', - 'C2aod','S2aod','start','end') - dtype['formats'] = ('f','f8','f8','f','f','f','f','f','f') + dtype['names'] = ( + 'time', + 'C2', + 'S2', + 'eC2', + 'eS2', + 'C2aod', + 'S2aod', + 'start', + 'end', + ) + dtype['formats'] = ('f', 'f8', 'f8', 'f', 'f', 'f', 'f', 'f', 'f') # read SLR 2,1 or 2,2 RL06 file from CSR # header text is commented and won't be read # Column 1: Approximate mid-point of monthly solution (years) @@ -204,11 +215,11 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): dinput['time'] = content['time'].copy() dinput['month'] = gravity_toolkit.time.calendar_to_grace(dinput['time']) # remove the monthly mean of the AOD model - dinput['C2m'] = content['C2'] - content['C2aod']*10**-10 - dinput['S2m'] = content['S2'] - content['S2aod']*10**-10 + dinput['C2m'] = content['C2'] - content['C2aod'] * 10**-10 + dinput['S2m'] = content['S2'] - content['S2aod'] * 10**-10 # scale SLR solution sigmas - dinput['eC2m'] = content['eC2']*10**-10 - dinput['eS2m'] = content['eS2']*10**-10 + dinput['eC2m'] = content['eC2'] * 10**-10 + dinput['eS2m'] = content['eS2'] * 10**-10 elif bool(re.search(r'GRAVIS-2B_GFZOP', SLR_file.name, re.I)): # Combined GRACE/SLR solution file produced by GFZ # Column 1: MJD of BEGINNING of solution data span @@ -231,7 +242,7 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line)) + HEADER = not bool(re.match(r'PRODUCT:+', line)) # add 1 to counter count += 1 @@ -252,24 +263,25 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) count = len(line_contents) # check for empty lines - if (count > 0): + if count > 0: # reading decimal year for start of span dinput['time'][t] = np.float64(line_contents[1]) # Spherical Harmonic data for line dinput['C2m'][t] = np.float64(line_contents[8]) - dinput['eC2m'][t] = np.float64(line_contents[10])*1e-10 + dinput['eC2m'][t] = np.float64(line_contents[10]) * 1e-10 dinput['S2m'][t] = np.float64(line_contents[11]) - dinput['eS2m'][t] = np.float64(line_contents[13])*1e-10 + dinput['eS2m'][t] = np.float64(line_contents[13]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] # The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with diff --git a/gravity_toolkit/__init__.py b/gravity_toolkit/__init__.py index a6eff2f4..99f1129e 100644 --- a/gravity_toolkit/__init__.py +++ b/gravity_toolkit/__init__.py @@ -15,6 +15,7 @@ Documentation is available at https://gravity-toolkit.readthedocs.io """ + import gravity_toolkit.geocenter import gravity_toolkit.mascons import gravity_toolkit.time @@ -27,15 +28,12 @@ associated_legendre, plm_colombo, plm_holmes, - plm_mohlenkamp + plm_mohlenkamp, ) from gravity_toolkit.clenshaw_summation import clenshaw_summation from gravity_toolkit.degree_amplitude import degree_amplitude from gravity_toolkit.destripe_harmonics import destripe_harmonics -from gravity_toolkit.fourier_legendre import ( - fourier_legendre, - legendre_gradient -) +from gravity_toolkit.fourier_legendre import fourier_legendre, legendre_gradient from gravity_toolkit.gauss_weights import gauss_weights from gravity_toolkit.gen_averaging_kernel import gen_averaging_kernel from gravity_toolkit.gen_disc_load import gen_disc_load @@ -48,42 +46,37 @@ from gravity_toolkit.grace_find_months import grace_find_months from gravity_toolkit.grace_input_months import ( grace_input_months, - read_ecmwf_corrections + read_ecmwf_corrections, ) from gravity_toolkit.grace_months_index import grace_months_index from gravity_toolkit.harmonics import harmonics from gravity_toolkit.harmonic_gradients import ( harmonic_gradients, - geostrophic_currents + geostrophic_currents, ) from gravity_toolkit.harmonic_summation import ( harmonic_summation, harmonic_transform, - stokes_summation + stokes_summation, ) from gravity_toolkit.legendre_polynomials import legendre_polynomials from gravity_toolkit.legendre import legendre from gravity_toolkit.ocean_stokes import ocean_stokes, land_stokes from gravity_toolkit.read_gfc_harmonics import read_gfc_harmonics -from gravity_toolkit.read_GIA_model import ( - read_GIA_model, - gia -) +from gravity_toolkit.read_GIA_model import read_GIA_model, gia from gravity_toolkit.read_GRACE_harmonics import read_GRACE_harmonics from gravity_toolkit.read_love_numbers import ( read_love_numbers, load_love_numbers, - love_numbers + love_numbers, ) from gravity_toolkit.read_SLR_harmonics import ( read_SLR_harmonics, - convert_weekly + convert_weekly, ) from gravity_toolkit.sea_level_equation import sea_level_equation -from gravity_toolkit.spatial import ( - spatial, - scaling_factors -) +from gravity_toolkit.spatial import spatial, scaling_factors from gravity_toolkit.units import units + # get version number __version__ = gravity_toolkit.version.version diff --git a/gravity_toolkit/associated_legendre.py b/gravity_toolkit/associated_legendre.py index cfb11abe..20b921c4 100644 --- a/gravity_toolkit/associated_legendre.py +++ b/gravity_toolkit/associated_legendre.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" associated_legendre.py Written by Tyler Sutterley (03/2023) @@ -19,14 +19,12 @@ Updated 09/2013: new format for file headers Written 03/2013 """ + from __future__ import division import numpy as np -def associated_legendre(LMAX, x, - method='holmes', - MMAX=None, - astype=np.float64 - ): + +def associated_legendre(LMAX, x, method='holmes', MMAX=None, astype=np.float64): """ Computes fully-normalized associated Legendre Polynomials and their first derivative @@ -57,18 +55,16 @@ def associated_legendre(LMAX, x, dplms: np.ndarray first derivative of Legendre polynomials """ - if (method.lower() == 'colombo'): + if method.lower() == 'colombo': return plm_colombo(LMAX, x, MMAX=MMAX, astype=astype) - elif (method.lower() == 'holmes'): + elif method.lower() == 'holmes': return plm_holmes(LMAX, x, MMAX=MMAX, astype=astype) - elif (method.lower() == 'mohlenkamp'): + elif method.lower() == 'mohlenkamp': return plm_mohlenkamp(LMAX, x, MMAX=MMAX, astype=astype) raise ValueError(f'Unknown method {method}') -def plm_colombo(LMAX, x, - MMAX=None, - astype=np.float64 - ): + +def plm_colombo(LMAX, x, MMAX=None, astype=np.float64): """ Computes fully-normalized associated Legendre Polynomials and their first derivative using a Standard forward column method :cite:p:`Colombo:1981vh` @@ -105,8 +101,8 @@ def plm_colombo(LMAX, x, MMAX = np.copy(LMAX) # allocating for the plm matrix and differentials - plm = np.zeros((LMAX+1,LMAX+1,jm)) - dplm = np.zeros((LMAX+1,LMAX+1,jm)) + plm = np.zeros((LMAX + 1, LMAX + 1, jm)) + dplm = np.zeros((LMAX + 1, LMAX + 1, jm)) # u is sine of colatitude (cosine of latitude) so that 0 <= s <= 1 # for x=cos(th): u=sin(th) @@ -115,39 +111,50 @@ def plm_colombo(LMAX, x, u[u == 0] = np.finfo(u.dtype).eps # Calculating the initial polynomials for the recursion - plm[0,0,:] = 1.0 - plm[1,0,:] = np.sqrt(3.0)*x - plm[1,1,:] = np.sqrt(3.0)*u + plm[0, 0, :] = 1.0 + plm[1, 0, :] = np.sqrt(3.0) * x + plm[1, 1, :] = np.sqrt(3.0) * u # calculating first derivatives for harmonics of degree 1 - dplm[1,0,:] = (1.0/u)*(x*plm[1,0,:] - np.sqrt(3)*plm[0,0,:]) - dplm[1,1,:] = (x/u)*plm[1,1,:] - for l in range(2, LMAX+1): - for m in range(0, l):# Zonal and Tesseral harmonics (non-sectorial) + dplm[1, 0, :] = (1.0 / u) * (x * plm[1, 0, :] - np.sqrt(3) * plm[0, 0, :]) + dplm[1, 1, :] = (x / u) * plm[1, 1, :] + for l in range(2, LMAX + 1): + for m in range(0, l): # Zonal and Tesseral harmonics (non-sectorial) # Computes the non-sectorial terms from previously computed # sectorial terms. - alm = np.sqrt(((2.0*l-1.0)*(2.0*l+1.0))/((l-m)*(l+m))) - blm = np.sqrt(((2.0*l+1.0)*(l+m-1.0)*(l-m-1.0))/((l-m)*(l+m)*(2.0*l-3.0))) + alm = np.sqrt( + ((2.0 * l - 1.0) * (2.0 * l + 1.0)) / ((l - m) * (l + m)) + ) + blm = np.sqrt( + ((2.0 * l + 1.0) * (l + m - 1.0) * (l - m - 1.0)) + / ((l - m) * (l + m) * (2.0 * l - 3.0)) + ) # if (m == l-1): plm[l-2,m,:] will be 0 - plm[l,m,:] = alm*x*plm[l-1,m,:] - blm*plm[l-2,m,:] + plm[l, m, :] = alm * x * plm[l - 1, m, :] - blm * plm[l - 2, m, :] # calculate first derivatives - flm = np.sqrt(((l**2.0 - m**2.0)*(2.0*l + 1.0))/(2.0*l - 1.0)) - dplm[l,m,:] = (1.0/u)*(l*x*plm[l,m,:] - flm*plm[l-1,m,:]) + flm = np.sqrt( + ((l**2.0 - m**2.0) * (2.0 * l + 1.0)) / (2.0 * l - 1.0) + ) + dplm[l, m, :] = (1.0 / u) * ( + l * x * plm[l, m, :] - flm * plm[l - 1, m, :] + ) # Sectorial harmonics # The sectorial harmonics serve as seed values for the recursion # starting with P00 and P11 (outside the loop) - plm[l,l,:] = u*np.sqrt((2.0*l+1.0)/(2.0*l))*np.squeeze(plm[l-1,l-1,:]) + plm[l, l, :] = ( + u + * np.sqrt((2.0 * l + 1.0) / (2.0 * l)) + * np.squeeze(plm[l - 1, l - 1, :]) + ) # calculate first derivatives for sectorial harmonics - dplm[l,l,:] = np.longdouble(l)*(x/u)*plm[l,l,:] + dplm[l, l, :] = np.longdouble(l) * (x / u) * plm[l, l, :] # return the legendre polynomials and their first derivative # truncating orders to MMAX - return plm[:,:MMAX+1,:], dplm[:,:MMAX+1,:] + return plm[:, : MMAX + 1, :], dplm[:, : MMAX + 1, :] + -def plm_holmes(LMAX, x, - MMAX=None, - astype=np.float64 - ): +def plm_holmes(LMAX, x, MMAX=None, astype=np.float64): """ Computes fully-normalized associated Legendre Polynomials and their first derivative using the recursion relation from :cite:p:`Holmes:2002ff` @@ -186,25 +193,39 @@ def plm_holmes(LMAX, x, scalef = 1.0e-280 # allocate for multiplicative factors, and plms - f1 = np.zeros(((LMAX+1)*(LMAX+2)//2), dtype=astype) - f2 = np.zeros(((LMAX+1)*(LMAX+2)//2), dtype=astype) - p = np.zeros(((LMAX+1)*(LMAX+2)//2,jm), dtype=astype) - plm = np.zeros((LMAX+1,LMAX+1,jm), dtype=astype) - dplm = np.zeros((LMAX+1,LMAX+1,jm), dtype=astype) + f1 = np.zeros(((LMAX + 1) * (LMAX + 2) // 2), dtype=astype) + f2 = np.zeros(((LMAX + 1) * (LMAX + 2) // 2), dtype=astype) + p = np.zeros(((LMAX + 1) * (LMAX + 2) // 2, jm), dtype=astype) + plm = np.zeros((LMAX + 1, LMAX + 1, jm), dtype=astype) + dplm = np.zeros((LMAX + 1, LMAX + 1, jm), dtype=astype) # Precompute multiplicative factors used in recursion relationships # Note that prefactors are not used for the case when m=l and m=l-1, # as a different recursion is used for these two values. - k = 2# k = l*(l+1)/2 + m - for l in range(2, LMAX+1): + k = 2 # k = l*(l+1)/2 + m + for l in range(2, LMAX + 1): k += 1 - f1[k] = np.sqrt(2.0*l-1.0)*np.sqrt(2.0*l+1.0)/np.longdouble(l) - f2[k] = np.longdouble(l-1.0)*np.sqrt(2.0*l+1.0)/(np.sqrt(2.0*l-3.0)*np.longdouble(l)) - for m in range(1, l-1): + f1[k] = ( + np.sqrt(2.0 * l - 1.0) * np.sqrt(2.0 * l + 1.0) / np.longdouble(l) + ) + f2[k] = ( + np.longdouble(l - 1.0) + * np.sqrt(2.0 * l + 1.0) + / (np.sqrt(2.0 * l - 3.0) * np.longdouble(l)) + ) + for m in range(1, l - 1): k += 1 - f1[k] = np.sqrt(2.0*l+1.0)*np.sqrt(2.0*l-1.0)/(np.sqrt(l+m)*np.sqrt(l-m)) - f2[k] = np.sqrt(2.0*l+1.0)*np.sqrt(l-m-1.0)*np.sqrt(l+m-1.0)/ \ - (np.sqrt(2.0*l-3.0)*np.sqrt(l+m)*np.sqrt(l-m)) + f1[k] = ( + np.sqrt(2.0 * l + 1.0) + * np.sqrt(2.0 * l - 1.0) + / (np.sqrt(l + m) * np.sqrt(l - m)) + ) + f2[k] = ( + np.sqrt(2.0 * l + 1.0) + * np.sqrt(l - m - 1.0) + * np.sqrt(l + m - 1.0) + / (np.sqrt(2.0 * l - 3.0) * np.sqrt(l + m) * np.sqrt(l - m)) + ) k += 2 # u is sine of colatitude (cosine of latitude) so that 0 <= s <= 1 @@ -214,60 +235,62 @@ def plm_holmes(LMAX, x, u[u == 0] = np.finfo(u.dtype).eps # Calculate P(l,0). These are not scaled. - p[0,:] = 1.0 - p[1,:] = np.sqrt(3.0)*x + p[0, :] = 1.0 + p[1, :] = np.sqrt(3.0) * x k = 1 - for l in range(2, LMAX+1): + for l in range(2, LMAX + 1): k += l - p[k,:] = f1[k]*x*p[k-l,:] - f2[k]*p[k-2*l+1,:] + p[k, :] = f1[k] * x * p[k - l, :] - f2[k] * p[k - 2 * l + 1, :] # Calculate P(m,m), P(m+1,m), and P(l,m) - pmm = np.sqrt(2.0)*scalef - rescalem = 1.0/scalef + pmm = np.sqrt(2.0) * scalef + rescalem = 1.0 / scalef kstart = 0 for m in range(1, LMAX): rescalem = rescalem * u # Calculate P(m,m) - kstart += m+1 - pmm = pmm * np.sqrt(2*m+1)/np.sqrt(2*m) - p[kstart,:] = pmm + kstart += m + 1 + pmm = pmm * np.sqrt(2 * m + 1) / np.sqrt(2 * m) + p[kstart, :] = pmm # Calculate P(m+1,m) - k = kstart+m+1 - p[k,:] = x*np.sqrt(2*m+3)*pmm + k = kstart + m + 1 + p[k, :] = x * np.sqrt(2 * m + 3) * pmm # Calculate P(l,m) - for l in range(m+2, LMAX+1): + for l in range(m + 2, LMAX + 1): k += l - p[k,:] = x*f1[k]*p[k-l,:] - f2[k]*p[k-2*l+1,:] - p[k-2*l+1,:] = p[k-2*l+1,:] * rescalem + p[k, :] = x * f1[k] * p[k - l, :] - f2[k] * p[k - 2 * l + 1, :] + p[k - 2 * l + 1, :] = p[k - 2 * l + 1, :] * rescalem # rescale - p[k,:] = p[k,:] * rescalem - p[k-LMAX,:] = p[k-LMAX,:] * rescalem + p[k, :] = p[k, :] * rescalem + p[k - LMAX, :] = p[k - LMAX, :] * rescalem # Calculate P(LMAX,LMAX) rescalem = rescalem * u - kstart += m+2 - p[kstart,:] = pmm * np.sqrt(2*LMAX+1) / np.sqrt(2*LMAX) * rescalem + kstart += m + 2 + p[kstart, :] = pmm * np.sqrt(2 * LMAX + 1) / np.sqrt(2 * LMAX) * rescalem # reshape Legendre polynomials to output dimensions - for m in range(LMAX+1): - for l in range(m,LMAX+1): - lm = (l*(l+1))//2 + m - plm[l,m,:] = p[lm,:] + for m in range(LMAX + 1): + for l in range(m, LMAX + 1): + lm = (l * (l + 1)) // 2 + m + plm[l, m, :] = p[lm, :] # calculate first derivatives - if (l == m): - dplm[l,m,:] = np.longdouble(m)*(x/u)*plm[l,m,:] + if l == m: + dplm[l, m, :] = np.longdouble(m) * (x / u) * plm[l, m, :] else: - flm = np.sqrt(((l**2.0 - m**2.0)*(2.0*l + 1.0))/(2.0*l - 1.0)) - dplm[l,m,:]= (1.0/u)*(l*x*plm[l,m,:] - flm*plm[l-1,m,:]) + flm = np.sqrt( + ((l**2.0 - m**2.0) * (2.0 * l + 1.0)) / (2.0 * l - 1.0) + ) + dplm[l, m, :] = (1.0 / u) * ( + l * x * plm[l, m, :] - flm * plm[l - 1, m, :] + ) # return the legendre polynomials and their first derivative # truncating orders to MMAX - return plm[:,:MMAX+1,:], dplm[:,:MMAX+1,:] + return plm[:, : MMAX + 1, :], dplm[:, : MMAX + 1, :] + -def plm_mohlenkamp(LMAX, x, - MMAX=None, - astype=np.float64 - ): +def plm_mohlenkamp(LMAX, x, MMAX=None, astype=np.float64): """ Computes fully-normalized associated Legendre Polynomials and their first derivative using the recursion relation from :cite:p:`Mohlenkamp:2016vv` @@ -307,54 +330,66 @@ def plm_mohlenkamp(LMAX, x, sx = len(x) # Initialize the output Legendre polynomials - plm = np.zeros((LMAX+1, MMAX+1, sx), dtype=astype) - dplm = np.zeros((LMAX+1, LMAX+1, sx), dtype=astype) + plm = np.zeros((LMAX + 1, MMAX + 1, sx), dtype=astype) + dplm = np.zeros((LMAX + 1, LMAX + 1, sx), dtype=astype) # Jacobi polynomial for the recurrence relation - jlmm = np.zeros((LMAX+1, MMAX+1, sx)) + jlmm = np.zeros((LMAX + 1, MMAX + 1, sx)) # for x=cos(th): u= sin(th) u = np.sqrt(1.0 - x**2) # update where u==0 to eps of data type to prevent invalid divisions u[u == 0] = np.finfo(u.dtype).eps # for all spherical harmonic orders of interest - for mm in range(0,MMAX+1):# equivalent to 0:MMAX + for mm in range(0, MMAX + 1): # equivalent to 0:MMAX # Initialize the recurrence relation # J-1,m,m Term == 0 # J0,m,m Term - if (mm > 0): + if mm > 0: # j ranges from 1 to mm for the product - j = np.arange(0,mm)+1.0 - jlmm[0,mm,:] = np.prod(np.sqrt(1.0 + 1.0/(2.0*j)))/np.sqrt(2.0) - else: # if mm == 0: jlmm = 1/sqrt(2) - jlmm[0,mm,:] = 1.0/np.sqrt(2.0) + j = np.arange(0, mm) + 1.0 + jlmm[0, mm, :] = np.prod(np.sqrt(1.0 + 1.0 / (2.0 * j))) / np.sqrt( + 2.0 + ) + else: # if mm == 0: jlmm = 1/sqrt(2) + jlmm[0, mm, :] = 1.0 / np.sqrt(2.0) # Jk,m,m Terms - for k in range(1, LMAX+1):# computation for SH degrees + for k in range(1, LMAX + 1): # computation for SH degrees # Initialization begins at -1 # this is to make the formula parallel the function written in # Martin Mohlenkamp's Guide to Spherical Harmonics # Jacobi General Terms - if (k == 1):# for degree 1 terms - jlmm[k,mm,:] = 2.0*x * jlmm[k-1,mm,:] * \ - np.sqrt(1.0 + (mm - 0.5)/k) * \ - np.sqrt(1.0 - (mm - 0.5)/(k + 2.0*mm)) - else:# for all other spherical harmonic degrees - jlmm[k,mm,:] = 2.0*x * jlmm[k-1,mm,:] * \ - np.sqrt(1.0 + (mm - 0.5)/k) * \ - np.sqrt(1.0 - (mm - 0.5)/(k + 2.0*mm)) - \ - jlmm[k-2,mm,:] * np.sqrt(1.0 + 4.0/(2.0*k + 2.0*mm - 3.0)) * \ - np.sqrt(1.0 - (1.0/k)) * np.sqrt(1.0 - 1.0/(k + 2.0*mm)) + if k == 1: # for degree 1 terms + jlmm[k, mm, :] = ( + 2.0 + * x + * jlmm[k - 1, mm, :] + * np.sqrt(1.0 + (mm - 0.5) / k) + * np.sqrt(1.0 - (mm - 0.5) / (k + 2.0 * mm)) + ) + else: # for all other spherical harmonic degrees + jlmm[k, mm, :] = 2.0 * x * jlmm[k - 1, mm, :] * np.sqrt( + 1.0 + (mm - 0.5) / k + ) * np.sqrt(1.0 - (mm - 0.5) / (k + 2.0 * mm)) - jlmm[ + k - 2, mm, : + ] * np.sqrt(1.0 + 4.0 / (2.0 * k + 2.0 * mm - 3.0)) * np.sqrt( + 1.0 - (1.0 / k) + ) * np.sqrt(1.0 - 1.0 / (k + 2.0 * mm)) # Normalization is geodesy convention - for l in range(mm,LMAX+1): # equivalent to mm:LMAX - if (mm == 0):# Geodesy normalization (m=0) == sqrt(2)*sin(th)^0 + for l in range(mm, LMAX + 1): # equivalent to mm:LMAX + if mm == 0: # Geodesy normalization (m=0) == sqrt(2)*sin(th)^0 # u^mm term is dropped as u^0 = 1 - plm[l,mm,:] = np.sqrt(2.0)*jlmm[l-mm,mm,:] - else:# Geodesy normalization all others == 2*sin(th)^mm - plm[l,mm,:] = 2.0*(u**mm)*jlmm[l-mm,mm,:] + plm[l, mm, :] = np.sqrt(2.0) * jlmm[l - mm, mm, :] + else: # Geodesy normalization all others == 2*sin(th)^mm + plm[l, mm, :] = 2.0 * (u**mm) * jlmm[l - mm, mm, :] # calculate first derivatives - if (l == mm): - dplm[l,mm,:] = np.longdouble(mm)*(x/u)*plm[l,mm,:] + if l == mm: + dplm[l, mm, :] = np.longdouble(mm) * (x / u) * plm[l, mm, :] else: - flm = np.sqrt(((l**2.0 - mm**2.0)*(2.0*l + 1.0))/(2.0*l - 1.0)) - dplm[l,mm,:]= (1.0/u)*(l*x*plm[l,mm,:] - flm*plm[l-1,mm,:]) + flm = np.sqrt( + ((l**2.0 - mm**2.0) * (2.0 * l + 1.0)) / (2.0 * l - 1.0) + ) + dplm[l, mm, :] = (1.0 / u) * ( + l * x * plm[l, mm, :] - flm * plm[l - 1, mm, :] + ) # return the legendre polynomials and their first derivative return plm, dplm diff --git a/gravity_toolkit/clenshaw_summation.py b/gravity_toolkit/clenshaw_summation.py index 4315d322..adecf99b 100644 --- a/gravity_toolkit/clenshaw_summation.py +++ b/gravity_toolkit/clenshaw_summation.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" clenshaw_summation.py Written by Tyler Sutterley (07/2026) Calculates the spatial field for a series of spherical harmonics for a @@ -67,18 +67,24 @@ simplified love number extrapolation if LMAX is greater than 696 Written 08/2017 """ + import numpy as np from gravity_toolkit.gauss_weights import gauss_weights from gravity_toolkit.units import units -def clenshaw_summation(clm, slm, lon, lat, - RAD=0, - UNITS=0, - LMAX=0, - LOVE=None, - ASTYPE=np.longdouble, - SCALE=1e-280 - ): + +def clenshaw_summation( + clm, + slm, + lon, + lat, + RAD=0, + UNITS=0, + LMAX=0, + LOVE=None, + ASTYPE=np.longdouble, + SCALE=1e-280, +): r""" Calculates the spatial field for a series of spherical harmonics for a sequence of ungridded points :cite:p:`Holmes:2002ff,Tscherning:1982tu` @@ -121,7 +127,7 @@ def clenshaw_summation(clm, slm, lon, lat, """ # check if lat and lon are the same size - if (len(lat) != len(lon)): + if len(lat) != len(lon): raise ValueError('Incompatible vector dimensions (lon, lat)') # calculate colatitude and longitude in radians @@ -135,16 +141,16 @@ def clenshaw_summation(clm, slm, lon, lat, npts = len(th) # Gaussian Smoothing - if (RAD != 0): - wl = 2.0*np.pi*gauss_weights(RAD, LMAX) + if RAD != 0: + wl = 2.0 * np.pi * gauss_weights(RAD, LMAX) else: # else = 1 - wl = np.ones((LMAX+1)) + wl = np.ones((LMAX + 1)) # Setting units factor for output # dfactor is the degree dependent coefficients factors = units(lmax=LMAX) - if isinstance(UNITS, (list,np.ndarray)): + if isinstance(UNITS, (list, np.ndarray)): # custom units dfactor = np.copy(UNITS) elif isinstance(UNITS, str): @@ -159,34 +165,28 @@ def clenshaw_summation(clm, slm, lon, lat, # complex spherical harmonics ylm = clm - 1j * slm # smooth degree dependent factors - f = dfactor*wl - # calculate arrays for clenshaw summations over colatitudes - cs_m = np.zeros((npts, LMAX+1), dtype=np.clongdouble) - for m in range(LMAX, -1, -1): - # convolve harmonics with unit factors and smoothing - cs_m[:, m] = _clenshaw(t, f, m, ylm, LMAX, SCALE=SCALE) - - # calculate cos(phi) - cos_phi_2 = 2.0*np.cos(phi) - # matrix of cos/sin m*phi summation (Euler's form) - m_phi = np.zeros((npts, LMAX+2), dtype=np.clongdouble) - # initialize matrix with values at lmax+1 and lmax - m_phi[:,LMAX+1] = np.exp(1j * (LMAX + 1) * phi) - m_phi[:,LMAX] = np.exp(1j * LMAX*phi) - # calculate summation for order LMAX - s_m = (cs_m[:,LMAX]*m_phi[:,LMAX]).real + f = dfactor * wl + + # calculating cos(m*phi) and sin(m*phi) using Euler's formula + mm = np.arange(0, LMAX + 1) + m_phi = np.exp(1j * np.einsum('m...,p...->pm...', mm, phi)) + + # initiate summation + s_m = 0.0 # iterate to calculate complete summation - for m in range(LMAX-1, 0, -1): + for m in range(LMAX, 0, -1): # calculate summation for order m - m_phi[:,m] = cos_phi_2*m_phi[:,m+1] - m_phi[:,m+2] - a_m = np.sqrt((2.0*m + 3.0)/(2.0*m + 2.0)) + a_m = np.sqrt((2.0 * m + 3.0) / (2.0 * m + 2.0)) + cs_m = _clenshaw(t, f, m, ylm, LMAX, SCALE=SCALE) # update summation and discard imaginary component - s_m = a_m*u*s_m + (cs_m[:,m]*m_phi[:,m]).real - # calculate spatial field - spatial = np.sqrt(3.0)*u*s_m + cs_m[:,0].real + s_m = a_m * u * s_m + (cs_m * m_phi[:, m]).real + # add the final terms to calculate spatial field + cs_m = _clenshaw(t, f, 0, ylm, LMAX, SCALE=SCALE) + spatial = np.sqrt(3.0) * u * s_m + cs_m.real # return the calculated spatial field return spatial + # PURPOSE: compute Clenshaw summation of the fully normalized associated # Legendre's function for constant order m def _clenshaw(t, f, m, Ylm1, lmax, SCALE=1e-280): @@ -211,45 +211,80 @@ def _clenshaw(t, f, m, Ylm1, lmax, SCALE=1e-280): Returns ------- - s_m_c: np.ndarray + cs_m: np.ndarray conditioned array for clenshaw summation """ # allocate for output matrix N = len(t) - s_m = np.zeros((N), dtype=np.clongdouble) + cs_m = np.zeros((N), dtype=np.clongdouble) # scaling to prevent overflow - ylm = SCALE*Ylm1.astype(np.clongdouble) + ylm = SCALE * Ylm1.astype(np.clongdouble) # convert lmax and m to float lm = np.float64(lmax) mm = np.float64(m) - if (m == lmax): - s_m[:] = f[lmax]*ylm[lmax,lmax] - elif (m == (lmax-1)): - a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/((lm-mm)*(lm+mm)))*t - s_m[:] = a_lm*f[lmax]*ylm[lmax,lmax-1] + f[lmax-1]*ylm[lmax-1,lmax-1] - elif ((m <= (lmax-2)) and (m >= 1)): - s_mm_minus_2 = f[lmax]*ylm[lmax,m] - a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/((lm-mm)*(lm+mm)))*t - s_mm_minus_1 = a_lm*s_mm_minus_2 + f[lmax-1]*ylm[lmax-1,m] - for l in range(lmax-2, m-1, -1): + if m == lmax: + cs_m[:] = f[lmax] * ylm[lmax, lmax] + elif m == (lmax - 1): + a_lm = ( + np.sqrt( + ((2.0 * lm - 1.0) * (2.0 * lm + 1.0)) / ((lm - mm) * (lm + mm)) + ) + * t + ) + cs_m[:] = ( + a_lm * f[lmax] * ylm[lmax, lmax - 1] + + f[lmax - 1] * ylm[lmax - 1, lmax - 1] + ) + elif (m <= (lmax - 2)) and (m >= 1): + s_mm_minus_2 = f[lmax] * ylm[lmax, m] + a_lm = ( + np.sqrt( + ((2.0 * lm - 1.0) * (2.0 * lm + 1.0)) / ((lm - mm) * (lm + mm)) + ) + * t + ) + s_mm_minus_1 = a_lm * s_mm_minus_2 + f[lmax - 1] * ylm[lmax - 1, m] + for l in range(lmax - 2, m - 1, -1): ll = np.float64(l) - a_lm=np.sqrt(((2.0*ll+1.0)*(2.0*ll+3.0))/((ll+1.0-mm)*(ll+1.0+mm)))*t - b_lm=np.sqrt(((2.*ll+5.)*(ll+mm+1.)*(ll-mm+1.))/((ll+2.-mm)*(ll+2.+mm)*(2.*ll+1.))) - s_mm_l = a_lm * s_mm_minus_1 - b_lm * s_mm_minus_2 + f[l]*ylm[l,m] + a_lm = ( + np.sqrt( + ((2.0 * ll + 1.0) * (2.0 * ll + 3.0)) + / ((ll + 1.0 - mm) * (ll + 1.0 + mm)) + ) + * t + ) + b_lm = np.sqrt( + ((2.0 * ll + 5.0) * (ll + mm + 1.0) * (ll - mm + 1.0)) + / ((ll + 2.0 - mm) * (ll + 2.0 + mm) * (2.0 * ll + 1.0)) + ) + s_mm_l = ( + a_lm * s_mm_minus_1 - b_lm * s_mm_minus_2 + f[l] * ylm[l, m] + ) s_mm_minus_2 = np.copy(s_mm_minus_1) s_mm_minus_1 = np.copy(s_mm_l) - s_m[:] = np.copy(s_mm_l) - elif (m == 0): - s_mm_minus_2 = f[lmax]*ylm[lmax,0] - a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/(lm*lm))*t - s_mm_minus_1 = a_lm * s_mm_minus_2 + f[lmax-1]*ylm[lmax-1,0] - for l in range(lmax-2, m-1, -1): + cs_m[:] = np.copy(s_mm_l) + elif m == 0: + s_mm_minus_2 = f[lmax] * ylm[lmax, 0] + a_lm = np.sqrt(((2.0 * lm - 1.0) * (2.0 * lm + 1.0)) / (lm * lm)) * t + s_mm_minus_1 = a_lm * s_mm_minus_2 + f[lmax - 1] * ylm[lmax - 1, 0] + for l in range(lmax - 2, m - 1, -1): ll = np.float64(l) - a_lm=np.sqrt(((2.0*ll+1.0)*(2.0*ll+3.0))/((ll+1.0)*(ll+1.0)))*t - b_lm=np.sqrt(((2.0*ll+5.0)*(ll+1.0)*(ll+1.0))/((ll+2.0)*(ll+2.0)*(2.0*ll+1.0))) - s_mm_l = a_lm * s_mm_minus_1 - b_lm * s_mm_minus_2 + f[l]*ylm[l,0] + a_lm = ( + np.sqrt( + ((2.0 * ll + 1.0) * (2.0 * ll + 3.0)) + / ((ll + 1.0) * (ll + 1.0)) + ) + * t + ) + b_lm = np.sqrt( + ((2.0 * ll + 5.0) * (ll + 1.0) * (ll + 1.0)) + / ((ll + 2.0) * (ll + 2.0) * (2.0 * ll + 1.0)) + ) + s_mm_l = ( + a_lm * s_mm_minus_1 - b_lm * s_mm_minus_2 + f[l] * ylm[l, 0] + ) s_mm_minus_2 = np.copy(s_mm_minus_1) s_mm_minus_1 = np.copy(s_mm_l) - s_m[:] = np.copy(s_mm_l) - # return rescaled s_m - return s_m/SCALE + cs_m[:] = np.copy(s_mm_l) + # return rescaled cs_m + return cs_m / SCALE diff --git a/gravity_toolkit/degree_amplitude.py b/gravity_toolkit/degree_amplitude.py index 64a8f5da..85e2adc7 100755 --- a/gravity_toolkit/degree_amplitude.py +++ b/gravity_toolkit/degree_amplitude.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" degree_amplitude.py Written Tyler Sutterley (03/2023) @@ -28,14 +28,16 @@ Updated 05/2015: added parameter MMAX for MMAX != LMAX Written 07/2013 """ + import numpy as np + def degree_amplitude( - clm, - slm, - LMAX=None, - MMAX=None, - ): + clm, + slm, + LMAX=None, + MMAX=None, +): """ Calculates the amplitude of each spherical harmonic degree @@ -59,7 +61,7 @@ def degree_amplitude( clm = np.atleast_3d(clm) slm = np.atleast_3d(slm) # check shape - LMp1,MMp1,nt = np.shape(clm) + LMp1, MMp1, nt = np.shape(clm) # upper bound of spherical harmonic degrees if LMAX is None: @@ -69,11 +71,13 @@ def degree_amplitude( MMAX = MMp1 - 1 # allocating for output array - amp = np.zeros((LMAX+1,nt)) - for l in range(LMAX+1): - m = np.arange(0,MMAX+1) + amp = np.zeros((LMAX + 1, nt)) + for l in range(LMAX + 1): + m = np.arange(0, MMAX + 1) # degree amplitude of spherical harmonic degree - amp[l,:] = np.sqrt(np.sum(clm[l,m,:]**2 + slm[l,m,:]**2,axis=0)) + amp[l, :] = np.sqrt( + np.sum(clm[l, m, :] ** 2 + slm[l, m, :] ** 2, axis=0) + ) # return the degree amplitude with singleton dimensions removed return np.squeeze(amp) diff --git a/gravity_toolkit/destripe_harmonics.py b/gravity_toolkit/destripe_harmonics.py index 932c0e07..4e881633 100644 --- a/gravity_toolkit/destripe_harmonics.py +++ b/gravity_toolkit/destripe_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" destripe_harmonics.py Original Fortran program remove_errors.f written by Isabella Velicogna Adapted by Chia-Wei Hsu (05/2018) @@ -56,17 +56,19 @@ Updated 05/2015: added parameter MMAX for MMAX != LMAX Updated 02/2014: generalization for GRACE GUI and other routines """ + import numpy as np + def destripe_harmonics( - clm1, - slm1, - LMIN=2, - LMAX=60, - MMAX=None, - ROUND=True, - NARROW=False, - ): + clm1, + slm1, + LMIN=2, + LMAX=60, + MMAX=None, + ROUND=True, + NARROW=False, +): """ Filters spherical harmonic coefficients for correlated striping errors :cite:p:`Swenson:2006hu` @@ -110,14 +112,14 @@ def destripe_harmonics( # matrix size declarations clmeven = np.zeros((LMAX), dtype=np.float64) slmeven = np.zeros((LMAX), dtype=np.float64) - clmodd = np.zeros((LMAX+1), dtype=np.float64) - slmodd = np.zeros((LMAX+1), dtype=np.float64) - clmsm = np.zeros((LMAX+1, MMAX+1), dtype=np.float64) - slmsm = np.zeros((LMAX+1, MMAX+1), dtype=np.float64) + clmodd = np.zeros((LMAX + 1), dtype=np.float64) + slmodd = np.zeros((LMAX + 1), dtype=np.float64) + clmsm = np.zeros((LMAX + 1, MMAX + 1), dtype=np.float64) + slmsm = np.zeros((LMAX + 1, MMAX + 1), dtype=np.float64) # start of the smoothing over orders (m) - for m in range(int(MMAX+1)): - smooth = np.exp(-np.float64(m)/10.0)*15.0 + for m in range(int(MMAX + 1)): + smooth = np.exp(-np.float64(m) / 10.0) * 15.0 if ROUND: # round(smooth) to nearest even instead of int(smooth) nsmooth = np.around(smooth) @@ -125,28 +127,28 @@ def destripe_harmonics( # Sean's method for finding nsmooth (use floor of smooth) nsmooth = np.int64(smooth) - if (nsmooth < 2): + if nsmooth < 2: # Isabella's method of picking nsmooth sets minimum to 2 nsmooth = np.int64(2) - rmat = np.zeros((3,3), dtype=np.float64) - lll = np.arange(np.float64(nsmooth)*2.+1.)-np.float64(nsmooth) + rmat = np.zeros((3, 3), dtype=np.float64) + lll = np.arange(np.float64(nsmooth) * 2.0 + 1.0) - np.float64(nsmooth) # create design matrix to have the following form: # [ 1 ll ll^2 ] # [ ll ll^2 ll^3 ] # [ ll^2 ll^3 ll^4 ] - for i,ill in enumerate(lll): - rmat[0,0] += 1.0 - rmat[0,1] += ill - rmat[0,2] += ill**2 + for i, ill in enumerate(lll): + rmat[0, 0] += 1.0 + rmat[0, 1] += ill + rmat[0, 2] += ill**2 - rmat[1,0] += ill - rmat[1,1] += ill**2 - rmat[1,2] += ill**3 + rmat[1, 0] += ill + rmat[1, 1] += ill**2 + rmat[1, 2] += ill**3 - rmat[2,0] += ill**2 - rmat[2,1] += ill**3 - rmat[2,2] += ill**4 + rmat[2, 0] += ill**2 + rmat[2, 1] += ill**3 + rmat[2, 2] += ill**4 # put the even and odd l's into their own arrays ieven = -1 @@ -154,133 +156,157 @@ def destripe_harmonics( leven = np.zeros((LMAX), dtype=np.int64) lodd = np.zeros((LMAX), dtype=np.int64) - for l in range(int(m),int(LMAX+1)): + for l in range(int(m), int(LMAX + 1)): # check if degree is odd or even - if np.remainder(l,2).astype(bool): + if np.remainder(l, 2).astype(bool): iodd += 1 lodd[iodd] = l - clmodd[iodd] = clm1[l,m].copy() - slmodd[iodd] = slm1[l,m].copy() + clmodd[iodd] = clm1[l, m].copy() + slmodd[iodd] = slm1[l, m].copy() else: ieven += 1 leven[ieven] = l - clmeven[ieven] = clm1[l,m].copy() - slmeven[ieven] = slm1[l,m].copy() + clmeven[ieven] = clm1[l, m].copy() + slmeven[ieven] = slm1[l, m].copy() # smooth, by fitting a quadratic polynomial to 7 points at a time # deal with even stokes coefficients l1 = 0 l2 = ieven - if (l1 > (l2-2*nsmooth)): - for l in range(l1,l2+1): + if l1 > (l2 - 2 * nsmooth): + for l in range(l1, l2 + 1): if NARROW: # Sean's method # Clm=Slm=0 if number of points is less than window size - clmsm[leven[l],m] = 0.0 - slmsm[leven[l],m] = 0.0 + clmsm[leven[l], m] = 0.0 + slmsm[leven[l], m] = 0.0 else: # Isabella's method # Clm and Slm passed through unaltered - clmsm[leven[l],m] = clm1[leven[l],m].copy() - slmsm[leven[l],m] = slm1[leven[l],m].copy() + clmsm[leven[l], m] = clm1[leven[l], m].copy() + slmsm[leven[l], m] = slm1[leven[l], m].copy() else: - for l in range(int(l1+nsmooth),int(l2-nsmooth+1)): + for l in range(int(l1 + nsmooth), int(l2 - nsmooth + 1)): rhsc = np.zeros((3), dtype=np.float64) rhss = np.zeros((3), dtype=np.float64) - for ll in range(int(-nsmooth),int(nsmooth+1)): - rhsc[0] += clmeven[l+ll] - rhsc[1] += clmeven[l+ll]*np.float64(ll) - rhsc[2] += clmeven[l+ll]*np.float64(ll**2) - rhss[0] += slmeven[l+ll] - rhss[1] += slmeven[l+ll]*np.float64(ll) - rhss[2] += slmeven[l+ll]*np.float64(ll**2) + for ll in range(int(-nsmooth), int(nsmooth + 1)): + rhsc[0] += clmeven[l + ll] + rhsc[1] += clmeven[l + ll] * np.float64(ll) + rhsc[2] += clmeven[l + ll] * np.float64(ll**2) + rhss[0] += slmeven[l + ll] + rhss[1] += slmeven[l + ll] * np.float64(ll) + rhss[2] += slmeven[l + ll] * np.float64(ll**2) # fit design matrix to coefficients # to get beta parameters - bhsc = np.linalg.lstsq(rmat,rhsc.T,rcond=-1)[0] - bhss = np.linalg.lstsq(rmat,rhss.T,rcond=-1)[0] + bhsc = np.linalg.lstsq(rmat, rhsc.T, rcond=-1)[0] + bhss = np.linalg.lstsq(rmat, rhss.T, rcond=-1)[0] # all other l is assigned as bhsc - clmsm[leven[l],m] = bhsc[0].copy() + clmsm[leven[l], m] = bhsc[0].copy() # all other l is assigned as bhss - slmsm[leven[l],m] = bhss[0].copy() + slmsm[leven[l], m] = bhss[0].copy() - if (l == (l1+nsmooth)): + if l == (l1 + nsmooth): # deal with l=l1+nsmooth - for ll in range(int(-nsmooth),0): - clmsm[leven[l+ll],m] = bhsc[0]+bhsc[1]*np.float64(ll) + \ - bhsc[2]*np.float64(ll**2) - slmsm[leven[l+ll],m] = bhss[0]+bhss[1]*np.float64(ll) + \ - bhss[2]*np.float64(ll**2) - - if (l == (l2-nsmooth)): + for ll in range(int(-nsmooth), 0): + clmsm[leven[l + ll], m] = ( + bhsc[0] + + bhsc[1] * np.float64(ll) + + bhsc[2] * np.float64(ll**2) + ) + slmsm[leven[l + ll], m] = ( + bhss[0] + + bhss[1] * np.float64(ll) + + bhss[2] * np.float64(ll**2) + ) + + if l == (l2 - nsmooth): # deal with l=l2-nsmnooth - for ll in range(1,int(nsmooth+1)): - clmsm[leven[l+ll],m] = bhsc[0]+bhsc[1]*np.float64(ll) + \ - bhsc[2]*np.float64(ll**2) - slmsm[leven[l+ll],m] = bhss[0]+bhss[1]*np.float64(ll) + \ - bhss[2]*np.float64(ll**2) + for ll in range(1, int(nsmooth + 1)): + clmsm[leven[l + ll], m] = ( + bhsc[0] + + bhsc[1] * np.float64(ll) + + bhsc[2] * np.float64(ll**2) + ) + slmsm[leven[l + ll], m] = ( + bhss[0] + + bhss[1] * np.float64(ll) + + bhss[2] * np.float64(ll**2) + ) # deal with odd stokes coefficients l1 = 0 l2 = iodd - if (l1 > (l2-2*nsmooth)): - for l in range(l1,l2+1): + if l1 > (l2 - 2 * nsmooth): + for l in range(l1, l2 + 1): if NARROW: # Sean's method # Clm=Slm=0 if number of points is less than window size - clmsm[lodd[l],m] = 0.0 - slmsm[lodd[l],m] = 0.0 + clmsm[lodd[l], m] = 0.0 + slmsm[lodd[l], m] = 0.0 else: # Isabella's method # Clm and Slm passed through unaltered - clmsm[lodd[l],m] = clm1[lodd[l],m].copy() - slmsm[lodd[l],m] = slm1[lodd[l],m].copy() + clmsm[lodd[l], m] = clm1[lodd[l], m].copy() + slmsm[lodd[l], m] = slm1[lodd[l], m].copy() else: - for l in range(int(l1+nsmooth),int(l2-nsmooth+1)): + for l in range(int(l1 + nsmooth), int(l2 - nsmooth + 1)): rhsc = np.zeros((3), dtype=np.float64) rhss = np.zeros((3), dtype=np.float64) - for ll in range(int(-nsmooth),int(nsmooth+1)): - rhsc[0] += clmodd[l+ll] - rhsc[1] += clmodd[l+ll]*np.float64(ll) - rhsc[2] += clmodd[l+ll]*np.float64(ll**2) - rhss[0] += slmodd[l+ll] - rhss[1] += slmodd[l+ll]*np.float64(ll) - rhss[2] += slmodd[l+ll]*np.float64(ll**2) + for ll in range(int(-nsmooth), int(nsmooth + 1)): + rhsc[0] += clmodd[l + ll] + rhsc[1] += clmodd[l + ll] * np.float64(ll) + rhsc[2] += clmodd[l + ll] * np.float64(ll**2) + rhss[0] += slmodd[l + ll] + rhss[1] += slmodd[l + ll] * np.float64(ll) + rhss[2] += slmodd[l + ll] * np.float64(ll**2) # fit design matrix to coefficients # to get beta parameters - bhsc = np.linalg.lstsq(rmat,rhsc.T,rcond=-1)[0] - bhss = np.linalg.lstsq(rmat,rhss.T,rcond=-1)[0] + bhsc = np.linalg.lstsq(rmat, rhsc.T, rcond=-1)[0] + bhss = np.linalg.lstsq(rmat, rhss.T, rcond=-1)[0] # all other l is assigned as bhsc - clmsm[lodd[l],m] = bhsc[0].copy() + clmsm[lodd[l], m] = bhsc[0].copy() # all other l is assigned as bhss - slmsm[lodd[l],m] = bhss[0].copy() + slmsm[lodd[l], m] = bhss[0].copy() - if (l == (l1+nsmooth)): + if l == (l1 + nsmooth): # deal with l=l1+nsmooth - for ll in range(int(-nsmooth),0): - clmsm[lodd[l+ll],m] = bhsc[0]+bhsc[1]*np.float64(ll) + \ - bhsc[2]*np.float64(ll**2) - slmsm[lodd[l+ll],m] = bhss[0]+bhss[1]*np.float64(ll) + \ - bhss[2]*np.float64(ll**2) - - if (l == (l2-nsmooth)): + for ll in range(int(-nsmooth), 0): + clmsm[lodd[l + ll], m] = ( + bhsc[0] + + bhsc[1] * np.float64(ll) + + bhsc[2] * np.float64(ll**2) + ) + slmsm[lodd[l + ll], m] = ( + bhss[0] + + bhss[1] * np.float64(ll) + + bhss[2] * np.float64(ll**2) + ) + + if l == (l2 - nsmooth): # deal with l=l2-nsmnooth - for ll in range(1,int(nsmooth+1)): - clmsm[lodd[l+ll],m] = bhsc[0]+bhsc[1]*np.float64(ll) + \ - bhsc[2]*np.float64(ll**2) - slmsm[lodd[l+ll],m] = bhss[0]+bhss[1]*np.float64(ll) + \ - bhss[2]*np.float64(ll**2) + for ll in range(1, int(nsmooth + 1)): + clmsm[lodd[l + ll], m] = ( + bhsc[0] + + bhsc[1] * np.float64(ll) + + bhsc[2] * np.float64(ll**2) + ) + slmsm[lodd[l + ll], m] = ( + bhss[0] + + bhss[1] * np.float64(ll) + + bhss[2] * np.float64(ll**2) + ) # deal with m greater than or equal to 5 - for l in range(int(m),int(LMAX+1)): - if (m >= 5): + for l in range(int(m), int(LMAX + 1)): + if m >= 5: # remove smoothed clm/slm from original spherical harmonics - Wclm[l,m] -= clmsm[l,m] - Wslm[l,m] -= slmsm[l,m] + Wclm[l, m] -= clmsm[l, m] + Wslm[l, m] -= slmsm[l, m] - return {'clm':Wclm,'slm':Wslm} + return {'clm': Wclm, 'slm': Wslm} diff --git a/gravity_toolkit/fourier_legendre.py b/gravity_toolkit/fourier_legendre.py index 44f60826..521b2cf9 100755 --- a/gravity_toolkit/fourier_legendre.py +++ b/gravity_toolkit/fourier_legendre.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" fourier_legendre.py Original IDL code gen_plms.pro written by Sean Swenson Adapted by Tyler Sutterley (07/2026) @@ -29,9 +29,11 @@ Updated 06/2019: using Python3 compatible division Written 04/2013 """ + from __future__ import division import numpy as np + def fourier_legendre(lmax, mmax): """ Computes Fourier coefficients of the associated Legendre functions @@ -51,168 +53,254 @@ def fourier_legendre(lmax, mmax): """ # allocate for output fourier coefficients - Almk = np.zeros((lmax+1,lmax+1,lmax+1)) - l_even = np.arange(0,lmax+1,2) - l_odd = np.arange(1,lmax,2) - m_even = np.arange(0,mmax+1,2) - m_odd = np.arange(1,mmax,2) + Almk = np.zeros((lmax + 1, lmax + 1, lmax + 1)) + l_even = np.arange(0, lmax + 1, 2) + l_odd = np.arange(1, lmax, 2) + m_even = np.arange(0, mmax + 1, 2) + m_odd = np.arange(1, mmax, 2) # First compute m=0, m=1 terms # Compute m = 0, l = even terms - Almk[l_even,0,0] = 1.0 - a1 = (l_even*(l_even+1.0))*Almk[l_even,0,0] - Almk[l_even,0,2] = a1 / (l_even*(l_even+1.0)-2.0) - for j in range(2,lmax,2):# equivalent to 2:lmax-2 - a1 = 2.0*(l_even*(l_even+1.0)-j**2.0)*Almk[l_even,0,j] - a2 = ((j-2.0)*(j-1.0)-l_even*(l_even+1.0))*Almk[l_even,0,j-2] - dfactor = (l_even*(l_even+1.0)-(j+2.0)*(j+1.0)) - Almk[l_even,0,j+2] = (a1 + a2) / dfactor - + Almk[l_even, 0, 0] = 1.0 + a1 = (l_even * (l_even + 1.0)) * Almk[l_even, 0, 0] + Almk[l_even, 0, 2] = a1 / (l_even * (l_even + 1.0) - 2.0) + for j in range(2, lmax, 2): # equivalent to 2:lmax-2 + a1 = 2.0 * (l_even * (l_even + 1.0) - j**2.0) * Almk[l_even, 0, j] + a2 = ((j - 2.0) * (j - 1.0) - l_even * (l_even + 1.0)) * Almk[ + l_even, 0, j - 2 + ] + dfactor = l_even * (l_even + 1.0) - (j + 2.0) * (j + 1.0) + Almk[l_even, 0, j + 2] = (a1 + a2) / dfactor # Special case for j = 0 fourier coefficient - Almk[l_even,0,0] = Almk[l_even,0,0]/2.0 + Almk[l_even, 0, 0] = Almk[l_even, 0, 0] / 2.0 # Normalize overall sum to 2 for m == 0 norm = np.zeros((len(l_even))) - for j in range(0,lmax+2,2):# equivalent to 0:lmax - ptemp = np.squeeze(Almk[l_even[:, np.newaxis],0,m_even]) - dtemp = 1.0/(1.0-j-m_even) + 1.0/(1.0+j-m_even) + \ - 1.0/(1.0-j+m_even) + 1.0/(1.0+j+m_even) - norm[l_even//2] = norm[l_even//2] + Almk[l_even,0,j] * \ - np.dot(ptemp, dtemp)/2.0 + for j in range(0, lmax + 2, 2): # equivalent to 0:lmax + ptemp = np.squeeze(Almk[l_even[:, np.newaxis], 0, m_even]) + dtemp = ( + 1.0 / (1.0 - j - m_even) + + 1.0 / (1.0 + j - m_even) + + 1.0 / (1.0 - j + m_even) + + 1.0 / (1.0 + j + m_even) + ) + norm[l_even // 2] = ( + norm[l_even // 2] + Almk[l_even, 0, j] * np.dot(ptemp, dtemp) / 2.0 + ) # normalize Almks - norm = np.sqrt(norm/2.0) - for l in range(0,lmax+2,2):# equivalent to 0:lmax - Almk[l,0,:] = Almk[l,0,:]/norm[l//2] - + norm = np.sqrt(norm / 2.0) + for l in range(0, lmax + 2, 2): # equivalent to 0:lmax + Almk[l, 0, :] = Almk[l, 0, :] / norm[l // 2] # Compute m = 0, l = odd terms - Almk[l_odd,0,1] = 1.0 - a1 = (2.0-l_odd*(l_odd+1.0))*Almk[l_odd,0,1] - Almk[l_odd,0,3] = a1 / (6.0-l_odd*(l_odd+1.0)) - for j in range(3,lmax-1,2):# equivalent to 3:lmax-3 - a1 = 2.0*(l_odd*(l_odd+1.0)-j**2.0)*Almk[l_odd,0,j] - a2 = ((j-2.0)*(j-1.0)-l_odd*(l_odd+1.0))*Almk[l_odd,0,j-2] - dfactor = (l_odd*(l_odd+1.0)-(j+2.0)*(j+1.0)) - Almk[l_odd,0,j+2] = (a1 + a2) / dfactor + Almk[l_odd, 0, 1] = 1.0 + a1 = (2.0 - l_odd * (l_odd + 1.0)) * Almk[l_odd, 0, 1] + Almk[l_odd, 0, 3] = a1 / (6.0 - l_odd * (l_odd + 1.0)) + for j in range(3, lmax - 1, 2): # equivalent to 3:lmax-3 + a1 = 2.0 * (l_odd * (l_odd + 1.0) - j**2.0) * Almk[l_odd, 0, j] + a2 = ((j - 2.0) * (j - 1.0) - l_odd * (l_odd + 1.0)) * Almk[ + l_odd, 0, j - 2 + ] + dfactor = l_odd * (l_odd + 1.0) - (j + 2.0) * (j + 1.0) + Almk[l_odd, 0, j + 2] = (a1 + a2) / dfactor # Normalize overall sum to 2 for m == 0 norm = np.zeros((len(l_odd))) - for j in range(1,lmax+1,2):# equivalent to 1:lmax-1 - ptemp = np.squeeze(Almk[l_odd[:, np.newaxis],0,m_odd]) - dtemp = 1.0/(1.0-j-m_odd) + 1.0/(1.0+j-m_odd) + \ - 1.0/(1.0-j+m_odd) + 1.0/(1.0+j+m_odd) - norm[(l_odd-1)//2] = norm[(l_odd-1)//2] + Almk[l_odd,0,j] * \ - np.dot(ptemp, dtemp)/2.0 + for j in range(1, lmax + 1, 2): # equivalent to 1:lmax-1 + ptemp = np.squeeze(Almk[l_odd[:, np.newaxis], 0, m_odd]) + dtemp = ( + 1.0 / (1.0 - j - m_odd) + + 1.0 / (1.0 + j - m_odd) + + 1.0 / (1.0 - j + m_odd) + + 1.0 / (1.0 + j + m_odd) + ) + norm[(l_odd - 1) // 2] = ( + norm[(l_odd - 1) // 2] + + Almk[l_odd, 0, j] * np.dot(ptemp, dtemp) / 2.0 + ) # normalize Almks - norm = np.sqrt(norm/2.0) - for l in range(1,lmax+1,2):# equivalent to 1:lmax-1 - Almk[l,0,:] = Almk[l,0,:]/norm[(l-1)//2] - + norm = np.sqrt(norm / 2.0) + for l in range(1, lmax + 1, 2): # equivalent to 1:lmax-1 + Almk[l, 0, :] = Almk[l, 0, :] / norm[(l - 1) // 2] # Compute m = 1, l = even terms - Almk[l_even,1,0] = 0.0 - Almk[l_even,1,2] = 1.0 - for j in range(2,lmax,2):# equivalent to 2:lmax-2 - a1 = 2.0*(l_even*(l_even+1)-j**2.0-2.0)*Almk[l_even,1,j] - a2 = ((j-2.0)*(j-1.0)-l_even*(l_even+1))*Almk[l_even,1,j-2] - dfactor = (l_even*(l_even+1.0)-(j+2.0)*(j+1.0)) - Almk[l_even,1,j+2] = (a1 + a2) / dfactor + Almk[l_even, 1, 0] = 0.0 + Almk[l_even, 1, 2] = 1.0 + for j in range(2, lmax, 2): # equivalent to 2:lmax-2 + a1 = 2.0 * (l_even * (l_even + 1) - j**2.0 - 2.0) * Almk[l_even, 1, j] + a2 = ((j - 2.0) * (j - 1.0) - l_even * (l_even + 1)) * Almk[ + l_even, 1, j - 2 + ] + dfactor = l_even * (l_even + 1.0) - (j + 2.0) * (j + 1.0) + Almk[l_even, 1, j + 2] = (a1 + a2) / dfactor # Normalize overall sum to 4 for m == 1 # different norm than that of the cosine series norm = np.zeros((len(l_even))) - for j in range(0,lmax+2,2):# equivalent to 0:lmax - ptemp = np.squeeze(Almk[l_even[:, np.newaxis],1,m_even]) - dtemp = -1.0/(1.0-j-m_even) + 1.0/(1+j-m_even) + \ - 1.0/(1.0-j+m_even) - 1.0/(1+j+m_even) - norm[l_even//2] = norm[l_even//2] + Almk[l_even,1,j] * \ - np.dot(ptemp, dtemp)/2.0 + for j in range(0, lmax + 2, 2): # equivalent to 0:lmax + ptemp = np.squeeze(Almk[l_even[:, np.newaxis], 1, m_even]) + dtemp = ( + -1.0 / (1.0 - j - m_even) + + 1.0 / (1 + j - m_even) + + 1.0 / (1.0 - j + m_even) + - 1.0 / (1 + j + m_even) + ) + norm[l_even // 2] = ( + norm[l_even // 2] + Almk[l_even, 1, j] * np.dot(ptemp, dtemp) / 2.0 + ) # normalize Almks - norm = np.sqrt(norm/4.0) - for l in range(0,lmax+2,2):# equivalent to 0:lmax - Almk[l,1,:] = Almk[l,1,:]/norm[l//2] + norm = np.sqrt(norm / 4.0) + for l in range(0, lmax + 2, 2): # equivalent to 0:lmax + Almk[l, 1, :] = Almk[l, 1, :] / norm[l // 2] # Compute m = 1, l = odd terms - Almk[l_odd,1,1] = 1.0 - Almk[l_odd,1,3] = 3.0*(l_odd*(l_odd+1)-2)*Almk[l_odd,1,1]/(l_odd*(l_odd+1)-6) - for j in range(3,lmax-1,2):# equivalent to 3:lmax-3 - a1 = 2.0*(l_odd*(l_odd+1.0)-j**2.0-2.0)*Almk[l_odd,1,j] - a2 = ((j-2.0)*(j-1.0)-l_odd*(l_odd+1.0))*Almk[l_odd,1,j-2] - dfactor = (l_odd*(l_odd+1.0)-(j+2.0)*(j+1.0)) - Almk[l_odd,1,j+2] = (a1 + a2) / dfactor + Almk[l_odd, 1, 1] = 1.0 + Almk[l_odd, 1, 3] = ( + 3.0 + * (l_odd * (l_odd + 1) - 2) + * Almk[l_odd, 1, 1] + / (l_odd * (l_odd + 1) - 6) + ) + for j in range(3, lmax - 1, 2): # equivalent to 3:lmax-3 + a1 = 2.0 * (l_odd * (l_odd + 1.0) - j**2.0 - 2.0) * Almk[l_odd, 1, j] + a2 = ((j - 2.0) * (j - 1.0) - l_odd * (l_odd + 1.0)) * Almk[ + l_odd, 1, j - 2 + ] + dfactor = l_odd * (l_odd + 1.0) - (j + 2.0) * (j + 1.0) + Almk[l_odd, 1, j + 2] = (a1 + a2) / dfactor # Normalize overall sum to 4 for m == 1 norm = np.zeros((len(l_odd))) - for j in range(1,lmax+1,2):# equivalent to 1:lmax-1 - ptemp = np.squeeze(Almk[l_odd[:, np.newaxis],1,m_odd]) - dtemp = -1.0/(1.0-j-m_odd) + 1.0/(1.0+j-m_odd) + \ - 1.0/(1.0-j+m_odd) - 1.0/(1.0+j+m_odd) - norm[(l_odd-1)//2] = norm[(l_odd-1)//2] + Almk[l_odd,1,j] * \ - np.dot(ptemp, dtemp)/2.0 + for j in range(1, lmax + 1, 2): # equivalent to 1:lmax-1 + ptemp = np.squeeze(Almk[l_odd[:, np.newaxis], 1, m_odd]) + dtemp = ( + -1.0 / (1.0 - j - m_odd) + + 1.0 / (1.0 + j - m_odd) + + 1.0 / (1.0 - j + m_odd) + - 1.0 / (1.0 + j + m_odd) + ) + norm[(l_odd - 1) // 2] = ( + norm[(l_odd - 1) // 2] + + Almk[l_odd, 1, j] * np.dot(ptemp, dtemp) / 2.0 + ) # normalize Almks - norm = np.sqrt(norm/4.0) - for l in range(1,lmax+1,2):# equivalent to 1:lmax-1 - Almk[l,1,:] = Almk[l,1,:]/norm[(l-1)//2] - + norm = np.sqrt(norm / 4.0) + for l in range(1, lmax + 1, 2): # equivalent to 1:lmax-1 + Almk[l, 1, :] = Almk[l, 1, :] / norm[(l - 1) // 2] # Compute coefficients for m > 0 # m = 0 terms on rhs have different normalization m = 0 # m = 0, l = even terms - for l in range(m,lmax-1):# equivalent to m:lmax-2 - a1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*Almk[l,m,m_even] - a2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*Almk[l+2,m,m_even] - a3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0)/2.0)*Almk[l,m+2,m_even] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)/2.0) - Almk[l+2,m+2,m_even] = (a1 - a2 + a3) / dfactor + for l in range(m, lmax - 1): # equivalent to m:lmax-2 + a1 = ( + np.sqrt((l + m + 2.0) * (l + m + 1.0) / (2.0 * l + 1.0)) + * Almk[l, m, m_even] + ) + a2 = ( + np.sqrt((l - m + 1.0) * (l - m + 2.0) / (2.0 * l + 5.0)) + * Almk[l + 2, m, m_even] + ) + a3 = ( + np.sqrt((l - m) * (l - m - 1.0) / (2.0 * l + 1.0) / 2.0) + * Almk[l, m + 2, m_even] + ) + dfactor = np.sqrt((l + m + 4.0) * (l + m + 3.0) / (2.0 * l + 5.0) / 2.0) + Almk[l + 2, m + 2, m_even] = (a1 - a2 + a3) / dfactor # m = 0, l = odd terms - for l in range(m+1,lmax-1):# equivalent to m+1:lmax-2 - a1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*Almk[l,m,m_odd] - a2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*Almk[l+2,m,m_odd] - a3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0)/2.0)*Almk[l,m+2,m_odd] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)/2.0) - Almk[l+2,m+2,m_odd] = (a1 - a2 + a3) / dfactor + for l in range(m + 1, lmax - 1): # equivalent to m+1:lmax-2 + a1 = ( + np.sqrt((l + m + 2.0) * (l + m + 1.0) / (2.0 * l + 1.0)) + * Almk[l, m, m_odd] + ) + a2 = ( + np.sqrt((l - m + 1.0) * (l - m + 2.0) / (2.0 * l + 5.0)) + * Almk[l + 2, m, m_odd] + ) + a3 = ( + np.sqrt((l - m) * (l - m - 1.0) / (2.0 * l + 1.0) / 2.0) + * Almk[l, m + 2, m_odd] + ) + dfactor = np.sqrt((l + m + 4.0) * (l + m + 3.0) / (2.0 * l + 5.0) / 2.0) + Almk[l + 2, m + 2, m_odd] = (a1 - a2 + a3) / dfactor # m = even terms - for m in range(2,lmax,2):# equivalent to 2:lmax-2 + for m in range(2, lmax, 2): # equivalent to 2:lmax-2 # m = even, > 2, l = even terms - for l in range(m,lmax,2):# equivalent to m:lmax-2 - a1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*Almk[l,m,m_even] - a2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*Almk[l+2,m,m_even] - a3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0))*Almk[l,m+2,m_even] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)) - Almk[l+2,m+2,m_even] = (a1 - a2 + a3) / dfactor + for l in range(m, lmax, 2): # equivalent to m:lmax-2 + a1 = ( + np.sqrt((l + m + 2.0) * (l + m + 1.0) / (2.0 * l + 1.0)) + * Almk[l, m, m_even] + ) + a2 = ( + np.sqrt((l - m + 1.0) * (l - m + 2.0) / (2.0 * l + 5.0)) + * Almk[l + 2, m, m_even] + ) + a3 = ( + np.sqrt((l - m) * (l - m - 1.0) / (2.0 * l + 1.0)) + * Almk[l, m + 2, m_even] + ) + dfactor = np.sqrt((l + m + 4.0) * (l + m + 3.0) / (2.0 * l + 5.0)) + Almk[l + 2, m + 2, m_even] = (a1 - a2 + a3) / dfactor # m = even, > 2, l = odd terms - for l in range(m+1,lmax-1,2): - a1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*Almk[l,m,m_odd] - a2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*Almk[l+2,m,m_odd] - a3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0))*Almk[l,m+2,m_odd] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)) - Almk[l+2,m+2,m_odd] = (a1 - a2 + a3) / dfactor + for l in range(m + 1, lmax - 1, 2): + a1 = ( + np.sqrt((l + m + 2.0) * (l + m + 1.0) / (2.0 * l + 1.0)) + * Almk[l, m, m_odd] + ) + a2 = ( + np.sqrt((l - m + 1.0) * (l - m + 2.0) / (2.0 * l + 5.0)) + * Almk[l + 2, m, m_odd] + ) + a3 = ( + np.sqrt((l - m) * (l - m - 1.0) / (2.0 * l + 1.0)) + * Almk[l, m + 2, m_odd] + ) + dfactor = np.sqrt((l + m + 4.0) * (l + m + 3.0) / (2.0 * l + 5.0)) + Almk[l + 2, m + 2, m_odd] = (a1 - a2 + a3) / dfactor # m = odd terms - for m in range(1,lmax-1,2):# equivalent to 1:lmax-3 + for m in range(1, lmax - 1, 2): # equivalent to 1:lmax-3 # m = odd, > 1, l = even terms - for l in range(m+1,lmax-1,2):# equivalent to m+1,lmax-2 - a1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*Almk[l,m,m_even] - a2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*Almk[l+2,m,m_even] - a3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0))*Almk[l,m+2,m_even] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)) - Almk[l+2,m+2,m_even] = (a1 - a2 + a3) / dfactor + for l in range(m + 1, lmax - 1, 2): # equivalent to m+1,lmax-2 + a1 = ( + np.sqrt((l + m + 2.0) * (l + m + 1.0) / (2.0 * l + 1.0)) + * Almk[l, m, m_even] + ) + a2 = ( + np.sqrt((l - m + 1.0) * (l - m + 2.0) / (2.0 * l + 5.0)) + * Almk[l + 2, m, m_even] + ) + a3 = ( + np.sqrt((l - m) * (l - m - 1.0) / (2.0 * l + 1.0)) + * Almk[l, m + 2, m_even] + ) + dfactor = np.sqrt((l + m + 4.0) * (l + m + 3.0) / (2.0 * l + 5.0)) + Almk[l + 2, m + 2, m_even] = (a1 - a2 + a3) / dfactor # m = odd, > 1, l = odd terms - for l in range(m,lmax-1,2):# equivalent to m:lmax-2 - a1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*Almk[l,m,m_odd] - a2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*Almk[l+2,m,m_odd] - a3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0))*Almk[l,m+2,m_odd] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)) - Almk[l+2,m+2,m_odd] = (a1 - a2 + a3) / dfactor + for l in range(m, lmax - 1, 2): # equivalent to m:lmax-2 + a1 = ( + np.sqrt((l + m + 2.0) * (l + m + 1.0) / (2.0 * l + 1.0)) + * Almk[l, m, m_odd] + ) + a2 = ( + np.sqrt((l - m + 1.0) * (l - m + 2.0) / (2.0 * l + 5.0)) + * Almk[l + 2, m, m_odd] + ) + a3 = ( + np.sqrt((l - m) * (l - m - 1.0) / (2.0 * l + 1.0)) + * Almk[l, m + 2, m_odd] + ) + dfactor = np.sqrt((l + m + 4.0) * (l + m + 3.0) / (2.0 * l + 5.0)) + Almk[l + 2, m + 2, m_odd] = (a1 - a2 + a3) / dfactor # return the fourier coefficients return Almk + def legendre_gradient(lmax, mmax): """ Calculates functions for evaluating the integral of a @@ -235,32 +323,32 @@ def legendre_gradient(lmax, mmax): # compute the fourier coefficients of the associated legendre functions Almk = fourier_legendre(lmax, mmax) # allocate for output fourier coefficients - Vlmk = np.zeros((lmax+1,lmax+1,lmax+1)) - Wlmk = np.zeros((lmax+1,lmax+1,lmax+1)) + Vlmk = np.zeros((lmax + 1, lmax + 1, lmax + 1)) + Wlmk = np.zeros((lmax + 1, lmax + 1, lmax + 1)) # for each spherical harmonic degree - for l in range(1, lmax+1): + for l in range(1, lmax + 1): # degree dependent factor - dfactor = np.sqrt((2.0*l + 1.0)/(2.0*l - 1.0)) + dfactor = np.sqrt((2.0 * l + 1.0) / (2.0 * l - 1.0)) # m=0 special case - Vfact = np.sqrt(l*(l + 1.0)/2.0) - Vlmk[l,0,:] = Vfact * Almk[l,1,:] - for m in range(2, l+1):# from 2 to l + Vfact = np.sqrt(l * (l + 1.0) / 2.0) + Vlmk[l, 0, :] = Vfact * Almk[l, 1, :] + for m in range(2, l + 1): # from 2 to l Vfact = np.sqrt((l + m) * (l - m + 1.0) / 4.0) Wfact = dfactor * np.sqrt((l - m) * (l - m + 1) / 4.0) - Vlmk[l,m-1,:] = Vfact * Almk[l,m,:] - Wlmk[l,m-1,:] = -Wfact * Almk[l-1,m,:] + Vlmk[l, m - 1, :] = Vfact * Almk[l, m, :] + Wlmk[l, m - 1, :] = -Wfact * Almk[l - 1, m, :] # m = 1 terms - Vfact = np.sqrt(l*(l + 1.0)/2.0) - Wfact = dfactor * np.sqrt(l*(l + 1.0)/2.0) - Vlmk[l,1,:] -= Vfact*Almk[l,0,:] - Wlmk[l,1,:] += dfactor*Wfact*Almk[l-1,0,:] - for m in range(2, l + 1):# from 2 to l + Vfact = np.sqrt(l * (l + 1.0) / 2.0) + Wfact = dfactor * np.sqrt(l * (l + 1.0) / 2.0) + Vlmk[l, 1, :] -= Vfact * Almk[l, 0, :] + Wlmk[l, 1, :] += dfactor * Wfact * Almk[l - 1, 0, :] + for m in range(2, l + 1): # from 2 to l Vfact = np.sqrt((l + m) * (l - m + 1.0) / 4.0) Wfact = dfactor * np.sqrt((l + m) * (l + m - 1) / 4.0) - Vlmk[l,m,:] -= Vfact * Almk[l,m-1,:] - Wlmk[l,m,:] += Wfact * Almk[l-1,m-1,:] + Vlmk[l, m, :] -= Vfact * Almk[l, m - 1, :] + Wlmk[l, m, :] += Wfact * Almk[l - 1, m - 1, :] # normalizations - Vlmk[l,:,:] /= np.sqrt(l * (l + 1.0)) - Wlmk[l,:,:] /= np.sqrt(l * (l + 1.0)) + Vlmk[l, :, :] /= np.sqrt(l * (l + 1.0)) + Wlmk[l, :, :] /= np.sqrt(l * (l + 1.0)) # return the coefficients return (Vlmk, Wlmk) diff --git a/gravity_toolkit/gauss_weights.py b/gravity_toolkit/gauss_weights.py index 29849fba..1649e24f 100755 --- a/gravity_toolkit/gauss_weights.py +++ b/gravity_toolkit/gauss_weights.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gauss_weights.py Original IDL code gauss_weights.pro written by Sean Swenson Adapted by Tyler Sutterley (03/2023) @@ -47,8 +47,10 @@ Updated 02/2014: changed variables from ints to floats to prevent truncation Written 05/2013 """ + import numpy as np + def gauss_weights(hw, LMAX, CUTOFF=1e-10): """ Computes the Gaussian weights as a function of degree using @@ -69,32 +71,34 @@ def gauss_weights(hw, LMAX, CUTOFF=1e-10): degree dependent weighting function """ # allocate for output weights - wl = np.zeros((LMAX+1)) + wl = np.zeros((LMAX + 1)) # radius of the Earth in km rad_e = 6371.0 - if (hw < CUTOFF): + if hw < CUTOFF: # distance is smaller than cutoff - wl[:] = 1.0/(2.0*np.pi) + wl[:] = 1.0 / (2.0 * np.pi) else: # calculate gaussian weights using recursion - b = np.log(2.0)/(1.0 - np.cos(hw/rad_e)) + b = np.log(2.0) / (1.0 - np.cos(hw / rad_e)) # weight for degree 0 - wl[0] = 1.0/(2.0*np.pi) + wl[0] = 1.0 / (2.0 * np.pi) # weight for degree 1 - wl[1] = wl[0]*((1.0+np.exp(-2.0*b))/(1.0-np.exp(-2.0*b))-1.0/b) + wl[1] = wl[0] * ( + (1.0 + np.exp(-2.0 * b)) / (1.0 - np.exp(-2.0 * b)) - 1.0 / b + ) # valid flag valid = True # spherical harmonic degree l = 2 # while valid (within cutoff) # and spherical harmonic degree is less than LMAX - while (valid and (l <= LMAX)): + while valid and (l <= LMAX): # calculate weight with recursion - wl[l] = (1.0-2.0*l)/b*wl[l-1]+wl[l-2] + wl[l] = (1.0 - 2.0 * l) / b * wl[l - 1] + wl[l - 2] # weight is less than cutoff - if (wl[l] < CUTOFF): + if wl[l] < CUTOFF: # set all weights to cutoff - wl[l:LMAX+1] = CUTOFF + wl[l : LMAX + 1] = CUTOFF # set valid flag valid = False # add 1 to l diff --git a/gravity_toolkit/gen_averaging_kernel.py b/gravity_toolkit/gen_averaging_kernel.py index b291906b..41e6aee8 100755 --- a/gravity_toolkit/gen_averaging_kernel.py +++ b/gravity_toolkit/gen_averaging_kernel.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gen_averaging_kernel.py Original IDL code gen_wclms_me.pro written by Sean Swenson Adapted by Tyler Sutterley (06/2023) @@ -54,11 +54,24 @@ Updated 05/2015: added parameter MMAX for MMAX != LMAX Written 05/2013 """ + import numpy as np import gravity_toolkit.units -def gen_averaging_kernel(gclm, gslm, eclm, eslm, sigma, hw, - LMAX=60, MMAX=None, CUTOFF=1e-15, UNITS=0, LOVE=None): + +def gen_averaging_kernel( + gclm, + gslm, + eclm, + eslm, + sigma, + hw, + LMAX=60, + MMAX=None, + CUTOFF=1e-15, + UNITS=0, + LOVE=None, +): r""" Generates averaging kernel coefficients which minimize the total error following :cite:t:`Swenson:2002hs` @@ -108,65 +121,65 @@ def gen_averaging_kernel(gclm, gslm, eclm, eslm, sigma, hw, # Earth Parameters factors = gravity_toolkit.units(lmax=LMAX) # extract arrays of kl, hl, and ll Love Numbers - if (UNITS == 0): + if UNITS == 0: # Input coefficients are fully-normalized dfactor = factors.harmonic(*LOVE).cmwe - elif (UNITS == 1): + elif UNITS == 1: # Inputs coefficients are mass (cmwe) - dfactor = np.ones((LMAX+1)) + dfactor = np.ones((LMAX + 1)) # average radius of the earth (km) - rad_e = factors.rad_e/1e5 + rad_e = factors.rad_e / 1e5 # allocate for gaussian function - gl = np.zeros((LMAX+1)) + gl = np.zeros((LMAX + 1)) # calculate gaussian weights using recursion - b = np.log(2.0)/(1.0-np.cos(hw/rad_e)) + b = np.log(2.0) / (1.0 - np.cos(hw / rad_e)) # weight for degree 0 - gl[0] = (1.0-np.exp(-2.0*b))/b + gl[0] = (1.0 - np.exp(-2.0 * b)) / b # weight for degree 1 - gl[1] = (1.0+np.exp(-2.0*b))/b - (1.0-np.exp(-2.0*b))/b**2 + gl[1] = (1.0 + np.exp(-2.0 * b)) / b - (1.0 - np.exp(-2.0 * b)) / b**2 # valid flag valid = True # spherical harmonic degree l = 2 # generate Legendre coefficients of Gaussian correlation function - while (valid and (l <= LMAX)): - gl[l] = (1.0 - 2.0*l)/b*gl[l-1] + gl[l-2] + while valid and (l <= LMAX): + gl[l] = (1.0 - 2.0 * l) / b * gl[l - 1] + gl[l - 2] # check validity - if (gl[l] < CUTOFF): - gl[l:LMAX+1] = CUTOFF + if gl[l] < CUTOFF: + gl[l : LMAX + 1] = CUTOFF valid = False # add to counter for spherical harmonic degree l += 1 # Convert sigma to correlation function amplitude - area = np.copy(gclm[0,0]) - temp_0 = np.zeros((LMAX+1)) - for l in range(0,LMAX+1):# equivalent to 0:LMAX - mm = np.min([MMAX,l])# find min of MMAX and l - m = np.arange(0,mm+1)# create m array 0:l or 0:MMAX - temp_0[l] = (gl[l]/2.0)*np.sum(gclm[l,m]**2 + gslm[l,m]**2) + area = np.copy(gclm[0, 0]) + temp_0 = np.zeros((LMAX + 1)) + for l in range(0, LMAX + 1): # equivalent to 0:LMAX + mm = np.min([MMAX, l]) # find min of MMAX and l + m = np.arange(0, mm + 1) # create m array 0:l or 0:MMAX + temp_0[l] = (gl[l] / 2.0) * np.sum(gclm[l, m] ** 2 + gslm[l, m] ** 2) # divide by the square of the area under the kernel - temp = np.sum(temp_0)/area**2 + temp = np.sum(temp_0) / area**2 # signal variance - sigma_0 = sigma/np.sqrt(temp) + sigma_0 = sigma / np.sqrt(temp) # Compute averaging kernel coefficients Ylms = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX) - Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) + Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) # for each spherical harmonic degree - for l in range(0,LMAX+1):# equivalent to 0:lmax + for l in range(0, LMAX + 1): # equivalent to 0:lmax # inverse of smoothed signal variance in output units - ldivg = (dfactor[l]**2)/(gl[l]*sigma_0**2) + ldivg = (dfactor[l] ** 2) / (gl[l] * sigma_0**2) # for each valid spherical harmonic order - mm = np.min([MMAX,l]) - for m in range(0,mm+1): - temp = 1.0 + 2.0*ldivg*eclm[l,m]**2 - Ylms.clm[l,m] = gclm[l,m]/temp - temp = 1.0 + 2.0*ldivg*eslm[l,m]**2 - Ylms.slm[l,m] = gslm[l,m]/temp + mm = np.min([MMAX, l]) + for m in range(0, mm + 1): + temp = 1.0 + 2.0 * ldivg * eclm[l, m] ** 2 + Ylms.clm[l, m] = gclm[l, m] / temp + temp = 1.0 + 2.0 * ldivg * eslm[l, m] ** 2 + Ylms.slm[l, m] = gslm[l, m] / temp # return kernels divided by the area under the kernel - return Ylms.scale(1.0/area) + return Ylms.scale(1.0 / area) diff --git a/gravity_toolkit/gen_disc_load.py b/gravity_toolkit/gen_disc_load.py index ca8de548..92e6fc8f 100644 --- a/gravity_toolkit/gen_disc_load.py +++ b/gravity_toolkit/gen_disc_load.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gen_disc_load.py Written by Tyler Sutterley (07/2026) Calculates gravitational spherical harmonic coefficients for a uniform disc load @@ -76,14 +76,17 @@ Updated 08/2017: Using Holmes and Featherstone relation for Plms Written 09/2016 """ + import numpy as np import gravity_toolkit.units import gravity_toolkit.harmonics from gravity_toolkit.associated_legendre import plm_holmes from gravity_toolkit.legendre_polynomials import legendre_polynomials -def gen_disc_load(data, lon, lat, area, LMAX=60, MMAX=None, UNITS=2, - PLM=None, LOVE=None): + +def gen_disc_load( + data, lon, lat, area, LMAX=60, MMAX=None, UNITS=2, PLM=None, LOVE=None +): r""" Calculates spherical harmonic coefficients for a uniform disc load :cite:p:`Holmes:2002ff,Longman:1962ev,Farrell:1972cm,Pollack:1973gi,Jacob:2012eo` @@ -131,95 +134,101 @@ def gen_disc_load(data, lon, lat, area, LMAX=60, MMAX=None, UNITS=2, MMAX = np.copy(LMAX) # convert lon and lat to radians - phi = np.radians(lon)# Longitude in radians - th = np.radians(90.0 - lat)# Colatitude in radians + phi = np.radians(lon) # Longitude in radians + th = np.radians(90.0 - lat) # Colatitude in radians # Earth Parameters factors = gravity_toolkit.units(lmax=LMAX) # 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*factors.rad_e**2)) + alpha = 1.0 - 1e10 * area / (2.0 * np.pi * factors.rad_e**2) # Calculate factor to convert from input units into g/cm^2 if isinstance(UNITS, (list, np.ndarray)): # custom units unit_conv = 1.0 dfactor = np.copy(UNITS) - elif (UNITS == 1): + elif UNITS == 1: # Input data is in cm water equivalent (cmwe) unit_conv = 1.0 # degree dependent factors to convert from coefficients # of mass into normalized geoid coefficients - dfactor = 4.0*np.pi*factors.spatial(*LOVE).cmwe/(1.0 + 2.0*factors.l) - elif (UNITS == 2): + dfactor = ( + 4.0 * np.pi * factors.spatial(*LOVE).cmwe / (1.0 + 2.0 * factors.l) + ) + elif UNITS == 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) + unit_conv = 1e15 / (1e10 * area) # degree dependent factors to convert from coefficients # of mass into normalized geoid coefficients - dfactor = 4.0*np.pi*factors.spatial(*LOVE).cmwe/(1.0 + 2.0*factors.l) - elif (UNITS == 3): + dfactor = ( + 4.0 * np.pi * factors.spatial(*LOVE).cmwe / (1.0 + 2.0 * factors.l) + ) + 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 # degree dependent factors to convert from coefficients # of mass into normalized geoid coefficients - dfactor = 4.0*np.pi*factors.spatial(*LOVE).cmwe/(1.0 + 2.0*factors.l) + dfactor = ( + 4.0 * np.pi * factors.spatial(*LOVE).cmwe / (1.0 + 2.0 * factors.l) + ) else: raise ValueError(f'Unknown units {UNITS}') # Calculating plms of the disc # allocating for constructed array - pl_alpha = np.zeros((LMAX+1)) + 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 + pl_alpha[0] = (1.0 - alpha) / 2.0 # for all other degrees: calculate the legendre polynomials up to LMAX+1 - pl_matrix,_ = legendre_polynomials(LMAX+1,alpha) - for l in range(1, LMAX+1):# LMAX+1 to include LMAX + pl_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 + 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,_ = plm_holmes(LMAX, np.cos(th)) + plmout, _ = plm_holmes(LMAX, np.cos(th)) # truncate precomputed plms to order - plmout = np.squeeze(plmout[:,:MMAX+1,:]) + plmout = np.squeeze(plmout[:, : MMAX + 1, :]) else: # truncate precomputed plms to degree and order - plmout = PLM[:LMAX+1,:MMAX+1] + 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) + m = np.arange(MMAX + 1) # Multiplying by the units conversion factor (unit_conv) to # convert from the input units into cmwe # Multiplying point mass data (converted to cmwe) 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 - d = unit_conv*data*np.exp(1j*m*phi) + d = unit_conv * data * np.exp(1j * m * phi) # Multiplying by plm_alpha (F_l from Jacob 2012) - plm = np.zeros((LMAX+1, MMAX+1)) + plm = np.zeros((LMAX + 1, MMAX + 1)) # Initializing output spherical harmonic matrices Ylms = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX) # rotate disc load to be centered at lat/lon - plm = np.einsum("lm...,l...->lm...", plmout, pl_alpha) + plm = np.einsum('lm...,l...->lm...', plmout, pl_alpha) # multiplying clm by cos(m*phi) and slm by sin(m*phi) # to get a field of spherical harmonics - ylm = np.einsum("lm...,m...->lm...", plm, d) + ylm = np.einsum('lm...,m...->lm...', plm, d) # Multiplying by factors to convert to fully normalized coefficients - Ylms.clm = np.einsum("l...,lm...->lm...", dfactor, ylm.real) - Ylms.slm = np.einsum("l...,lm...->lm...", dfactor, ylm.imag) + Ylms.clm = np.einsum('l...,lm...->lm...', dfactor, ylm.real) + Ylms.slm = np.einsum('l...,lm...->lm...', dfactor, ylm.imag) # return the output spherical harmonics object return Ylms diff --git a/gravity_toolkit/gen_harmonics.py b/gravity_toolkit/gen_harmonics.py index f4d561cd..07b14fb0 100644 --- a/gravity_toolkit/gen_harmonics.py +++ b/gravity_toolkit/gen_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gen_harmonics.py Written by Tyler Sutterley (07/2026) Converts data from the spatial domain to spherical harmonic coefficients @@ -63,11 +63,13 @@ Updated 05/2015: updated output for MMAX != LMAX Written 05/2013 """ + import numpy as np import gravity_toolkit.harmonics from gravity_toolkit.associated_legendre import plm_holmes from gravity_toolkit.fourier_legendre import fourier_legendre + def gen_harmonics(data, lon, lat, **kwargs): """ Converts data from the spatial domain to spherical harmonic coefficients @@ -105,10 +107,10 @@ def gen_harmonics(data, lon, lat, **kwargs): spherical harmonic order to MMAX """ # set default keyword arguments - kwargs.setdefault('LMAX',60) - kwargs.setdefault('MMAX',None) - kwargs.setdefault('PLM',0) - kwargs.setdefault('METHOD','integration') + kwargs.setdefault('LMAX', 60) + kwargs.setdefault('MMAX', None) + kwargs.setdefault('PLM', 0) + kwargs.setdefault('METHOD', 'integration') # upper bound of spherical harmonic orders (default = LMAX) if kwargs['MMAX'] is None: kwargs['MMAX'] = np.copy(kwargs['LMAX']) @@ -119,13 +121,14 @@ def gen_harmonics(data, lon, lat, **kwargs): sz = np.shape(data) dinput = np.transpose(data) if (sz[0] == len(lat)) else np.copy(data) # convert spatial field into spherical harmonics - if (kwargs['METHOD'].lower() == 'integration'): + if kwargs['METHOD'].lower() == 'integration': Ylms = integration(dinput, lon, lat, **kwargs) - elif (kwargs['METHOD'].lower() == 'fourier'): + elif kwargs['METHOD'].lower() == 'fourier': Ylms = fourier(dinput, lon, lat, **kwargs) # return the output spherical harmonics object return Ylms + def integration(data, lon, lat, LMAX=60, MMAX=None, PLM=0, **kwargs): """ Converts data from the spatial domain to spherical harmonic coefficients @@ -160,44 +163,47 @@ def integration(data, lon, lat, LMAX=60, MMAX=None, PLM=0, **kwargs): phi = np.radians(np.squeeze(lon)) th = np.radians(90.0 - np.squeeze(lat)) # reformatting longitudes to range 0:360 (if previously -180:180) - phi = np.where(phi < 0, phi + 2.0*np.pi, phi) + phi = np.where(phi < 0, phi + 2.0 * np.pi, phi) # grid step in radians dphi = np.abs(phi[1] - phi[0]) dth = np.abs(th[1] - th[0]) # LMAX+1 as there are LMAX+1 elements between 0 and LMAX - ll = np.arange(LMAX+1) - mm = np.arange(MMAX+1) + ll = np.arange(LMAX + 1) + mm = np.arange(MMAX + 1) # Calculating cos/sin of phi arrays (output [m,phi]) - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", mm, phi)) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', mm, phi)) # Multiplying sin(th) with differentials of theta and phi # to calculate the integration factor at each latitude - int_fact = np.sin(th)*dphi*dth - coeff = 1.0/(4.0*np.pi) + int_fact = np.sin(th) * dphi * dth + coeff = 1.0 / (4.0 * np.pi) # Calculate polynomials using Holmes and Featherstone (2002) relation - if (np.ndim(PLM) == 0): + if np.ndim(PLM) == 0: PLM, dplm = plm_holmes(LMAX, np.cos(th)) # Multiply plms by integration factors [sin(theta)*dtheta*dphi] # truncate plms to maximum spherical harmonic order if MMAX < LMAX - plm = np.einsum("lmh...,h...->lmh...", PLM[:LMAX+1,:MMAX+1,:], int_fact) + plm = np.einsum( + 'lmh...,h...->lmh...', PLM[: LMAX + 1, : MMAX + 1, :], int_fact + ) # Initializing output spherical harmonic matrices Ylms = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX) - Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) + Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) # Multiplying gridded data with sin/cos of m#phis (output [m,theta]) # This will sum through all phis in the dot product - d = np.einsum("mp...,ph...->mh...", m_phi, data) + d = np.einsum('mp...,ph...->mh...', m_phi, data) # Summing product of plms and data over all latitudes - ylm = np.einsum("lmh...,mh...->lm...", plm, d) + ylm = np.einsum('lmh...,mh...->lm...', plm, d) # convert to output normalization (4-pi normalized harmonics) # truncate to MMAX if specified (if l > MMAX) - Ylms.clm = coeff*ylm.real[:LMAX+1, :MMAX+1] - Ylms.slm = coeff*ylm.imag[:LMAX+1, :MMAX+1] + Ylms.clm = coeff * ylm.real[: LMAX + 1, : MMAX + 1] + Ylms.slm = coeff * ylm.imag[: LMAX + 1, : MMAX + 1] # return the output spherical harmonics object return Ylms + def fourier(data, lon, lat, LMAX=60, MMAX=None, PLM=0, **kwargs): """ Computes the spherical harmonic coefficients of a spatial field @@ -236,16 +242,16 @@ def fourier(data, lon, lat, LMAX=60, MMAX=None, PLM=0, **kwargs): phi = np.radians(np.squeeze(lon)) th = np.radians(90.0 - np.squeeze(lat)) # reformatting longitudes to range 0:360 (if previously -180:180) - phi = np.where(phi < 0, phi + 2.0*np.pi, phi) + phi = np.where(phi < 0, phi + 2.0 * np.pi, phi) # grid step in radians dphi = np.abs(phi[1] - phi[0]) dth = np.abs(th[1] - th[0]) # MMAX+1 to include MMAX - mm = np.arange(MMAX+1) + mm = np.arange(MMAX + 1) # Calculate cos and sin coefficients of signal - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", mm, phi)) - d = np.einsum("mp...,ph...->mh...", m_phi, data) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', mm, phi)) + d = np.einsum('mp...,ph...->mh...', m_phi, data) # normalize coefficients d[0, :] *= 1.0 / nlon d[1:, :] *= 2.0 / nlon @@ -254,41 +260,47 @@ def fourier(data, lon, lat, LMAX=60, MMAX=None, PLM=0, **kwargs): # Because the function is defined on (0,pi) # it can be expanded in just cosine terms. # this routine assumes that 0 and pi are not included - f = np.zeros((MMAX+1,MMAX+1), dtype=np.complex128) - m_even = slice(0, MMAX+1, 2) + f = np.zeros((MMAX + 1, MMAX + 1), dtype=np.complex128) + m_even = slice(0, MMAX + 1, 2) m_odd = slice(1, MMAX, 2) - if np.isclose([th[0],th[nlat-1]], [0.0,np.pi]).all(): + if np.isclose([th[0], th[nlat - 1]], [0.0, np.pi]).all(): # global case (includes poles) # non-endpoints - k_th = np.exp(1j * np.einsum("h...,k...->kh...", th[1:nlat-1], mm)) - f[m_even,:] = 2.0*np.einsum("mh...,kh...->mk", d[m_even,1:nlat-1],k_th.real) - f[m_odd,:] = 2.0*np.einsum("mh...,kh...->mk", d[m_odd,1:nlat-1],k_th.imag) + k_th = np.exp(1j * np.einsum('h...,k...->kh...', th[1 : nlat - 1], mm)) + f[m_even, :] = 2.0 * np.einsum( + 'mh...,kh...->mk', d[m_even, 1 : nlat - 1], k_th.real + ) + f[m_odd, :] = 2.0 * np.einsum( + 'mh...,kh...->mk', d[m_odd, 1 : nlat - 1], k_th.imag + ) # endpoints - k_th = np.exp(1j * mm* th[0]) - f[m_even,:] += np.einsum("m...,k...->mk", d[m_even,0], k_th) - f[m_odd,:] += np.einsum("m...,k...->mk", d[m_odd,0], k_th) - k_th = np.exp(1j * mm * th[nlat-1]) - f[m_even,:] += np.einsum("m...,k...->mk", d[m_even,nlat-1], k_th) - f[m_odd,:] += np.einsum("m...,k...->mk", d[m_odd,nlat-1], k_th) - elif not np.isclose([th[0],th[nlat-1]], [0.0,np.pi]).any(): - k_th = np.exp(1j * np.einsum("h...,k...->kh...", th, mm)) - f[m_even,:] = 2.0*np.einsum("mh...,kh...->mk", d[m_even,:],k_th.real) - f[m_odd,:] = 2.0*np.einsum("mh...,kh...->mk", d[m_odd,:],k_th.imag) + k_th = np.exp(1j * mm * th[0]) + f[m_even, :] += np.einsum('m...,k...->mk', d[m_even, 0], k_th) + f[m_odd, :] += np.einsum('m...,k...->mk', d[m_odd, 0], k_th) + k_th = np.exp(1j * mm * th[nlat - 1]) + f[m_even, :] += np.einsum('m...,k...->mk', d[m_even, nlat - 1], k_th) + f[m_odd, :] += np.einsum('m...,k...->mk', d[m_odd, nlat - 1], k_th) + elif not np.isclose([th[0], th[nlat - 1]], [0.0, np.pi]).any(): + k_th = np.exp(1j * np.einsum('h...,k...->kh...', th, mm)) + f[m_even, :] = 2.0 * np.einsum( + 'mh...,kh...->mk', d[m_even, :], k_th.real + ) + f[m_odd, :] = 2.0 * np.einsum('mh...,kh...->mk', d[m_odd, :], k_th.imag) else: raise ValueError('Latitude coordinates incompatible') # Normalize theta fourier coefficients - f[:,0] *= 1.0/(2.0*nlat) - f[:,1:MMAX+1] *= 1.0/nlat + f[:, 0] *= 1.0 / (2.0 * nlat) + f[:, 1 : MMAX + 1] *= 1.0 / nlat # Correct normalization for the incomplete coverage of the sphere - f[:] *= nlon*dphi/(2.0*np.pi) * nlat*dth/np.pi + f[:] *= nlon * dphi / (2.0 * np.pi) * nlat * dth / np.pi # Calculate cos and sin coefficients of Legendre functions # Expand m = even terms in a cosine series # Expand m = odd terms in a sine series # Both are stride 2 - if (np.ndim(PLM) == 0): + if np.ndim(PLM) == 0: Almk = fourier_legendre(LMAX, MMAX) else: # use precomputed alms to improve computational speed @@ -296,67 +308,87 @@ def fourier(data, lon, lat, LMAX=60, MMAX=None, PLM=0, **kwargs): # Initializing output spherical harmonic matrices Ylms = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX) - Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) + Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) # calculate spherical harmonics for m == even terms # even l terms (l even, m even, k even) - l_even = slice(0, LMAX+1, 2) + l_even = slice(0, LMAX + 1, 2) n_even = np.arange(m_even.start, m_even.stop, m_even.step) k_even = np.zeros((len(n_even), len(n_even))) - for k in range(0,MMAX+2,2): - k_even[:,k//2] = 0.5*(1.0/(1.0-n_even-k) + 1.0/(1.0+n_even-k) + - 1.0/(1.0-n_even+k) + 1.0/(1.0+n_even+k)) + for k in range(0, MMAX + 2, 2): + k_even[:, k // 2] = 0.5 * ( + 1.0 / (1.0 - n_even - k) + + 1.0 / (1.0 + n_even - k) + + 1.0 / (1.0 - n_even + k) + + 1.0 / (1.0 + n_even + k) + ) # calculate summation over coefficients - Aeven = np.einsum("lmk...,kn...->lmn...", Almk[l_even,m_even,m_even], k_even) - Yeven = np.einsum("lmn...,mn...->lm...", Aeven, f[m_even,m_even]) - Ylms.clm[l_even,m_even] = Yeven.real - Ylms.slm[l_even,m_even] = Yeven.imag + Aeven = np.einsum( + 'lmk...,kn...->lmn...', Almk[l_even, m_even, m_even], k_even + ) + Yeven = np.einsum('lmn...,mn...->lm...', Aeven, f[m_even, m_even]) + Ylms.clm[l_even, m_even] = Yeven.real + Ylms.slm[l_even, m_even] = Yeven.imag # odd l terms (l odd, m even, k odd) l_odd = slice(1, LMAX, 2) n_odd = np.arange(m_odd.start, m_odd.stop, m_odd.step) k_odd = np.zeros((len(n_odd), len(n_odd))) - for k in range(1, MMAX+1, 2): - k_odd[:,(k-1)//2] = 0.5*(1.0/(1.0-n_odd-k) + 1.0/(1.0+n_odd-k) + - 1.0/(1.0-n_odd+k) + 1.0/(1.0+n_odd+k)) + for k in range(1, MMAX + 1, 2): + k_odd[:, (k - 1) // 2] = 0.5 * ( + 1.0 / (1.0 - n_odd - k) + + 1.0 / (1.0 + n_odd - k) + + 1.0 / (1.0 - n_odd + k) + + 1.0 / (1.0 + n_odd + k) + ) # calculate summation over coefficients - Aodd = np.einsum("lmk...,kn...->lmn...", Almk[l_odd,m_even,m_odd], k_odd) - Yodd = np.einsum("lmn...,mn...->lm...", Aodd, f[m_even,m_odd]) - Ylms.clm[l_odd,m_even] = Yodd.real - Ylms.slm[l_odd,m_even] = Yodd.imag + Aodd = np.einsum('lmk...,kn...->lmn...', Almk[l_odd, m_even, m_odd], k_odd) + Yodd = np.einsum('lmn...,mn...->lm...', Aodd, f[m_even, m_odd]) + Ylms.clm[l_odd, m_even] = Yodd.real + Ylms.slm[l_odd, m_even] = Yodd.imag # calculate spherical harmonics for m == odd terms # even l terms (l even, m odd, k even) - l_even = slice(2, LMAX+1, 2)# do not in include l=0 + l_even = slice(2, LMAX + 1, 2) # do not in include l=0 n_even = np.arange(m_even.start, m_even.stop, m_even.step) k_even = np.zeros((len(n_even), len(n_even))) - for k in range(0,MMAX+2,2): - k_even[:,k//2] = 0.5*(-1.0/(1.0-n_even-k) + 1.0/(1.0+n_even-k) + - 1.0/(1.0-n_even+k) - 1.0/(1.0+n_even+k)) - Aeven = np.einsum("lmk...,kn...->lmn...", Almk[l_even,m_odd,m_even], k_even) - Yeven = np.einsum("lmn...,mn...->lm...", Aeven, f[m_odd,m_even]) - Ylms.clm[l_even,m_odd] = Yeven.real - Ylms.slm[l_even,m_odd] = Yeven.imag + for k in range(0, MMAX + 2, 2): + k_even[:, k // 2] = 0.5 * ( + -1.0 / (1.0 - n_even - k) + + 1.0 / (1.0 + n_even - k) + + 1.0 / (1.0 - n_even + k) + - 1.0 / (1.0 + n_even + k) + ) + Aeven = np.einsum( + 'lmk...,kn...->lmn...', Almk[l_even, m_odd, m_even], k_even + ) + Yeven = np.einsum('lmn...,mn...->lm...', Aeven, f[m_odd, m_even]) + Ylms.clm[l_even, m_odd] = Yeven.real + Ylms.slm[l_even, m_odd] = Yeven.imag # odd l terms (l odd, m odd, k odd) l_odd = slice(1, LMAX, 2) n_odd = np.arange(m_odd.start, m_odd.stop, m_odd.step) k_odd = np.zeros((len(n_odd), len(n_odd))) - for k in range(1,MMAX+1,2): - k_odd[:,(k-1)//2] = 0.5*(-1.0/(1.0-n_odd-k) + 1.0/(1.0+n_odd-k) + - 1.0/(1.0-n_odd+k) - 1.0/(1.0+n_odd+k)) + for k in range(1, MMAX + 1, 2): + k_odd[:, (k - 1) // 2] = 0.5 * ( + -1.0 / (1.0 - n_odd - k) + + 1.0 / (1.0 + n_odd - k) + + 1.0 / (1.0 - n_odd + k) + - 1.0 / (1.0 + n_odd + k) + ) # calculate summation over coefficients - Aodd = np.einsum("lmk...,kn...->lmn...", Almk[l_odd,m_odd,m_odd], k_odd) - Yodd = np.einsum("lmn...,mn...->lm...", Aodd, f[m_odd,m_odd]) - Ylms.clm[l_odd,m_odd] = Yodd.real - Ylms.slm[l_odd,m_odd] = Yodd.imag + Aodd = np.einsum('lmk...,kn...->lmn...', Almk[l_odd, m_odd, m_odd], k_odd) + Yodd = np.einsum('lmn...,mn...->lm...', Aodd, f[m_odd, m_odd]) + Ylms.clm[l_odd, m_odd] = Yodd.real + Ylms.slm[l_odd, m_odd] = Yodd.imag # Divide by Plm normalization - Ylms.clm[:,0] /= 2.0 - Ylms.slm[:,0] /= 2.0 - Ylms.clm[:,1:MMAX+1] /= 4.0 - Ylms.slm[:,1:MMAX+1] /= 4.0 + Ylms.clm[:, 0] /= 2.0 + Ylms.slm[:, 0] /= 2.0 + Ylms.clm[:, 1 : MMAX + 1] /= 4.0 + Ylms.slm[:, 1 : MMAX + 1] /= 4.0 # return the output spherical harmonics object return Ylms diff --git a/gravity_toolkit/gen_point_load.py b/gravity_toolkit/gen_point_load.py index 563e2807..f3be4cd7 100644 --- a/gravity_toolkit/gen_point_load.py +++ b/gravity_toolkit/gen_point_load.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gen_point_load.py Written by Tyler Sutterley (07/2026) Calculates gravitational spherical harmonic coefficients for point masses @@ -59,11 +59,13 @@ Updated 07/2020: added function docstrings Written 05/2020 """ + import numpy as np import gravity_toolkit.units import gravity_toolkit.harmonics from gravity_toolkit.legendre import legendre + def gen_point_load(data, lon, lat, LMAX=60, MMAX=None, UNITS=1, LOVE=None): """ Calculates spherical harmonic coefficients for point masses @@ -119,33 +121,34 @@ def gen_point_load(data, lon, lat, LMAX=60, MMAX=None, UNITS=1, LOVE=None): # custom units dfactor = np.copy(UNITS) int_fact[:] = 1.0 - elif (UNITS == 1): + elif UNITS == 1: # Default Parameter: Input in grams (g) - dfactor = factors.spatial(*LOVE).cmwe/(factors.rad_e**2) + dfactor = factors.spatial(*LOVE).cmwe / (factors.rad_e**2) int_fact[:] = 1.0 - elif (UNITS == 2): + elif UNITS == 2: # Input in gigatonnes (Gt) - dfactor = factors.spatial(*LOVE).cmwe/(factors.rad_e**2) + dfactor = factors.spatial(*LOVE).cmwe / (factors.rad_e**2) int_fact[:] = 1e15 else: raise ValueError(f'Unknown units {UNITS}') # flattened form of data converted to units - D = int_fact*data.flatten() + D = int_fact * data.flatten() # Initializing output spherical harmonic matrices Ylms = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX) - Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) + Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) # for each degree l - for l in range(LMAX+1): - m1 = np.min([l,MMAX]) + 1 + for l in range(LMAX + 1): + m1 = np.min([l, MMAX]) + 1 SPH = _complex_harmonics(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] + Ylms.clm[l, :m1] = SPH.real[:m1] + Ylms.slm[l, :m1] = SPH.imag[:m1] # return the output spherical harmonics object return Ylms + # calculate spherical harmonics of degree l evaluated at (theta,phi) def _complex_harmonics(l, data, phi, theta, coeff): """ @@ -173,12 +176,12 @@ def _complex_harmonics(l, data, phi, theta, coeff): # calculate normalized legendre polynomials (order, points) Pl = legendre(l, np.cos(theta), NORMALIZE=True) # spherical harmonic orders up to degree l - m = np.arange(0, l+1) + m = np.arange(0, l + 1) # calculate Euler's of order m multiplied by azimuth phi - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", m, phi)) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', m, phi)) # reshape data to (order, points) - D = np.kron(np.ones((l+1, 1)), data[np.newaxis, :]) + D = np.kron(np.ones((l + 1, 1)), data[np.newaxis, :]) # calculate spherical harmonics summing over all points - Yl = np.einsum("mp...,mp...,mp...->m...", D, Pl, m_phi) + Yl = np.einsum('mp...,mp...,mp...->m...', D, Pl, m_phi) # return harmonics for degree l multiplied by coefficients - return coeff*Yl + return coeff * Yl diff --git a/gravity_toolkit/gen_spherical_cap.py b/gravity_toolkit/gen_spherical_cap.py index cc95fd78..7cf6c818 100755 --- a/gravity_toolkit/gen_spherical_cap.py +++ b/gravity_toolkit/gen_spherical_cap.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gen_spherical_cap.py Written by Tyler Sutterley (07/2026) Calculates gravitational spherical harmonic coefficients for a spherical cap @@ -94,14 +94,27 @@ Updated 06/2012: major revision to code organizzation Written 04/2012 """ + import numpy as np import gravity_toolkit.units import gravity_toolkit.harmonics from gravity_toolkit.associated_legendre import plm_holmes from gravity_toolkit.legendre_polynomials import legendre_polynomials -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): + +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, +): r""" Calculates spherical harmonic coefficients for a spherical cap :cite:p:`Holmes:2002ff,Longman:1962ev,Farrell:1972cm,Pollack:1973gi,Jacob:2012eo` @@ -149,8 +162,8 @@ def gen_spherical_cap(data, lon, lat, LMAX=60, MMAX=None, MMAX = np.copy(LMAX) # convert lon and lat to radians - phi = np.radians(lon)# Longitude in radians - th = np.radians(90.0 - lat)# Colatitude in radians + phi = np.radians(lon) # Longitude in radians + th = np.radians(90.0 - lat) # Colatitude in radians # Earth Parameters factors = gravity_toolkit.units(lmax=LMAX) @@ -159,20 +172,20 @@ def gen_spherical_cap(data, lon, lat, LMAX=60, MMAX=None, # 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 RAD_CAP != 0: # if given spherical cap radius in degrees # converting to radians alpha = np.radians(RAD_CAP) - elif (AREA != 0): + elif AREA != 0: # if given spherical cap area in cm^2 # radius in centimeters - radius_cm = np.sqrt(AREA/np.pi) + radius_cm = np.sqrt(AREA / np.pi) # Calculating angular radius of spherical cap - alpha = (radius_cm/factors.rad_e) - elif (RAD_KM != 0): + alpha = radius_cm / factors.rad_e + elif RAD_KM != 0: # if given spherical cap radius in kilometers # Calculating angular radius of spherical cap - alpha = (1e5*RAD_KM)/factors.rad_e + alpha = (1e5 * RAD_KM) / factors.rad_e else: raise ValueError('Input RAD_CAP, AREA or RAD_KM of spherical cap') @@ -181,31 +194,37 @@ def gen_spherical_cap(data, lon, lat, LMAX=60, MMAX=None, # custom units unit_conv = 1.0 dfactor = np.copy(UNITS) - elif (UNITS == 1): + elif UNITS == 1: # Input data is in cm water equivalent (cmwe) unit_conv = 1.0 # degree dependent factors to convert from coefficients # of mass into normalized geoid coefficients - dfactor = 4.0*np.pi*factors.spatial(*LOVE).cmwe/(1.0 + 2.0*factors.l) - elif (UNITS == 2): + dfactor = ( + 4.0 * np.pi * factors.spatial(*LOVE).cmwe / (1.0 + 2.0 * factors.l) + ) + elif UNITS == 2: # Input data is in gigatonnes (Gt) # calculate spherical cap area from angular radius - area = np.pi*(alpha*factors.rad_e)**2 + area = np.pi * (alpha * factors.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 + unit_conv = 1.0e15 / area # degree dependent factors to convert from coefficients # of mass into normalized geoid coefficients - dfactor = 4.0*np.pi*factors.spatial(*LOVE).cmwe/(1.0 + 2.0*factors.l) - elif (UNITS == 3): + dfactor = ( + 4.0 * np.pi * factors.spatial(*LOVE).cmwe / (1.0 + 2.0 * factors.l) + ) + 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 # degree dependent factors to convert from coefficients # of mass into normalized geoid coefficients - dfactor = 4.0*np.pi*factors.spatial(*LOVE).cmwe/(1.0 + 2.0*factors.l) + dfactor = ( + 4.0 * np.pi * factors.spatial(*LOVE).cmwe / (1.0 + 2.0 * factors.l) + ) else: raise ValueError(f'Unknown units {UNITS}') @@ -214,19 +233,19 @@ def gen_spherical_cap(data, lon, lat, LMAX=60, MMAX=None, # 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)) + 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 + pl_alpha[0] = (1.0 - np.cos(alpha)) / 2.0 # for all other degrees: calculate the legendre polynomials up to LMAX+1 - pl_matrix,_ = legendre_polynomials(LMAX+1,np.cos(alpha)) - for l in range(1, LMAX+1):# LMAX+1 to include LMAX + pl_matrix, _ = legendre_polynomials(LMAX + 1, np.cos(alpha)) + for l in range(1, LMAX + 1): # LMAX+1 to include LMAX # from Longman (1962) and Jacob et al (2012) # unnormalizing Legendre polynomials # sqrt(2*l - 1) == sqrt(2*(l-1) + 1) # sqrt(2*l + 3) == sqrt(2*(l+1) + 1) - pl_lower = pl_matrix[l-1]/np.sqrt(2.0*l-1.0) - pl_upper = pl_matrix[l+1]/np.sqrt(2.0*l+3.0) - pl_alpha[l] = (pl_lower - pl_upper)/2.0 + 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 @@ -237,27 +256,27 @@ def gen_spherical_cap(data, lon, lat, LMAX=60, MMAX=None, # 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) + m = np.arange(MMAX + 1) # Multiplying by the units conversion factor (unit_conv) to # convert from the input units into cmwe # Multiplying point mass data (converted to cmwe) 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 - d = unit_conv*data*np.exp(1j*m*phi) + d = unit_conv * data * np.exp(1j * m * phi) # Multiplying by plm_alpha (F_l from Jacob 2012) - plm = np.zeros((LMAX+1, MMAX+1)) + plm = np.zeros((LMAX + 1, MMAX + 1)) # Initializing output spherical harmonic matrices Ylms = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX) # rotate spherical cap to be centered at lat/lon - plm = np.einsum("lm...,l...->lm...", PLM[:LMAX+1,:MMAX+1], pl_alpha) + plm = np.einsum('lm...,l...->lm...', PLM[: LMAX + 1, : MMAX + 1], pl_alpha) # multiplying clm by cos(m*phi) and slm by sin(m*phi) # to get a field of spherical harmonics - ylm = np.einsum("lm...,m...->lm...", plm, d) + ylm = np.einsum('lm...,m...->lm...', plm, d) # Multiplying by factors to convert to fully normalized coefficients - Ylms.clm = np.einsum("l...,lm...->lm...", dfactor, ylm.real) - Ylms.slm = np.einsum("l...,lm...->lm...", dfactor, ylm.imag) + Ylms.clm = np.einsum('l...,lm...->lm...', dfactor, ylm.real) + Ylms.slm = np.einsum('l...,lm...->lm...', dfactor, ylm.imag) # return the output spherical harmonics object return Ylms diff --git a/gravity_toolkit/gen_stokes.py b/gravity_toolkit/gen_stokes.py index 271c7e64..54a139f5 100755 --- a/gravity_toolkit/gen_stokes.py +++ b/gravity_toolkit/gen_stokes.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gen_stokes.py Written by Tyler Sutterley (07/2026) @@ -74,13 +74,16 @@ revised structure of mathematics to improve computational efficiency Written 09/2011 """ + import numpy as np import gravity_toolkit.units import gravity_toolkit.harmonics from gravity_toolkit.associated_legendre import plm_holmes -def gen_stokes(data, lon, lat, LMIN=0, LMAX=60, MMAX=None, UNITS=1, - PLM=None, LOVE=None): + +def gen_stokes( + data, lon, lat, LMIN=0, LMAX=60, MMAX=None, UNITS=1, PLM=None, LOVE=None +): r""" Converts data from the spatial domain to spherical harmonic coefficients :cite:p:`Wahr:1998hy` @@ -134,7 +137,7 @@ def gen_stokes(data, lon, lat, LMIN=0, LMAX=60, MMAX=None, UNITS=1, # Longitude in radians phi = np.radians(np.squeeze(lon.copy())) # reformatting longitudes to range 0:360 (if previously -180:180) - phi = np.where(phi < 0, phi + 2.0*np.pi, phi) + phi = np.where(phi < 0, phi + 2.0 * np.pi, phi) # colatitude in radians th = np.radians(90.0 - np.squeeze(lat.copy())) # grid step in radians @@ -154,50 +157,52 @@ def gen_stokes(data, lon, lat, LMIN=0, LMAX=60, MMAX=None, UNITS=1, if isinstance(UNITS, (list, np.ndarray)): # custom units dfactor = np.copy(UNITS) - int_fact[:] = np.sin(th)*dphi*dth - elif (UNITS == 1): + int_fact[:] = np.sin(th) * dphi * dth + elif UNITS == 1: # Default Parameter: Input in cm w.e. (g/cm^2) dfactor = factors.spatial(*LOVE).cmwe - int_fact[:] = np.sin(th)*dphi*dth - elif (UNITS == 2): + int_fact[:] = np.sin(th) * dphi * dth + elif UNITS == 2: # Input in gigatonnes (Gt) dfactor = factors.spatial(*LOVE).cmwe # rad_e: Average Radius of the Earth [cm] - int_fact[:] = 1e15/(factors.rad_e**2) - elif (UNITS == 3): + int_fact[:] = 1e15 / (factors.rad_e**2) + elif UNITS == 3: # Input in kg/m^2 (mm w.e.) dfactor = factors.spatial(*LOVE).mmwe - int_fact[:] = np.sin(th)*dphi*dth + int_fact[:] = np.sin(th) * dphi * dth else: raise ValueError(f'Unknown units {UNITS}') # Calculating cos/sin of phi arrays # output [m,phi] - mm = np.arange(MMAX+1) - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", mm, phi)) + mm = np.arange(MMAX + 1) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', mm, phi)) # Calculating fully-normalized Legendre Polynomials # Output is plm[l,m,th] - plm = np.zeros((LMAX+1, MMAX+1, nlat)) + 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)) # truncate legendre polynomials to degree and order - plm = np.einsum("lmh...,h...->lmh...", PLM[:LMAX+1,:MMAX+1,:], int_fact) + plm = np.einsum( + 'lmh...,h...->lmh...', PLM[: LMAX + 1, : MMAX + 1, :], int_fact + ) # Initializing output spherical harmonic matrices Ylms = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX) # Multiplying gridded data with sin/cos of m#phis # This will sum through all phis in the dot product # output [m,theta] - d = np.einsum("mp...,ph...->mh...", m_phi, data) + d = np.einsum('mp...,ph...->mh...', m_phi, data) # Summing product of plms and data over all latitudes - ylm = np.einsum("lmh...,mh...->lm...", plm, d) + ylm = np.einsum('lmh...,mh...->lm...', plm, d) # Multiplying by factors to convert to fully normalized coefficients - Ylms.clm = np.einsum("l...,lm...->lm...", dfactor, ylm.real) - Ylms.slm = np.einsum("l...,lm...->lm...", dfactor, ylm.imag) + Ylms.clm = np.einsum('l...,lm...->lm...', dfactor, ylm.real) + Ylms.slm = np.einsum('l...,lm...->lm...', dfactor, ylm.imag) # return the output spherical harmonics object - return Ylms \ No newline at end of file + return Ylms diff --git a/gravity_toolkit/geocenter.py b/gravity_toolkit/geocenter.py index 674c532b..a6c932bf 100644 --- a/gravity_toolkit/geocenter.py +++ b/gravity_toolkit/geocenter.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" geocenter.py Written by Tyler Sutterley (07/2026) Data class for reading and processing geocenter data @@ -43,6 +43,7 @@ Updated 02/2014: minor update to if statement Updated 03/2013: converted to python """ + import re import io import copy @@ -58,6 +59,7 @@ # attempt imports netCDF4 = import_dependency('netCDF4') + class geocenter(object): """ Data class for reading and processing geocenter data @@ -83,29 +85,33 @@ class geocenter(object): radius: float, default 6371000790.009159 Average Radius of the Earth [mm] """ + np.seterr(invalid='ignore') + def __init__(self, **kwargs): # WGS84 ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # Mean Earth's Radius in mm having the same volume as WGS84 ellipsoid - kwargs.setdefault('radius', 1000.0*a_axis*(1.0 - flat)**(1.0/3.0)) + kwargs.setdefault( + 'radius', 1000.0 * a_axis * (1.0 - flat) ** (1.0 / 3.0) + ) # cartesian coordinates - kwargs.setdefault('X',None) - kwargs.setdefault('Y',None) - kwargs.setdefault('Z',None) + kwargs.setdefault('X', None) + kwargs.setdefault('Y', None) + kwargs.setdefault('Z', None) # set default class attributes - self.C10=None - self.C11=None - self.S11=None - self.X=copy.copy(kwargs['X']) - self.Y=copy.copy(kwargs['Y']) - self.Z=copy.copy(kwargs['Z']) - self.time=None - self.month=None - self.filename=None + self.C10 = None + self.C11 = None + self.S11 = None + self.X = copy.copy(kwargs['X']) + self.Y = copy.copy(kwargs['Y']) + self.Z = copy.copy(kwargs['Z']) + self.time = None + self.month = None + self.filename = None # Average Radius of the Earth [mm] - self.radius=copy.copy(kwargs['radius']) + self.radius = copy.copy(kwargs['radius']) # iterator self.__index__ = 0 @@ -129,8 +135,11 @@ def case_insensitive_filename(self, filename): # check if file presently exists with input case if not self.filename.exists(): # search for filename without case dependence - f = [f.name for f in self.filename.parent.iterdir() if - re.match(self.filename.name, f.name, re.I)] + f = [ + f.name + for f in self.filename.parent.iterdir() + if re.match(self.filename.name, f.name, re.I) + ] if not f: msg = f'{filename} not found in file system' raise FileNotFoundError(msg) @@ -165,22 +174,26 @@ def from_AOD1B(self, release, year, month, product='glo'): raise FileNotFoundError(msg) # read AOD1b geocenter skipping over commented header text with AOD1B_file.open(mode='r', encoding='utf8') as f: - file_contents=[i for i in f.read().splitlines() if not re.match(r'#',i)] + file_contents = [ + i for i in f.read().splitlines() if not re.match(r'#', i) + ] # extract X,Y,Z from each line in the file n_lines = len(file_contents) temp = geocenter() temp.X = np.zeros((n_lines)) temp.Y = np.zeros((n_lines)) temp.Z = np.zeros((n_lines)) - for i,line in enumerate(file_contents): + for i, line in enumerate(file_contents): line_contents = line.split() # first column: ISO-formatted date and time - cal_date = time.strptime(line_contents[0],r'%Y-%m-%dT%H:%M:%S') + cal_date = time.strptime(line_contents[0], r'%Y-%m-%dT%H:%M:%S') # verify that dates are within year and month - assert (cal_date.tm_year == year) - assert (cal_date.tm_mon == month) + assert cal_date.tm_year == year + assert cal_date.tm_mon == month # second-fourth columns: X, Y and Z geocenter variations - temp.X[i],temp.Y[i],temp.Z[i] = np.array(line_contents[1:],dtype='f') + temp.X[i], temp.Y[i], temp.Z[i] = np.array( + line_contents[1:], dtype='f' + ) # convert X,Y,Z into spherical harmonics temp.from_cartesian() # return the spherical harmonic coefficients @@ -203,7 +216,7 @@ def from_gravis(self, geocenter_file, **kwargs): # set filename self.case_insensitive_filename(geocenter_file) # set default keyword arguments - kwargs.setdefault('header',True) + kwargs.setdefault('header', True) # Combined GRACE/SLR geocenter solution file produced by GFZ GravIS # Column 1: MJD of BEGINNING of solution data span @@ -230,7 +243,7 @@ def from_gravis(self, geocenter_file, **kwargs): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - kwargs['header'] = not bool(re.match(r'PRODUCT:+',line)) + kwargs['header'] = not bool(re.match(r'PRODUCT:+', line)) # add 1 to counter count += 1 @@ -255,10 +268,10 @@ def from_gravis(self, geocenter_file, **kwargs): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) count = len(line_contents) # check for empty lines - if (count > 0): + if count > 0: # reading decimal year for start of span dinput['time'][t] = np.float64(line_contents[1]) # Spherical Harmonic data for line @@ -266,16 +279,17 @@ def from_gravis(self, geocenter_file, **kwargs): dinput['C11'][t] = np.float64(line_contents[5]) dinput['S11'][t] = np.float64(line_contents[8]) # monthly spherical harmonic formal standard deviations - dinput['eC10'][t] = np.float64(line_contents[4])*1e-10 - dinput['eC11'][t] = np.float64(line_contents[7])*1e-10 - dinput['eS11'][t] = np.float64(line_contents[10])*1e-10 + dinput['eC10'][t] = np.float64(line_contents[4]) * 1e-10 + dinput['eC11'][t] = np.float64(line_contents[7]) * 1e-10 + dinput['eS11'][t] = np.float64(line_contents[10]) * 1e-10 # GRACE/GRACE-FO month of geocenter solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] # The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with @@ -334,10 +348,10 @@ def from_SLR(self, geocenter_file, **kwargs): # set filename self.case_insensitive_filename(geocenter_file) # set default keyword arguments - kwargs.setdefault('AOD',False) - kwargs.setdefault('columns',[]) - kwargs.setdefault('header',0) - kwargs.setdefault('release',None) + kwargs.setdefault('AOD', False) + kwargs.setdefault('columns', []) + kwargs.setdefault('header', 0) + kwargs.setdefault('release', None) # copy keyword arguments to variables COLUMNS = copy.copy(kwargs['columns']) HEADER = copy.copy(kwargs['header']) @@ -345,7 +359,9 @@ def from_SLR(self, geocenter_file, **kwargs): # directory setup for AOD1b data starting with input degree 1 file # this will verify that the input paths work base_dir = self.filename.parent.parent - self.directory = base_dir.joinpath('AOD1B', kwargs['release'], 'geocenter') + self.directory = base_dir.joinpath( + 'AOD1B', kwargs['release'], 'geocenter' + ) # check that AOD1B directory exists if not self.directory.exists(): msg = f'{str(self.directory)} not found in file system' @@ -375,10 +391,10 @@ def from_SLR(self, geocenter_file, **kwargs): JD = np.zeros((ndate)) # for each date - for t,file_line in enumerate(file_contents[HEADER:]): + for t, file_line in enumerate(file_contents[HEADER:]): # find numerical instances in line # replacing fortran double precision exponential - line_contents = rx.findall(file_line.replace('D','E')) + line_contents = rx.findall(file_line.replace('D', 'E')) # extract date self.time[t] = np.float64(line_contents[COLUMNS.index('time')]) @@ -407,8 +423,9 @@ def from_SLR(self, geocenter_file, **kwargs): # Calculation of the Julian date from calendar date JD[t] = gravity_toolkit.time.calendar_to_julian(self.time[t]) # convert the julian date into calendar dates - YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian(JD[t], - FORMAT='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + JD[t], FORMAT='tuple' + ) # calculate the GRACE/GRACE-FO month (Apr02 == 004) # https://grace.jpl.nasa.gov/data/grace-months/ self.month[t] = gravity_toolkit.time.calendar_to_grace(YY, month=MM) @@ -459,8 +476,8 @@ def from_UCI(self, geocenter_file, **kwargs): while (HEADER is False) and (count < file_lines): # file line at count line = file_contents[count] - #if End of YAML Header is found: set HEADER flag - HEADER = bool(re.search(r"\# End of YAML header",line)) + # if End of YAML Header is found: set HEADER flag + HEADER = bool(re.search(r'\# End of YAML header', line)) # add 1 to counter count += 1 @@ -476,8 +493,9 @@ def from_UCI(self, geocenter_file, **kwargs): DEG1['JD'] = np.zeros((n_mon)) DEG1['month'] = np.zeros((n_mon), dtype=np.int64) # parse the YAML header (specifying yaml loader) - DEG1.update(yaml.load('\n'.join(file_contents[:count]), - Loader=yaml.BaseLoader)) + DEG1.update( + yaml.load('\n'.join(file_contents[:count]), Loader=yaml.BaseLoader) + ) # compile numerical expression operator regex_pattern = r'[-+]?(?:(?:\d*\.\d+)|(?:\d+\.?))(?:[Ee][+-]?\d+)?' @@ -491,7 +509,7 @@ def from_UCI(self, geocenter_file, **kwargs): # for each output data variable for key in variables: DEG1[key] = np.zeros((n_mon)) - comment_text, = rx.findall(variables[key]['comment']) + (comment_text,) = rx.findall(variables[key]['comment']) columns[key] = int(comment_text) - 1 # for every other line: @@ -508,13 +526,20 @@ def from_UCI(self, geocenter_file, **kwargs): # check if year is a leap year days_per_year = np.sum(gravity_toolkit.time.calendar_days(year)) # calculation of day of the year - day_of_the_year = days_per_year*(DEG1['time'][t] % 1) + day_of_the_year = days_per_year * (DEG1['time'][t] % 1) # calculate Julian day - DEG1['JD'][t] = np.float64(367.0*year - np.floor(7.0*(year)/4.0) - - np.floor(3.0*(np.floor((year - 8.0/7.0)/100.0) + 1.0)/4.0) + - np.floor(275.0/9.0) + day_of_the_year + 1721028.5) + DEG1['JD'][t] = np.float64( + 367.0 * year + - np.floor(7.0 * (year) / 4.0) + - np.floor( + 3.0 * (np.floor((year - 8.0 / 7.0) / 100.0) + 1.0) / 4.0 + ) + + np.floor(275.0 / 9.0) + + day_of_the_year + + 1721028.5 + ) # extract fully-normalized degree one spherical harmonics - for key,val in columns.items(): + for key, val in columns.items(): DEG1[key][t] = np.float64(line_contents[val]) # return the geocenter harmonics @@ -537,7 +562,7 @@ def from_swenson(self, geocenter_file, **kwargs): # set filename self.case_insensitive_filename(geocenter_file) # set default keyword arguments - kwargs.setdefault('header',True) + kwargs.setdefault('header', True) # read degree 1 file and get contents with self.filename.open(mode='r', encoding='utf8') as f: @@ -552,7 +577,7 @@ def from_swenson(self, geocenter_file, **kwargs): # file line at count line = file_contents[count] # find Time within line to set HEADER flag to False when found - kwargs['header'] = not bool(re.search(r"Time",line)) + kwargs['header'] = not bool(re.search(r'Time', line)) # add 1 to counter count += 1 @@ -580,14 +605,14 @@ def from_swenson(self, geocenter_file, **kwargs): line_contents = rx.findall(line) # extracting time - self.time[t]=np.float64(line_contents[0]) + self.time[t] = np.float64(line_contents[0]) # extracting spherical harmonics and convert to cmwe - self.C10[t]=0.1*np.float64(line_contents[1]) - self.C11[t]=0.1*np.float64(line_contents[2]) - self.S11[t]=0.1*np.float64(line_contents[3]) + self.C10[t] = 0.1 * np.float64(line_contents[1]) + self.C11[t] = 0.1 * np.float64(line_contents[2]) + self.S11[t] = 0.1 * np.float64(line_contents[3]) # calculate the GRACE months - if (len(line_contents) == 5): + if len(line_contents) == 5: # months are included as last column self.month[t] = np.int64(line_contents[4]) else: @@ -600,7 +625,8 @@ def from_swenson(self, geocenter_file, **kwargs): # https://grace.jpl.nasa.gov/data/grace-months/ # Notes on special months (e.g. 119, 120) below self.month[t] = gravity_toolkit.time.calendar_to_grace( - cal_date['year'], month=cal_date['month']) + cal_date['year'], month=cal_date['month'] + ) # The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with # Accelerometer shutoffs make the relation between month number @@ -638,8 +664,8 @@ def from_tellus(self, geocenter_file, **kwargs): # set filename self.case_insensitive_filename(geocenter_file) # set default keyword arguments - kwargs.setdefault('header',True) - kwargs.setdefault('JPL',True) + kwargs.setdefault('header', True) + kwargs.setdefault('JPL', True) # read degree 1 file and get contents with self.filename.open(mode='r', encoding='utf8') as f: @@ -650,19 +676,19 @@ def from_tellus(self, geocenter_file, **kwargs): # counts the number of lines in the header count = 0 # Reading over header text - header_flag = r"end\sof\sheader" if kwargs['JPL'] else r"'\(a6," + header_flag = r'end\sof\sheader' if kwargs['JPL'] else r"'\(a6," while kwargs['header']: # file line at count line = file_contents[count] # find header_flag within line to set HEADER flag to False when found - kwargs['header'] = not bool(re.match(header_flag,line)) + kwargs['header'] = not bool(re.match(header_flag, line)) # add 1 to counter count += 1 # number of months within the file - n_mon = (file_lines - count)//2 + n_mon = (file_lines - count) // 2 # GRACE/GRACE-FO months - self.month = np.zeros((n_mon),dtype=np.int64) + self.month = np.zeros((n_mon), dtype=np.int64) # calendar dates in year-decimal self.time = np.zeros((n_mon)) # spherical harmonic data @@ -689,10 +715,10 @@ def from_tellus(self, geocenter_file, **kwargs): # spherical harmonic order m = np.int64(line_contents[2]) # extract spherical harmonic data for order - if (m == 0): + if m == 0: self.C10[t] = np.float64(line_contents[3]) self.eC10[t] = np.float64(line_contents[5]) - elif (m == 1): + elif m == 1: self.C11[t] = np.float64(line_contents[3]) self.S11[t] = np.float64(line_contents[4]) self.eC11[t] = np.float64(line_contents[5]) @@ -703,30 +729,35 @@ def from_tellus(self, geocenter_file, **kwargs): # calendar year and month if kwargs['JPL']: # start and end date of month - start_date = time.strptime(line_contents[7][:8],r'%Y%m%d') - end_date = time.strptime(line_contents[8][:8],r'%Y%m%d') + start_date = time.strptime(line_contents[7][:8], r'%Y%m%d') + end_date = time.strptime(line_contents[8][:8], r'%Y%m%d') # convert date to year decimal - ts = gravity_toolkit.time.convert_calendar_decimal(start_date.tm_year, - start_date.tm_mon, day=start_date.tm_mday) - te = gravity_toolkit.time.convert_calendar_decimal(end_date.tm_year, - end_date.tm_mon, day=end_date.tm_mday) + ts = gravity_toolkit.time.convert_calendar_decimal( + start_date.tm_year, + start_date.tm_mon, + day=start_date.tm_mday, + ) + te = gravity_toolkit.time.convert_calendar_decimal( + end_date.tm_year, end_date.tm_mon, day=end_date.tm_mday + ) # calculate mean time - self.time[t] = np.mean([ts,te]) + self.time[t] = np.mean([ts, te]) # calculate year and month for estimating GRACE/GRACE-FO month year = np.floor(self.time[t]) - month = np.int64(12*(self.time[t] % 1) + 1) + month = np.int64(12 * (self.time[t] % 1) + 1) else: # dates of month - cal_date = time.strptime(line_contents[0][:6],r'%Y%m') + cal_date = time.strptime(line_contents[0][:6], r'%Y%m') # calculate year and month for estimating GRACE/GRACE-FO month year = cal_date.tm_year month = cal_date.tm_mon # convert date to year decimal - self.time[t], = gravity_toolkit.time.convert_calendar_decimal( - cal_date.tm_year, cal_date.tm_mon) + (self.time[t],) = gravity_toolkit.time.convert_calendar_decimal( + cal_date.tm_year, cal_date.tm_mon + ) # estimated GRACE/GRACE-FO month # Accelerometer shutoffs complicate the month number calculation - self.month[t] = gravity_toolkit.time.calendar_to_grace(year,month) + self.month[t] = gravity_toolkit.time.calendar_to_grace(year, month) # will only advance in time after reading the # order 1 coefficients (t+0=t) @@ -757,15 +788,15 @@ def from_netCDF4(self, geocenter_file, group=None, **kwargs): # set filename self.case_insensitive_filename(geocenter_file) # Open the netCDF4 file for reading - if (kwargs['compression'] == 'gzip'): + if kwargs['compression'] == 'gzip': # read gzipped file as in-memory (diskless) netCDF4 dataset with gzip.open(self.filename, mode='r') as f: - fileID = netCDF4.Dataset(uuid.uuid4().hex, - memory=f.read()) - elif (kwargs['compression'] == 'bytes'): + fileID = netCDF4.Dataset(uuid.uuid4().hex, memory=f.read()) + elif kwargs['compression'] == 'bytes': # read as in-memory (diskless) netCDF4 dataset - fileID = netCDF4.Dataset(uuid.uuid4().hex, - memory=self.filename.read()) + fileID = netCDF4.Dataset( + uuid.uuid4().hex, memory=self.filename.read() + ) else: fileID = netCDF4.Dataset(self.filename, mode='r') # check if reading from root group or sub-group @@ -790,9 +821,22 @@ def copy(self, **kwargs): default keys in ``geocenter`` object """ # set default keyword arguments - kwargs.setdefault('fields',['time','month', - 'C10','C11','S11','eC10','eC11','eS11', - 'X','Y','Z']) + kwargs.setdefault( + 'fields', + [ + 'time', + 'month', + 'C10', + 'C11', + 'S11', + 'eC10', + 'eC11', + 'eS11', + 'X', + 'Y', + 'Z', + ], + ) temp = geocenter() # try to assign variables to self for key in kwargs['fields']: @@ -815,9 +859,25 @@ def from_dict(self, temp, **kwargs): default keys in dictionary """ # set default keyword arguments - kwargs.setdefault('fields',['time','month', - 'C10','C11','S11','eC10','eC11','eS11', - 'X','Y','Z','X_sigma','Y_sigma','Z_sigma']) + kwargs.setdefault( + 'fields', + [ + 'time', + 'month', + 'C10', + 'C11', + 'S11', + 'eC10', + 'eC11', + 'eS11', + 'X', + 'Y', + 'Z', + 'X_sigma', + 'Y_sigma', + 'Z_sigma', + ], + ) # assign dictionary variables to self for key in kwargs['fields']: try: @@ -840,7 +900,7 @@ def from_harmonics(self, temp, **kwargs): # assign degree and order fields temp.update_dimensions() # set default keyword arguments - kwargs.setdefault('fields',['time','month','filename']) + kwargs.setdefault('fields', ['time', 'month', 'filename']) # try to assign variables to self for key in kwargs['fields']: try: @@ -849,14 +909,14 @@ def from_harmonics(self, temp, **kwargs): except AttributeError: pass # get spherical harmonic objects - if (temp.ndim == 2): - self.C10 = np.copy(temp.clm[1,0]) - self.C11 = np.copy(temp.clm[1,1]) - self.S11 = np.copy(temp.slm[1,1]) - elif (temp.ndim == 3): - self.C10 = np.copy(temp.clm[1,0,:]) - self.C11 = np.copy(temp.clm[1,1,:]) - self.S11 = np.copy(temp.slm[1,1,:]) + if temp.ndim == 2: + self.C10 = np.copy(temp.clm[1, 0]) + self.C11 = np.copy(temp.clm[1, 1]) + self.S11 = np.copy(temp.slm[1, 1]) + elif temp.ndim == 3: + self.C10 = np.copy(temp.clm[1, 0, :]) + self.C11 = np.copy(temp.clm[1, 1, :]) + self.S11 = np.copy(temp.slm[1, 1, :]) # return the geocenter object return self @@ -875,9 +935,9 @@ def from_matrix(self, clm, slm): clm = np.atleast_3d(clm) slm = np.atleast_3d(slm) # output geocenter object - self.C10 = np.copy(clm[1,0,:]) - self.C11 = np.copy(clm[1,1,:]) - self.S11 = np.copy(slm[1,1,:]) + self.C10 = np.copy(clm[1, 0, :]) + self.C11 = np.copy(clm[1, 1, :]) + self.S11 = np.copy(slm[1, 1, :]) return self def to_dict(self, **kwargs): @@ -892,9 +952,25 @@ def to_dict(self, **kwargs): # output dictionary temp = {} # set default keyword arguments - kwargs.setdefault('fields',['time','month', - 'C10','C11','S11','eC10','eC11','eS11', - 'X','Y','Z','X_sigma','Y_sigma','Z_sigma']) + kwargs.setdefault( + 'fields', + [ + 'time', + 'month', + 'C10', + 'C11', + 'S11', + 'eC10', + 'eC11', + 'eS11', + 'X', + 'Y', + 'Z', + 'X_sigma', + 'Y_sigma', + 'Z_sigma', + ], + ) # assign dictionary variables to self for key in kwargs['fields']: try: @@ -911,14 +987,14 @@ def to_matrix(self): Converts a ``geocenter`` object to spherical harmonic matrices """ # verify dimensions - _,nt = np.shape(np.atleast_2d(self.C10)) + _, nt = np.shape(np.atleast_2d(self.C10)) # output spherical harmonics - clm = np.zeros((2,2,nt)) - slm = np.zeros((2,2,nt)) + clm = np.zeros((2, 2, nt)) + slm = np.zeros((2, 2, nt)) # copy geocenter harmonics to matrices - clm[1,0,:] = np.atleast_2d(self.C10) - clm[1,1,:] = np.atleast_2d(self.C11) - slm[1,1,:] = np.atleast_2d(self.S11) + clm[1, 0, :] = np.atleast_2d(self.C10) + clm[1, 1, :] = np.atleast_2d(self.C11) + slm[1, 1, :] = np.atleast_2d(self.S11) return dict(clm=clm, slm=slm) def to_cartesian(self, kl=0.0): @@ -932,16 +1008,16 @@ def to_cartesian(self, kl=0.0): """ # Stokes Coefficients to cartesian geocenter try: - self.Z = self.C10*self.radius*np.sqrt(3.0)/(1.0 + kl) - self.X = self.C11*self.radius*np.sqrt(3.0)/(1.0 + kl) - self.Y = self.S11*self.radius*np.sqrt(3.0)/(1.0 + kl) + self.Z = self.C10 * self.radius * np.sqrt(3.0) / (1.0 + kl) + self.X = self.C11 * self.radius * np.sqrt(3.0) / (1.0 + kl) + self.Y = self.S11 * self.radius * np.sqrt(3.0) / (1.0 + kl) except Exception as exc: pass # convert errors to cartesian geocenter try: - self.Z_sigma = self.eC10*self.radius*np.sqrt(3.0)/(1.0 + kl) - self.X_sigma = self.eC11*self.radius*np.sqrt(3.0)/(1.0 + kl) - self.Y_sigma = self.eS11*self.radius*np.sqrt(3.0)/(1.0 + kl) + self.Z_sigma = self.eC10 * self.radius * np.sqrt(3.0) / (1.0 + kl) + self.X_sigma = self.eC11 * self.radius * np.sqrt(3.0) / (1.0 + kl) + self.Y_sigma = self.eS11 * self.radius * np.sqrt(3.0) / (1.0 + kl) except Exception as exc: pass return self @@ -960,14 +1036,14 @@ def to_cmwe(self, kl=0.0): # Average Radius of the Earth [cm] rad_e = 6.371e8 # convert to centimeters water equivalent - self.C10 *= (rho_e*rad_e)/(1.0 + kl) - self.C11 *= (rho_e*rad_e)/(1.0 + kl) - self.S11 *= (rho_e*rad_e)/(1.0 + kl) + self.C10 *= (rho_e * rad_e) / (1.0 + kl) + self.C11 *= (rho_e * rad_e) / (1.0 + kl) + self.S11 *= (rho_e * rad_e) / (1.0 + kl) # convert errors to centimeters water equivalent try: - self.eC10 *= (rho_e*rad_e)/(1.0 + kl) - self.eC11 *= (rho_e*rad_e)/(1.0 + kl) - self.eS11 *= (rho_e*rad_e)/(1.0 + kl) + self.eC10 *= (rho_e * rad_e) / (1.0 + kl) + self.eC11 *= (rho_e * rad_e) / (1.0 + kl) + self.eS11 *= (rho_e * rad_e) / (1.0 + kl) except Exception as exc: pass return self @@ -1005,14 +1081,14 @@ def from_cartesian(self, kl=0.0): gravitational load love number of degree 1 """ # cartesian geocenter to Stokes Coefficients - self.C10 = (1.0 + kl)*self.Z/(self.radius*np.sqrt(3.0)) - self.C11 = (1.0 + kl)*self.X/(self.radius*np.sqrt(3.0)) - self.S11 = (1.0 + kl)*self.Y/(self.radius*np.sqrt(3.0)) + self.C10 = (1.0 + kl) * self.Z / (self.radius * np.sqrt(3.0)) + self.C11 = (1.0 + kl) * self.X / (self.radius * np.sqrt(3.0)) + self.S11 = (1.0 + kl) * self.Y / (self.radius * np.sqrt(3.0)) # convert cartesian geocenter to stokes coefficients try: - self.eC10 = (1.0 + kl)*self.Z_sigma/(self.radius*np.sqrt(3.0)) - self.eC11 = (1.0 + kl)*self.X_sigma/(self.radius*np.sqrt(3.0)) - self.eS11 = (1.0 + kl)*self.Y_sigma/(self.radius*np.sqrt(3.0)) + self.eC10 = (1.0 + kl) * self.Z_sigma / (self.radius * np.sqrt(3.0)) + self.eC11 = (1.0 + kl) * self.X_sigma / (self.radius * np.sqrt(3.0)) + self.eS11 = (1.0 + kl) * self.Y_sigma / (self.radius * np.sqrt(3.0)) except Exception as exc: pass return self @@ -1031,14 +1107,14 @@ def from_cmwe(self, kl=0.0): # Average Radius of the Earth [cm] rad_e = 6.371e8 # convert from centimeters water equivalent - self.C10 *= (1.0 + kl)/(rho_e*rad_e) - self.C11 *= (1.0 + kl)/(rho_e*rad_e) - self.S11 *= (1.0 + kl)/(rho_e*rad_e) + self.C10 *= (1.0 + kl) / (rho_e * rad_e) + self.C11 *= (1.0 + kl) / (rho_e * rad_e) + self.S11 *= (1.0 + kl) / (rho_e * rad_e) # convert errors from centimeters water equivalent try: - self.eC10 *= (1.0 + kl)/(rho_e*rad_e) - self.eC11 *= (1.0 + kl)/(rho_e*rad_e) - self.eS11 *= (1.0 + kl)/(rho_e*rad_e) + self.eC10 *= (1.0 + kl) / (rho_e * rad_e) + self.eC11 *= (1.0 + kl) / (rho_e * rad_e) + self.eS11 *= (1.0 + kl) / (rho_e * rad_e) except Exception as exc: pass return self @@ -1089,7 +1165,7 @@ def mean(self, apply=False, indices=Ellipsis): self.C11 -= temp.C11 self.S11 -= temp.S11 # calculate mean of temporal variables - for key in ['time','month']: + for key in ['time', 'month']: try: val = getattr(self, key) setattr(temp, key, np.mean(val[indices])) @@ -1167,9 +1243,9 @@ def scale(self, var): temp.time = np.copy(self.time) temp.month = np.copy(self.month) # multiply by a single constant or a time-variable scalar - temp.C10 = var*self.C10 - temp.C11 = var*self.C11 - temp.S11 = var*self.S11 + temp.C10 = var * self.C10 + temp.C11 = var * self.C11 + temp.S11 = var * self.S11 return temp def power(self, power): @@ -1184,9 +1260,9 @@ def power(self, power): temp = geocenter() temp.time = np.copy(self.time) temp.month = np.copy(self.month) - temp.C10 = np.power(self.C10,power) - temp.C11 = np.power(self.C11,power) - temp.S11 = np.power(self.S11,power) + temp.C10 = np.power(self.C10, power) + temp.C11 = np.power(self.C11, power) + temp.S11 = np.power(self.S11, power) return temp def get(self, field: str): @@ -1211,12 +1287,11 @@ def fields(self): return ['C10', 'C11', 'S11'] def __str__(self): - """String representation of the ``geocenter`` object - """ + """String representation of the ``geocenter`` object""" properties = ['gravity_toolkit.geocenter'] if self.month: - properties.append(f" start_month: {min(self.month)}") - properties.append(f" end_month: {max(self.month)}") + properties.append(f' start_month: {min(self.month)}') + properties.append(f' end_month: {max(self.month)}') return '\n'.join(properties) def __add__(self, other): @@ -1265,12 +1340,12 @@ def __mul__(self, other): return temp.scale(other) else: return temp.multiply(other) - + def __pow__(self, other): """Raise values from a ``geocenter`` object to a power""" temp = self.copy() return temp.power(other) - + def __sub__(self, other): """Subtract values from a ``geocenter`` object""" temp = self.copy() @@ -1285,19 +1360,16 @@ def __truediv__(self, other): return temp.divide(other) def __len__(self): - """Number of months - """ + """Number of months""" return len(self.month) if np.any(self.month) else 0 def __iter__(self): - """Iterate over GRACE/GRACE-FO months - """ + """Iterate over GRACE/GRACE-FO months""" self.__index__ = 0 return self def __next__(self): - """Get the next month of data - """ + """Get the next month of data""" temp = geocenter() try: temp.time = self.time[self.__index__].copy() diff --git a/gravity_toolkit/grace_date.py b/gravity_toolkit/grace_date.py index 40cade0b..991dea50 100644 --- a/gravity_toolkit/grace_date.py +++ b/gravity_toolkit/grace_date.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_date.py Written by Tyler Sutterley (05/2023) Contributions by Hugo Lecomte and Yara Mohajerani @@ -102,6 +102,7 @@ the dataset from an external main level program Updated 04/2012: changes for RL05 data """ + from __future__ import print_function import logging @@ -110,6 +111,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: parses GRACE/GRACE-FO data files and assigns month numbers def grace_date(base_dir, PROC='', DREL='', DSET='', OUTPUT=True, MODE=0o775): """ @@ -173,34 +175,48 @@ def grace_date(base_dir, PROC='', DREL='', DSET='', OUTPUT=True, MODE=0o775): n_files = len(input_files) # define date variables - start_yr = np.zeros((n_files))# year start date - end_yr = np.zeros((n_files))# year end date - start_day = np.zeros((n_files))# day number start date - end_day = np.zeros((n_files))# day number end date - mid_day = np.zeros((n_files))# mid-month day - tot_days = np.zeros((n_files))# number of days since Jan 2002 - tdec = np.zeros((n_files))# date in decimal form - mon = np.zeros((n_files,), dtype=np.int64)# GRACE/GRACE-FO month number + start_yr = np.zeros((n_files)) # year start date + end_yr = np.zeros((n_files)) # year end date + start_day = np.zeros((n_files)) # day number start date + end_day = np.zeros((n_files)) # day number end date + mid_day = np.zeros((n_files)) # mid-month day + tot_days = np.zeros((n_files)) # number of days since Jan 2002 + tdec = np.zeros((n_files)) # date in decimal form + mon = np.zeros((n_files,), dtype=np.int64) # GRACE/GRACE-FO month number # for each data file - for t,infile in enumerate(input_files): - if PROC in ('GRAZ','Swarm',): + for t, infile in enumerate(input_files): + if PROC in ( + 'GRAZ', + 'Swarm', + ): # get date lists for the start and end of fields - start_date,end_date = gravtk.time.parse_gfc_file( - infile, PROC, DSET) + start_date, end_date = gravtk.time.parse_gfc_file( + infile, PROC, DSET + ) # start and end year start_yr[t] = np.float64(start_date[0]) end_yr[t] = np.float64(end_date[0]) # number of days in each month for the calendar year dpm = gravtk.time.calendar_days(start_yr[t]) # start and end day of the year - start_day[t] = np.sum(dpm[:start_date[1]-1]) + start_date[2] + \ - start_date[3]/24. + start_date[4]/1440. + start_date[5]/86400. - end_day[t] = np.sum(dpm[:end_date[1]-1]) + end_date[2] + \ - end_date[3]/24. + end_date[4]/1440. + end_date[5]/86400. + start_day[t] = ( + np.sum(dpm[: start_date[1] - 1]) + + start_date[2] + + start_date[3] / 24.0 + + start_date[4] / 1440.0 + + start_date[5] / 86400.0 + ) + end_day[t] = ( + np.sum(dpm[: end_date[1] - 1]) + + end_date[2] + + end_date[3] / 24.0 + + end_date[4] / 1440.0 + + end_date[5] / 86400.0 + ) else: # get date lists for the start and end of fields - start_date,end_date = gravtk.time.parse_grace_file(infile) + start_date, end_date = gravtk.time.parse_grace_file(infile) # start and end year start_yr[t] = np.float64(start_date[0]) end_yr[t] = np.float64(end_date[0]) @@ -211,22 +227,23 @@ def grace_date(base_dir, PROC='', DREL='', DSET='', OUTPUT=True, MODE=0o775): # number of days in the starting year for leap and standard years dpy = gravtk.time.calendar_days(start_yr[t]).sum() # end date taking into account measurements taken on different years - end_cyclic = (end_yr[t]-start_yr[t])*dpy + end_day[t] + end_cyclic = (end_yr[t] - start_yr[t]) * dpy + end_day[t] # calculate mid-month value mid_day[t] = np.mean([start_day[t], end_cyclic]) # calculate Modified Julian Day from start_yr and mid_day - MJD = gravtk.time.convert_calendar_dates(start_yr[t], - 1.0,mid_day[t],epoch=(1858,11,17,0,0,0)) + MJD = gravtk.time.convert_calendar_dates( + start_yr[t], 1.0, mid_day[t], epoch=(1858, 11, 17, 0, 0, 0) + ) # convert from Modified Julian Days to calendar dates - cal_date = gravtk.time.convert_julian(MJD+2400000.5) + cal_date = gravtk.time.convert_julian(MJD + 2400000.5) # Calculating the mid-month date in decimal form - tdec[t] = start_yr[t] + mid_day[t]/dpy + tdec[t] = start_yr[t] + mid_day[t] / dpy # Calculation of total days since start of campaign count = 0 - n_yrs = np.int64(start_yr[t]-2002) + n_yrs = np.int64(start_yr[t] - 2002) # for each of the GRACE years up to the file year for iyr in range(n_yrs): # year @@ -236,17 +253,18 @@ def grace_date(base_dir, PROC='', DREL='', DSET='', OUTPUT=True, MODE=0o775): count += gravtk.time.calendar_days(year).sum() # calculating the total number of days since 2002 - tot_days[t] = np.mean([count+start_day[t], count+end_cyclic]) + tot_days[t] = np.mean([count + start_day[t], count + end_cyclic]) # Calculates the month number (or 10-day number for CNES RL01,RL02) - if ((PROC == 'CNES') and (DREL in ('RL01','RL02'))): - mon[t] = np.round(1.0+(tot_days[t]-tot_days[0])/10.0) + if (PROC == 'CNES') and (DREL in ('RL01', 'RL02')): + mon[t] = np.round(1.0 + (tot_days[t] - tot_days[0]) / 10.0) else: # calculate the GRACE/GRACE-FO month (Apr02 == 004) # https://grace.jpl.nasa.gov/data/grace-months/ # Notes on special months (e.g. 119, 120) below mon[t] = gravtk.time.calendar_to_grace( - cal_date['year'],cal_date['month']) + cal_date['year'], cal_date['month'] + ) # The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with # Accelerometer shutoffs make the relation between month number @@ -261,8 +279,8 @@ def grace_date(base_dir, PROC='', DREL='', DSET='', OUTPUT=True, MODE=0o775): grace_date_file = grace_dir.joinpath(f'{PROC}_{DREL}_DATES.txt') fid = grace_date_file.open(mode='w', encoding='utf8') # date file header information - args = ('Mid-date','Month','Start_Day','End_Day','Total_Days') - print('{0} {1:>10} {2:>11} {3:>10} {4:>13}'.format(*args),file=fid) + args = ('Mid-date', 'Month', 'Start_Day', 'End_Day', 'Total_Days') + print('{0} {1:>10} {2:>11} {3:>10} {4:>13}'.format(*args), file=fid) # create python dictionary mapping input file names with GRACE months grace_files = {} @@ -272,10 +290,15 @@ def grace_date(base_dir, PROC='', DREL='', DSET='', OUTPUT=True, MODE=0o775): grace_files[mon[t]] = grace_dir.joinpath(infile) # print to GRACE dates ascii file (NOTE: tot_days will be rounded) if OUTPUT: - print((f'{tdec[t]:13.8f} {mon[t]:03d} ' - f'{start_yr[t]:8.0f} {start_day[t]:03.0f} ' - f'{end_yr[t]:8.0f} {end_day[t]:03.0f} ' - f'{tot_days[t]:8.0f}'), file=fid) + print( + ( + f'{tdec[t]:13.8f} {mon[t]:03d} ' + f'{start_yr[t]:8.0f} {start_day[t]:03.0f} ' + f'{end_yr[t]:8.0f} {end_day[t]:03.0f} ' + f'{tot_days[t]:8.0f}' + ), + file=fid, + ) # close date file # set permissions level of output date file @@ -286,6 +309,7 @@ def grace_date(base_dir, PROC='', DREL='', DSET='', OUTPUT=True, MODE=0o775): # return the python dictionary that maps GRACE months with GRACE files return grace_files + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -295,49 +319,83 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Data processing center or satellite mission - parser.add_argument('--center','-c', - metavar='PROC', type=str, nargs='+', - default=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + nargs='+', + default=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, nargs='+', + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + nargs='+', default=['RL06'], - help='GRACE/GRACE-FO Data Release') + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO data product - parser.add_argument('--product','-p', - metavar='DSET', type=str.upper, nargs='+', - default=['GAC','GAD','GSM'], - choices=['GAA','GAB','GAC','GAD','GSM'], - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str.upper, + nargs='+', + default=['GAC', 'GAD', 'GSM'], + choices=['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + help='GRACE/GRACE-FO Level-2 data product', + ) # output GRACE/GRACE-FO ascii date file - parser.add_argument('--output','-O', - default=False, action='store_true', - help='Overwrite existing data') + parser.add_argument( + '--output', + '-O', + default=False, + action='store_true', + help='Overwrite existing data', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run GRACE/GRACE-FO date program for pr in args.center: for rl in args.release: for ds in args.product: - grace_date(args.directory, PROC=pr, DREL=rl, DSET=ds, - OUTPUT=args.output, MODE=args.mode) + grace_date( + args.directory, + PROC=pr, + DREL=rl, + DSET=ds, + OUTPUT=args.output, + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/gravity_toolkit/grace_find_months.py b/gravity_toolkit/grace_find_months.py index 2105fb09..0fb5e5b0 100644 --- a/gravity_toolkit/grace_find_months.py +++ b/gravity_toolkit/grace_find_months.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_find_months.py Written by Tyler Sutterley (05/2023) @@ -51,10 +51,12 @@ Updated 09/2013: missing periods for for CNES Written 05/2013 """ + import pathlib import numpy as np from gravity_toolkit.grace_date import grace_date + def grace_find_months(base_dir, PROC, DREL, DSET='GSM'): """ Parses date index file @@ -113,9 +115,9 @@ def grace_find_months(base_dir, PROC, DREL, DSET='GSM'): grace_date(base_dir, PROC=PROC, DREL=DREL, DSET=DSET, OUTPUT=True) # names and formats of GRACE/GRACE-FO date ascii file - names = ('t','mon','styr','stday','endyr','endday','total') - formats = ('f','i','i','i','i','i','i') - dtype = np.dtype({'names':names, 'formats':formats}) + names = ('t', 'mon', 'styr', 'stday', 'endyr', 'endday', 'total') + formats = ('f', 'i', 'i', 'i', 'i', 'i', 'i') + dtype = np.dtype({'names': names, 'formats': formats}) # read GRACE/GRACE-FO date ascii file # skip the header row and extract dates (decimal format) and months date_input = np.loadtxt(grace_date_file, skiprows=1, dtype=dtype) @@ -132,7 +134,7 @@ def grace_find_months(base_dir, PROC, DREL, DSET='GSM'): var_info['missing'] = sorted(set(all_months) - set(date_input['mon'])) # If CNES RL01/2: simply convert into numpy array # else: remove months 1-3 and convert into numpy array - if ((PROC == 'CNES') & (DREL in ('RL01','RL02'))): + if (PROC == 'CNES') & (DREL in ('RL01', 'RL02')): var_info['missing'] = np.array(var_info['missing'], dtype=np.int64) else: var_info['missing'] = np.array(var_info['missing'][3:], dtype=np.int64) diff --git a/gravity_toolkit/grace_input_months.py b/gravity_toolkit/grace_input_months.py index 3bc40197..0bd49f54 100644 --- a/gravity_toolkit/grace_input_months.py +++ b/gravity_toolkit/grace_input_months.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_input_months.py Written by Tyler Sutterley (10/2023) Contributions by Hugo Lecomte and Yara Mohajerani @@ -169,6 +169,7 @@ Updated 07/2013: can use different geocenter solutions Written 05/2013 """ + from __future__ import print_function, division import re @@ -184,8 +185,20 @@ from gravity_toolkit.read_GRACE_harmonics import read_GRACE_harmonics from gravity_toolkit.read_gfc_harmonics import read_gfc_harmonics -def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, - missing, SLR_C20, DEG1, **kwargs): + +def grace_input_months( + base_dir, + PROC, + DREL, + DSET, + LMAX, + start_mon, + end_mon, + missing, + SLR_C20, + DEG1, + **kwargs, +): """ Reads GRACE/GRACE-FO files for a spherical harmonic degree and order and a date range @@ -248,7 +261,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, - ``'SLR'``: Satellite laser ranging coefficients from CSR :cite:p:`Cheng:2013tz` - ``'UCI'``: GRACE/GRACE-FO coefficients from :cite:p:`Sutterley:2019bx` - ``'Swenson'``: GRACE-derived coefficients from :cite:p:`Swenson:2008cr` - - ``'GFZ'``: GFZ GravIS coefficients + - ``'GFZ'``: GFZ GravIS coefficients MMAX: int or NoneType, default None Upper bound of Spherical Harmonic Orders SLR_21: str or NoneType, default '' @@ -319,16 +332,16 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, Attributes of input files and corrections """ # set default keyword arguments - kwargs.setdefault('MMAX',LMAX) - kwargs.setdefault('SLR_21','') - kwargs.setdefault('SLR_22','') - kwargs.setdefault('SLR_C30','') - kwargs.setdefault('SLR_C40','') - kwargs.setdefault('SLR_C50','') - kwargs.setdefault('DEG1_FILE',None) - kwargs.setdefault('MODEL_DEG1',False) - kwargs.setdefault('ATM',False) - kwargs.setdefault('POLE_TIDE',False) + kwargs.setdefault('MMAX', LMAX) + kwargs.setdefault('SLR_21', '') + kwargs.setdefault('SLR_22', '') + kwargs.setdefault('SLR_C30', '') + kwargs.setdefault('SLR_C40', '') + kwargs.setdefault('SLR_C50', '') + kwargs.setdefault('DEG1_FILE', None) + kwargs.setdefault('MODEL_DEG1', False) + kwargs.setdefault('ATM', False) + kwargs.setdefault('POLE_TIDE', False) # directory of exact GRACE/GRACE-FO product base_dir = pathlib.Path(base_dir).expanduser().absolute() @@ -342,55 +355,53 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, # Range of months from start_mon to end_mon (end_mon+1 to include end_mon) # Removing the missing months and months not to consider - months = sorted(set(np.arange(start_mon, end_mon+1)) - set(missing)) + months = sorted(set(np.arange(start_mon, end_mon + 1)) - set(missing)) # number of months to consider in analysis n_cons = len(months) # Initializing input data matrices grace_Ylms = {} - grace_Ylms['clm'] = np.zeros((LMAX+1, MMAX+1, n_cons)) - grace_Ylms['slm'] = np.zeros((LMAX+1, MMAX+1, n_cons)) - grace_Ylms['eclm'] = np.zeros((LMAX+1, MMAX+1, n_cons)) - grace_Ylms['eslm'] = np.zeros((LMAX+1, MMAX+1, n_cons)) + grace_Ylms['clm'] = np.zeros((LMAX + 1, MMAX + 1, n_cons)) + grace_Ylms['slm'] = np.zeros((LMAX + 1, MMAX + 1, n_cons)) + grace_Ylms['eclm'] = np.zeros((LMAX + 1, MMAX + 1, n_cons)) + grace_Ylms['eslm'] = np.zeros((LMAX + 1, MMAX + 1, n_cons)) grace_Ylms['time'] = np.zeros((n_cons)) grace_Ylms['month'] = np.zeros((n_cons), dtype=np.int64) # output dimensions - grace_Ylms['l'] = np.arange(LMAX+1) - grace_Ylms['m'] = np.arange(MMAX+1) + grace_Ylms['l'] = np.arange(LMAX + 1) + grace_Ylms['m'] = np.arange(MMAX + 1) # attributes for processing run attributes = collections.OrderedDict() # input GRACE/GRACE-FO and correction files - attributes['lineage'] = [None]*n_cons + attributes['lineage'] = [None] * n_cons # associate GRACE/GRACE-FO files with each GRACE/GRACE-FO month - grace_files = grace_date(base_dir, - PROC=PROC, - DREL=DREL, - DSET=DSET, - OUTPUT=False + grace_files = grace_date( + base_dir, PROC=PROC, DREL=DREL, DSET=DSET, OUTPUT=False ) # importing data from GRACE/GRACE-FO files - for i,grace_month in enumerate(months): + for i, grace_month in enumerate(months): # read spherical harmonic data products infile = grace_files[grace_month] # log input file if debugging logging.debug(f'Reading file {i:d}: {str(infile)}') # read GRACE/GRACE-FO/Swarm file - if PROC in ('GRAZ','Swarm'): + if PROC in ('GRAZ', 'Swarm'): # Degree 2 zonals will be converted to a tide free state Ylms = read_gfc_harmonics(infile, TIDE='tide_free') else: # Effects of Pole tide drift will be compensated if specified - Ylms = read_GRACE_harmonics(infile, LMAX, MMAX=MMAX, - POLE_TIDE=kwargs['POLE_TIDE']) + Ylms = read_GRACE_harmonics( + infile, LMAX, MMAX=MMAX, POLE_TIDE=kwargs['POLE_TIDE'] + ) # truncate harmonics to degree and order - grace_Ylms['clm'][:,:,i] = Ylms['clm'][0:LMAX+1, 0:MMAX+1] - grace_Ylms['slm'][:,:,i] = Ylms['slm'][0:LMAX+1, 0:MMAX+1] + grace_Ylms['clm'][:, :, i] = Ylms['clm'][0 : LMAX + 1, 0 : MMAX + 1] + grace_Ylms['slm'][:, :, i] = Ylms['slm'][0 : LMAX + 1, 0 : MMAX + 1] # truncate harmonic errors to degree and order - grace_Ylms['eclm'][:,:,i] = Ylms['eclm'][0:LMAX+1, 0:MMAX+1] - grace_Ylms['eslm'][:,:,i] = Ylms['eslm'][0:LMAX+1, 0:MMAX+1] + grace_Ylms['eclm'][:, :, i] = Ylms['eclm'][0 : LMAX + 1, 0 : MMAX + 1] + grace_Ylms['eslm'][:, :, i] = Ylms['eslm'][0 : LMAX + 1, 0 : MMAX + 1] # copy date variables grace_Ylms['time'][i] = np.copy(Ylms['time']) grace_Ylms['month'][i] = np.int64(grace_month) @@ -404,12 +415,12 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, FLAGS = [] # Replacing C2,0 with SLR values - if (SLR_C20 == 'CSR'): - if (DREL == 'RL04'): + if SLR_C20 == 'CSR': + if DREL == 'RL04': SLR_file = base_dir.joinpath('TN-05_C20_SLR.txt') - elif (DREL == 'RL05'): + elif DREL == 'RL05': SLR_file = base_dir.joinpath('TN-07_C20_SLR.txt') - elif (DREL == 'RL06'): + elif DREL == 'RL06': # SLR_file = base_dir.joinpath('TN-11_C20_SLR.txt') SLR_file = base_dir.joinpath('C20_RL06.txt') # log SLR file if debugging @@ -418,7 +429,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C20_input = gravity_toolkit.SLR.C20(SLR_file) FLAGS.append('_wCSR_C20') attributes['SLR C20'] = ('CSR', SLR_file.name) - elif (SLR_C20 == 'GFZ'): + elif SLR_C20 == 'GFZ': SLR_file = base_dir.joinpath(f'GFZ_{DREL}_C20_SLR.dat') # log SLR file if debugging logging.debug(f'Reading SLR C20 file: {str(SLR_file)}') @@ -426,7 +437,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C20_input = gravity_toolkit.SLR.C20(SLR_file) FLAGS.append('_wGFZ_C20') attributes['SLR C20'] = ('GFZ', SLR_file.name) - elif (SLR_C20 == 'GSFC'): + elif SLR_C20 == 'GSFC': SLR_file = base_dir.joinpath('TN-14_C30_C20_GSFC_SLR.txt') # log SLR file if debugging logging.debug(f'Reading SLR C20 file: {str(SLR_file)}') @@ -436,7 +447,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, attributes['SLR C20'] = ('GSFC', SLR_file.name) # Replacing C2,1/S2,1 with SLR values - if (kwargs['SLR_21'] == 'CSR'): + if kwargs['SLR_21'] == 'CSR': SLR_file = base_dir.joinpath(f'C21_S21_{DREL}.txt') # log SLR file if debugging logging.debug(f'Reading SLR C21/S21 file: {str(SLR_file)}') @@ -444,7 +455,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C21_input = gravity_toolkit.SLR.CS2(SLR_file) FLAGS.append('_wCSR_21') attributes['SLR 21'] = ('CSR', SLR_file.name) - elif (kwargs['SLR_21'] == 'GFZ'): + elif kwargs['SLR_21'] == 'GFZ': GravIS_file = 'GRAVIS-2B_GFZOP_GRACE+SLR_LOW_DEGREES_0003.dat' SLR_file = base_dir.joinpath(GravIS_file) # log SLR file if debugging @@ -453,19 +464,20 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C21_input = gravity_toolkit.SLR.CS2(SLR_file) FLAGS.append('_wGFZ_21') attributes['SLR 21'] = ('GFZ GravIS', SLR_file.name) - elif (kwargs['SLR_21'] == 'GSFC'): + elif kwargs['SLR_21'] == 'GSFC': # calculate monthly averages from 7-day arcs SLR_file = base_dir.joinpath('gsfc_slr_5x5c61s61.txt') # log SLR file if debugging logging.debug(f'Reading SLR C21/S21 file: {str(SLR_file)}') # read SLR file - C21_input = gravity_toolkit.SLR.CS2(SLR_file, - DATE=grace_Ylms['time'], ORDER=1) + C21_input = gravity_toolkit.SLR.CS2( + SLR_file, DATE=grace_Ylms['time'], ORDER=1 + ) FLAGS.append('_wGSFC_21') attributes['SLR 21'] = ('GSFC', SLR_file.name) # Replacing C2,2/S2,2 with SLR values - if (kwargs['SLR_22'] == 'CSR'): + if kwargs['SLR_22'] == 'CSR': SLR_file = base_dir.joinpath(f'C22_S22_{DREL}.txt') # log SLR file if debugging logging.debug(f'Reading SLR C22/S22 file: {str(SLR_file)}') @@ -473,18 +485,19 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C22_input = gravity_toolkit.SLR.CS2(SLR_file) FLAGS.append('_wCSR_22') attributes['SLR 22'] = ('CSR', SLR_file.name) - elif (kwargs['SLR_22'] == 'GSFC'): + elif kwargs['SLR_22'] == 'GSFC': SLR_file = base_dir.joinpath('gsfc_slr_5x5c61s61.txt') # log SLR file if debugging logging.debug(f'Reading SLR C22/S22 file: {str(SLR_file)}') # read SLR file - C22_input = gravity_toolkit.SLR.CS2(SLR_file, - DATE=grace_Ylms['time'], ORDER=2) + C22_input = gravity_toolkit.SLR.CS2( + SLR_file, DATE=grace_Ylms['time'], ORDER=2 + ) FLAGS.append('_wGSFC_22') attributes['SLR 22'] = ('GSFC', SLR_file.name) # Replacing C3,0 with SLR values - if (kwargs['SLR_C30'] == 'CSR'): + if kwargs['SLR_C30'] == 'CSR': SLR_file = base_dir.joinpath('CSR_Monthly_5x5_Gravity_Harmonics.txt') # log SLR file if debugging logging.debug(f'Reading SLR C30 file: {str(SLR_file)}') @@ -492,7 +505,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C30_input = gravity_toolkit.SLR.C30(SLR_file) FLAGS.append('_wCSR_C30') attributes['SLR C30'] = ('CSR', SLR_file.name) - elif (kwargs['SLR_C30'] == 'LARES'): + elif kwargs['SLR_C30'] == 'LARES': SLR_file = base_dir.joinpath('C30_LARES_filtered.txt') # log SLR file if debugging logging.debug(f'Reading SLR C30 file: {str(SLR_file)}') @@ -500,7 +513,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C30_input = gravity_toolkit.SLR.C30(SLR_file) FLAGS.append('_wLARES_C30') attributes['SLR_C30'] = ('CSR LARES', SLR_file.name) - elif (kwargs['SLR_C30'] == 'GFZ'): + elif kwargs['SLR_C30'] == 'GFZ': GravIS_file = 'GRAVIS-2B_GFZOP_GRACE+SLR_LOW_DEGREES_0003.dat' SLR_file = base_dir.joinpath(GravIS_file) # log SLR file if debugging @@ -509,7 +522,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C30_input = gravity_toolkit.SLR.C30(SLR_file) FLAGS.append('_wGFZ_C30') attributes['SLR C30'] = ('GFZ GravIS', SLR_file.name) - elif (kwargs['SLR_C30'] == 'GSFC'): + elif kwargs['SLR_C30'] == 'GSFC': SLR_file = base_dir.joinpath('TN-14_C30_C20_GSFC_SLR.txt') # log SLR file if debugging logging.debug(f'Reading SLR C30 file: {str(SLR_file)}') @@ -519,7 +532,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, attributes['SLR C30'] = ('GSFC', SLR_file.name) # Replacing C4,0 with SLR values - if (kwargs['SLR_C40'] == 'CSR'): + if kwargs['SLR_C40'] == 'CSR': SLR_file = base_dir.joinpath('CSR_Monthly_5x5_Gravity_Harmonics.txt') # log SLR file if debugging logging.debug(f'Reading SLR C40 file: {str(SLR_file)}') @@ -527,7 +540,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C40_input = gravity_toolkit.SLR.C40(SLR_file) FLAGS.append('_wCSR_C40') attributes['SLR C40'] = ('CSR', SLR_file.name) - elif (kwargs['SLR_C40'] == 'LARES'): + elif kwargs['SLR_C40'] == 'LARES': SLR_file = base_dir.joinpath('C40_LARES_filtered.txt') # log SLR file if debugging logging.debug(f'Reading SLR C40 file: {str(SLR_file)}') @@ -535,18 +548,17 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C40_input = gravity_toolkit.SLR.C40(SLR_file) FLAGS.append('_wLARES_C40') attributes['SLR C40'] = ('CSR LARES', SLR_file.name) - elif (kwargs['SLR_C40'] == 'GSFC'): + elif kwargs['SLR_C40'] == 'GSFC': SLR_file = base_dir.joinpath('gsfc_slr_5x5c61s61.txt') # log SLR file if debugging logging.debug(f'Reading SLR C40 file: {str(SLR_file)}') # read SLR file - C40_input = gravity_toolkit.SLR.C40(SLR_file, - DATE=grace_Ylms['time']) + C40_input = gravity_toolkit.SLR.C40(SLR_file, DATE=grace_Ylms['time']) FLAGS.append('_wGSFC_C40') attributes['SLR C40'] = ('GSFC', SLR_file.name) # Replacing C5,0 with SLR values - if (kwargs['SLR_C50'] == 'CSR'): + if kwargs['SLR_C50'] == 'CSR': SLR_file = base_dir.joinpath('CSR_Monthly_5x5_Gravity_Harmonics.txt') # log SLR file if debugging logging.debug(f'Reading SLR C50 file: {str(SLR_file)}') @@ -554,7 +566,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C50_input = gravity_toolkit.SLR.C50(SLR_file) FLAGS.append('_wCSR_C50') attributes['SLR C50'] = ('CSR', SLR_file.name) - elif (kwargs['SLR_C50'] == 'LARES'): + elif kwargs['SLR_C50'] == 'LARES': SLR_file = base_dir.joinpath('C50_LARES_filtered.txt') # log SLR file if debugging logging.debug(f'Reading SLR C50 file: {str(SLR_file)}') @@ -562,39 +574,40 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C50_input = gravity_toolkit.SLR.C50(SLR_file) FLAGS.append('_wLARES_C50') attributes['SLR C50'] = ('CSR LARES', SLR_file.name) - elif (kwargs['SLR_C50'] == 'GSFC'): + elif kwargs['SLR_C50'] == 'GSFC': # SLR_file = base_dir.joinpath('GSFC_SLR_C20_C30_C50_GSM_replacement.txt') SLR_file = base_dir.joinpath('gsfc_slr_5x5c61s61.txt') # log SLR file if debugging logging.debug(f'Reading SLR C50 file: {str(SLR_file)}') # read SLR file - C50_input = gravity_toolkit.SLR.C50(SLR_file, - DATE=grace_Ylms['time']) + C50_input = gravity_toolkit.SLR.C50(SLR_file, DATE=grace_Ylms['time']) FLAGS.append('_wGSFC_C50') attributes['SLR C50'] = ('GSFC', SLR_file.name) # Correcting for Degree 1 (geocenter variations) # reading degree 1 file for given release if specified - if (DEG1 == 'Tellus'): + if DEG1 == 'Tellus': # Tellus (PO.DAAC) degree 1 - if DREL in ('RL04','RL05'): + if DREL in ('RL04', 'RL05'): # old degree one files - default_geocenter = base_dir.joinpath('geocenter', - f'deg1_coef_{DREL}.txt') + default_geocenter = base_dir.joinpath( + 'geocenter', f'deg1_coef_{DREL}.txt' + ) JPL = False else: # new TN-13 degree one files - default_geocenter = base_dir.joinpath('geocenter', - f'TN-13_GEOC_{PROC}_{DREL}.txt') + default_geocenter = base_dir.joinpath( + 'geocenter', f'TN-13_GEOC_{PROC}_{DREL}.txt' + ) JPL = True # read degree one files from JPL GRACE Tellus DEG1_file = kwargs.get('DEG1_FILE') or default_geocenter # log geocenter file if debugging logging.debug(f'Reading Geocenter file: {DEG1_file}') - DEG1_input = gravity_toolkit.geocenter().from_tellus(DEG1_file,JPL=JPL) + DEG1_input = gravity_toolkit.geocenter().from_tellus(DEG1_file, JPL=JPL) FLAGS.append(f'_w{DEG1}_DEG1') attributes['geocenter'] = ('JPL Tellus', DEG1_file.name) - elif (DEG1 == 'SLR'): + elif DEG1 == 'SLR': # CSR Satellite Laser Ranging (SLR) degree 1 # # SLR-derived degree-1 mass variations # # ftp://ftp.csr.utexas.edu/pub/slr/geocenter/ @@ -613,22 +626,38 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, # new file of degree-1 mass variations from Minkang Cheng # http://download.csr.utexas.edu/outgoing/cheng/gct2est.220_5s - DEG1_file = base_dir.joinpath('geocenter','gct2est.220_5s') - COLUMNS = ['MJD','time','X','Y','Z','XM','YM','ZM', - 'X_sigma','Y_sigma','Z_sigma','XM_sigma','YM_sigma','ZM_sigma'] + DEG1_file = base_dir.joinpath('geocenter', 'gct2est.220_5s') + COLUMNS = [ + 'MJD', + 'time', + 'X', + 'Y', + 'Z', + 'XM', + 'YM', + 'ZM', + 'X_sigma', + 'Y_sigma', + 'Z_sigma', + 'XM_sigma', + 'YM_sigma', + 'ZM_sigma', + ] # log geocenter file if debugging logging.debug(f'Reading Geocenter file: {DEG1_file}') # read degree one files from CSR satellite laser ranging DEG1_input = gravity_toolkit.geocenter(radius=6.378136e9).from_SLR( - DEG1_file, AOD=True, release=DREL, header=15, columns=COLUMNS) + DEG1_file, AOD=True, release=DREL, header=15, columns=COLUMNS + ) FLAGS.append(f'_w{DEG1}_DEG1') attributes['geocenter'] = ('CSR SLR', DEG1_file.name) - elif DEG1 in ('SLF','UCI'): + elif DEG1 in ('SLF', 'UCI'): # degree one files from Sutterley and Velicogna (2019) # default: iterated and with self-attraction and loading effects args = (PROC, DREL, 'MPIOM', 'SLF_iter') - default_geocenter = base_dir.joinpath('geocenter', - '{0}_{1}_{2}_{3}.txt'.format(*args)) + default_geocenter = base_dir.joinpath( + 'geocenter', '{0}_{1}_{2}_{3}.txt'.format(*args) + ) # read degree one files from Sutterley and Velicogna (2019) DEG1_file = kwargs.get('DEG1_FILE') or default_geocenter # log geocenter file if debugging @@ -636,10 +665,11 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, DEG1_input = gravity_toolkit.geocenter().from_UCI(DEG1_file) FLAGS.append(f'_w{DEG1}_DEG1') attributes['geocenter'] = ('UCI', DEG1_file.name) - elif (DEG1 == 'Swenson'): + elif DEG1 == 'Swenson': # degree 1 coefficients provided by Sean Swenson in mm w.e. - default_geocenter = base_dir.joinpath('geocenter', - f'gad_gsm.{DREL}.txt') + default_geocenter = base_dir.joinpath( + 'geocenter', f'gad_gsm.{DREL}.txt' + ) # read degree one files from Swenson et al. (2008) DEG1_file = kwargs.get('DEG1_FILE') or default_geocenter # log geocenter file if debugging @@ -647,11 +677,12 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, DEG1_input = gravity_toolkit.geocenter().from_swenson(DEG1_file) FLAGS.append(f'_w{DEG1}_DEG1') attributes['geocenter'] = ('Swenson', DEG1_file.name) - elif (DEG1 == 'GFZ'): + elif DEG1 == 'GFZ': # degree 1 coefficients provided by GFZ GravIS # http://gravis.gfz-potsdam.de/corrections - default_geocenter = base_dir.joinpath('geocenter', - 'GRAVIS-2B_GFZOP_GEOCENTER_0003.dat') + default_geocenter = base_dir.joinpath( + 'geocenter', 'GRAVIS-2B_GFZOP_GEOCENTER_0003.dat' + ) # read degree one files from GFZ GravIS DEG1_file = kwargs.get('DEG1_FILE') or default_geocenter # log geocenter file if debugging @@ -672,98 +703,98 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, grace_Ylms['title'] = ''.join(FLAGS) # Replace C20 with SLR coefficients - if SLR_C20 in ('CSR','GFZ','GSFC'): + if SLR_C20 in ('CSR', 'GFZ', 'GSFC'): # verify that there are replacement C20 months for specified range months_test = sorted(set(months) - set(C20_input['month'])) if months_test: gm = ','.join(f'{gm:03d}' for gm in months_test) raise IOError(f'No Matching C20 Months ({gm})') # replace C20 with SLR coefficients - for i,grace_month in enumerate(months): + for i, grace_month in enumerate(months): count = np.count_nonzero(C20_input['month'] == grace_month) - if (count != 0): - k, = np.flatnonzero(C20_input['month'] == grace_month) - grace_Ylms['clm'][2,0,i] = np.copy(C20_input['data'][k]) - grace_Ylms['eclm'][2,0,i] = np.copy(C20_input['error'][k]) + if count != 0: + (k,) = np.flatnonzero(C20_input['month'] == grace_month) + grace_Ylms['clm'][2, 0, i] = np.copy(C20_input['data'][k]) + grace_Ylms['eclm'][2, 0, i] = np.copy(C20_input['error'][k]) # Replace C21/S21 with SLR coefficients for single-accelerometer months - if kwargs['SLR_21'] in ('CSR','GFZ','GSFC'): + if kwargs['SLR_21'] in ('CSR', 'GFZ', 'GSFC'): # verify that there are replacement C21/S21 months for specified range months_test = sorted(set(single_acc_months) - set(C21_input['month'])) if months_test: gm = ','.join(f'{gm:03d}' for gm in months_test) raise IOError(f'No Matching C21/S21 Months ({gm})') # replace C21/S21 with SLR coefficients - for i,grace_month in enumerate(months): + for i, grace_month in enumerate(months): count = np.count_nonzero(C21_input['month'] == grace_month) if (count != 0) and (grace_month > 176): - k, = np.flatnonzero(C21_input['month'] == grace_month) - grace_Ylms['clm'][2,1,i] = np.copy(C21_input['C2m'][k]) - grace_Ylms['slm'][2,1,i] = np.copy(C21_input['S2m'][k]) - grace_Ylms['eclm'][2,1,i] = np.copy(C21_input['eC2m'][k]) - grace_Ylms['eslm'][2,1,i] = np.copy(C21_input['eS2m'][k]) + (k,) = np.flatnonzero(C21_input['month'] == grace_month) + grace_Ylms['clm'][2, 1, i] = np.copy(C21_input['C2m'][k]) + grace_Ylms['slm'][2, 1, i] = np.copy(C21_input['S2m'][k]) + grace_Ylms['eclm'][2, 1, i] = np.copy(C21_input['eC2m'][k]) + grace_Ylms['eslm'][2, 1, i] = np.copy(C21_input['eS2m'][k]) # Replace C22/S22 with SLR coefficients for single-accelerometer months - if kwargs['SLR_22'] in ('CSR','GSFC'): + if kwargs['SLR_22'] in ('CSR', 'GSFC'): # verify that there are replacement C22/S22 months for specified range months_test = sorted(set(single_acc_months) - set(C22_input['month'])) if months_test: gm = ','.join(f'{gm:03d}' for gm in months_test) raise IOError(f'No Matching C22/S22 Months ({gm})') # replace C22/S22 with SLR coefficients - for i,grace_month in enumerate(months): + for i, grace_month in enumerate(months): count = np.count_nonzero(C22_input['month'] == grace_month) if (count != 0) and (grace_month > 176): - k, = np.flatnonzero(C22_input['month'] == grace_month) - grace_Ylms['clm'][2,2,i] = np.copy(C22_input['C2m'][k]) - grace_Ylms['slm'][2,2,i] = np.copy(C22_input['S2m'][k]) - grace_Ylms['eclm'][2,2,i] = np.copy(C22_input['eC2m'][k]) - grace_Ylms['eslm'][2,2,i] = np.copy(C22_input['eS2m'][k]) + (k,) = np.flatnonzero(C22_input['month'] == grace_month) + grace_Ylms['clm'][2, 2, i] = np.copy(C22_input['C2m'][k]) + grace_Ylms['slm'][2, 2, i] = np.copy(C22_input['S2m'][k]) + grace_Ylms['eclm'][2, 2, i] = np.copy(C22_input['eC2m'][k]) + grace_Ylms['eslm'][2, 2, i] = np.copy(C22_input['eS2m'][k]) # Replace C30 with SLR coefficients for single-accelerometer months - if kwargs['SLR_C30'] in ('CSR','GFZ','GSFC','LARES'): + if kwargs['SLR_C30'] in ('CSR', 'GFZ', 'GSFC', 'LARES'): # verify that there are replacement C30 months for specified range months_test = sorted(set(single_acc_months) - set(C30_input['month'])) if months_test: gm = ','.join(f'{gm:03d}' for gm in months_test) raise IOError(f'No Matching C30 Months ({gm})') # replace C30 with SLR coefficients - for i,grace_month in enumerate(months): + for i, grace_month in enumerate(months): count = np.count_nonzero(C30_input['month'] == grace_month) if (count != 0) and (grace_month > 176): - k, = np.flatnonzero(C30_input['month'] == grace_month) - grace_Ylms['clm'][3,0,i] = np.copy(C30_input['data'][k]) - grace_Ylms['eclm'][3,0,i] = np.copy(C30_input['error'][k]) + (k,) = np.flatnonzero(C30_input['month'] == grace_month) + grace_Ylms['clm'][3, 0, i] = np.copy(C30_input['data'][k]) + grace_Ylms['eclm'][3, 0, i] = np.copy(C30_input['error'][k]) # Replace C40 with SLR coefficients for single-accelerometer months - if kwargs['SLR_C40'] in ('CSR','GSFC','LARES'): + if kwargs['SLR_C40'] in ('CSR', 'GSFC', 'LARES'): # verify that there are replacement C40 months for specified range months_test = sorted(set(single_acc_months) - set(C40_input['month'])) if months_test: gm = ','.join(f'{gm:03d}' for gm in months_test) raise IOError(f'No Matching C40 Months ({gm})') # replace C40 with SLR coefficients - for i,grace_month in enumerate(months): + for i, grace_month in enumerate(months): count = np.count_nonzero(C40_input['month'] == grace_month) if (count != 0) and (grace_month > 176): - k, = np.flatnonzero(C40_input['month'] == grace_month) - grace_Ylms['clm'][4,0,i] = np.copy(C40_input['data'][k]) - grace_Ylms['eclm'][4,0,i] = np.copy(C40_input['error'][k]) + (k,) = np.flatnonzero(C40_input['month'] == grace_month) + grace_Ylms['clm'][4, 0, i] = np.copy(C40_input['data'][k]) + grace_Ylms['eclm'][4, 0, i] = np.copy(C40_input['error'][k]) # Replace C50 with SLR coefficients for single-accelerometer months - if kwargs['SLR_C50'] in ('CSR','GSFC','LARES'): + if kwargs['SLR_C50'] in ('CSR', 'GSFC', 'LARES'): # verify that there are replacement C50 months for specified range months_test = sorted(set(single_acc_months) - set(C50_input['month'])) if months_test: gm = ','.join(f'{gm:03d}' for gm in months_test) raise IOError(f'No Matching C50 Months ({gm})') # replace C50 with SLR coefficients - for i,grace_month in enumerate(months): + for i, grace_month in enumerate(months): count = np.count_nonzero(C50_input['month'] == grace_month) if (count != 0) and (grace_month > 176): - k, = np.flatnonzero(C50_input['month'] == grace_month) - grace_Ylms['clm'][5,0,i] = np.copy(C50_input['data'][k]) - grace_Ylms['eclm'][5,0,i] = np.copy(C50_input['error'][k]) + (k,) = np.flatnonzero(C50_input['month'] == grace_month) + grace_Ylms['clm'][5, 0, i] = np.copy(C50_input['data'][k]) + grace_Ylms['eclm'][5, 0, i] = np.copy(C50_input['error'][k]) # Use Degree 1 coefficients # Tellus: Tellus Degree 1 (PO.DAAC following Sun et al., 2016) @@ -771,17 +802,35 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, # UCI: OMCT/MPIOM coefficients with Sea Level Fingerprint land-water mass # Swenson: GRACE-derived coefficients from Sean Swenson # GFZ: GRACE/GRACE-FO coefficients from GFZ GravIS - if DEG1 in ('GFZ','SLR','SLF','Swenson','Tellus','UCI'): + if DEG1 in ('GFZ', 'SLR', 'SLF', 'Swenson', 'Tellus', 'UCI'): # check if modeling degree 1 or if all months are available if kwargs['MODEL_DEG1']: # least-squares modeling the degree 1 coefficients # fitting annual, semi-annual, linear and quadratic terms - C10_model = regress_model(DEG1_input.time, DEG1_input.C10, - grace_Ylms['time'], ORDER=2, CYCLES=[0.5,1.0], RELATIVE=2003.3) - C11_model = regress_model(DEG1_input.time, DEG1_input.C11, - grace_Ylms['time'], ORDER=2, CYCLES=[0.5,1.0], RELATIVE=2003.3) - S11_model = regress_model(DEG1_input.time, DEG1_input.S11, - grace_Ylms['time'], ORDER=2, CYCLES=[0.5,1.0], RELATIVE=2003.3) + C10_model = regress_model( + DEG1_input.time, + DEG1_input.C10, + grace_Ylms['time'], + ORDER=2, + CYCLES=[0.5, 1.0], + RELATIVE=2003.3, + ) + C11_model = regress_model( + DEG1_input.time, + DEG1_input.C11, + grace_Ylms['time'], + ORDER=2, + CYCLES=[0.5, 1.0], + RELATIVE=2003.3, + ) + S11_model = regress_model( + DEG1_input.time, + DEG1_input.S11, + grace_Ylms['time'], + ORDER=2, + CYCLES=[0.5, 1.0], + RELATIVE=2003.3, + ) else: # check that all months are available for a given geocenter months_test = sorted(set(months) - set(DEG1_input.month)) @@ -789,19 +838,19 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, gm = ','.join(f'{gm:03d}' for gm in months_test) raise IOError(f'No Matching Geocenter Months ({gm})') # for each considered date - for i,grace_month in enumerate(months): - k, = np.flatnonzero(DEG1_input.month == grace_month) + for i, grace_month in enumerate(months): + (k,) = np.flatnonzero(DEG1_input.month == grace_month) count = np.count_nonzero(DEG1_input.month == grace_month) # Degree 1 is missing for particular month if (count == 0) and kwargs['MODEL_DEG1']: # using least-squares modeled coefficients from regress_model - grace_Ylms['clm'][1,0,i] = np.copy(C10_model[i]) - grace_Ylms['clm'][1,1,i] = np.copy(C11_model[i]) - grace_Ylms['slm'][1,1,i] = np.copy(S11_model[i]) - else:# using coefficients from data file - grace_Ylms['clm'][1,0,i] = np.copy(DEG1_input.C10[k]) - grace_Ylms['clm'][1,1,i] = np.copy(DEG1_input.C11[k]) - grace_Ylms['slm'][1,1,i] = np.copy(DEG1_input.S11[k]) + grace_Ylms['clm'][1, 0, i] = np.copy(C10_model[i]) + grace_Ylms['clm'][1, 1, i] = np.copy(C11_model[i]) + grace_Ylms['slm'][1, 1, i] = np.copy(S11_model[i]) + else: # using coefficients from data file + grace_Ylms['clm'][1, 0, i] = np.copy(DEG1_input.C10[k]) + grace_Ylms['clm'][1, 1, i] = np.copy(DEG1_input.C11[k]) + grace_Ylms['slm'][1, 1, i] = np.copy(DEG1_input.S11[k]) # read and add/remove the GAE and GAF atmospheric correction coefficients if kwargs['ATM']: @@ -810,17 +859,17 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, # add files to lineage attribute attributes['lineage'].extend(atm_corr['files']) # Removing GAE/GAF/GAG from RL05 GSM Products - if (DSET == 'GSM'): - for m in range(0,MMAX+1):# MMAX+1 to include l - for l in range(m,LMAX+1):# LMAX+1 to include LMAX - grace_Ylms['clm'][l,m,:] -= atm_corr['clm'][l,m,:] - grace_Ylms['slm'][l,m,:] -= atm_corr['slm'][l,m,:] + if DSET == 'GSM': + for m in range(0, MMAX + 1): # MMAX+1 to include l + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX + grace_Ylms['clm'][l, m, :] -= atm_corr['clm'][l, m, :] + grace_Ylms['slm'][l, m, :] -= atm_corr['slm'][l, m, :] # Adding GAE/GAF/GAG to RL05 Atmospheric Products (GAA,GAC) - elif DSET in ('GAC','GAA'): - for m in range(0,MMAX+1):# MMAX+1 to include l - for l in range(m,LMAX+1):# LMAX+1 to include LMAX - grace_Ylms['clm'][l,m,:] += atm_corr['clm'][l,m,:] - grace_Ylms['slm'][l,m,:] += atm_corr['slm'][l,m,:] + elif DSET in ('GAC', 'GAA'): + for m in range(0, MMAX + 1): # MMAX+1 to include l + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX + grace_Ylms['clm'][l, m, :] += atm_corr['clm'][l, m, :] + grace_Ylms['slm'][l, m, :] += atm_corr['slm'][l, m, :] # input directory for product grace_Ylms['directory'] = grace_dir @@ -830,6 +879,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, # return the harmonic solutions and associated attributes return grace_Ylms + # PURPOSE: read atmospheric jump corrections from Fagiolini et al. (2015) def read_ecmwf_corrections(base_dir, LMAX, months, MMAX=None): """ @@ -873,46 +923,47 @@ def read_ecmwf_corrections(base_dir, LMAX, months, MMAX=None): infile = base_dir.joinpath(val) logging.debug(f'Reading ECMWF file: {str(infile)}') # allocate for clm and slm of atmospheric corrections - atm_corr_clm[key] = np.zeros((LMAX+1, MMAX+1)) - atm_corr_slm[key] = np.zeros((LMAX+1, MMAX+1)) + atm_corr_clm[key] = np.zeros((LMAX + 1, MMAX + 1)) + atm_corr_slm[key] = np.zeros((LMAX + 1, MMAX + 1)) # GRACE correction files are compressed gz files - with gzip.open(infile,'rb') as f: + with gzip.open(infile, 'rb') as f: file_contents = f.read().decode('ISO-8859-1').splitlines() # for each line in the GRACE correction file for line in file_contents: # find if line starts with GRCOF2 - if bool(re.match(r'GRCOF2',line)): + if bool(re.match(r'GRCOF2', line)): # split the line into individual components line_contents = line.split() # degree and order for the line l1 = np.int64(line_contents[1]) m1 = np.int64(line_contents[2]) # if degree and order are below the truncation limits - if ((l1 <= LMAX) and (m1 <= MMAX)): - atm_corr_clm[key][l1,m1] = np.float64(line_contents[3]) - atm_corr_slm[key][l1,m1] = np.float64(line_contents[4]) + if (l1 <= LMAX) and (m1 <= MMAX): + atm_corr_clm[key][l1, m1] = np.float64(line_contents[3]) + atm_corr_slm[key][l1, m1] = np.float64(line_contents[4]) # create output atmospheric corrections to be removed/added to data atm_corr = {} - atm_corr['clm'] = np.zeros((LMAX+1, LMAX+1, n_cons)) - atm_corr['slm'] = np.zeros((LMAX+1, LMAX+1, n_cons)) + atm_corr['clm'] = np.zeros((LMAX + 1, LMAX + 1, n_cons)) + atm_corr['slm'] = np.zeros((LMAX + 1, LMAX + 1, n_cons)) atm_corr['files'] = sorted(corr_file.values()) # for each considered date - for i,grace_month in enumerate(months): + for i, grace_month in enumerate(months): # remove correction based on dates if (grace_month >= 50) & (grace_month <= 97): - atm_corr['clm'][:,:,i] = atm_corr_clm['GAE'][:,:] - atm_corr['slm'][:,:,i] = atm_corr_slm['GAE'][:,:] + atm_corr['clm'][:, :, i] = atm_corr_clm['GAE'][:, :] + atm_corr['slm'][:, :, i] = atm_corr_slm['GAE'][:, :] elif (grace_month >= 98) & (grace_month <= 161): - atm_corr['clm'][:,:,i] = atm_corr_clm['GAF'][:,:] - atm_corr['slm'][:,:,i] = atm_corr_slm['GAF'][:,:] - elif (grace_month > 161): - atm_corr['clm'][:,:,i] = atm_corr_clm['GAG'][:,:] - atm_corr['slm'][:,:,i] = atm_corr_slm['GAG'][:,:] + atm_corr['clm'][:, :, i] = atm_corr_clm['GAF'][:, :] + atm_corr['slm'][:, :, i] = atm_corr_slm['GAF'][:, :] + elif grace_month > 161: + atm_corr['clm'][:, :, i] = atm_corr_clm['GAG'][:, :] + atm_corr['slm'][:, :, i] = atm_corr_slm['GAG'][:, :] # return the atmospheric corrections return atm_corr + # PURPOSE: calculate a regression model for extrapolating values def regress_model(t_in, d_in, t_out, ORDER=2, CYCLES=None, RELATIVE=0.0): """ @@ -952,17 +1003,17 @@ def regress_model(t_in, d_in, t_out, ORDER=2, CYCLES=None, RELATIVE=0.0): DMAT = [] MMAT = [] # add polynomial orders (0=constant, 1=linear, 2=quadratic) - for o in range(ORDER+1): - DMAT.append((t_in-RELATIVE)**o) - MMAT.append((t_out-RELATIVE)**o) + for o in range(ORDER + 1): + DMAT.append((t_in - RELATIVE) ** o) + MMAT.append((t_out - RELATIVE) ** o) # add cyclical terms (0.5=semi-annual, 1=annual) for c in CYCLES: - DMAT.append(np.sin(2.0*np.pi*t_in/np.float64(c))) - DMAT.append(np.cos(2.0*np.pi*t_in/np.float64(c))) - MMAT.append(np.sin(2.0*np.pi*t_out/np.float64(c))) - MMAT.append(np.cos(2.0*np.pi*t_out/np.float64(c))) + DMAT.append(np.sin(2.0 * np.pi * t_in / np.float64(c))) + DMAT.append(np.cos(2.0 * np.pi * t_in / np.float64(c))) + MMAT.append(np.sin(2.0 * np.pi * t_out / np.float64(c))) + MMAT.append(np.cos(2.0 * np.pi * t_out / np.float64(c))) # Calculating Least-Squares Coefficients # Standard Least-Squares fitting (the [0] denotes coefficients output) beta_mat = np.linalg.lstsq(np.transpose(DMAT), d_in, rcond=-1)[0] # return modeled time-series - return np.dot(np.transpose(MMAT),beta_mat) + return np.dot(np.transpose(MMAT), beta_mat) diff --git a/gravity_toolkit/grace_months_index.py b/gravity_toolkit/grace_months_index.py index f42fb585..a294b44d 100644 --- a/gravity_toolkit/grace_months_index.py +++ b/gravity_toolkit/grace_months_index.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_months_index.py Written by Tyler Sutterley (05/2023) @@ -63,6 +63,7 @@ Updated 05/2013: added years to month label Written 07/2012 """ + from __future__ import print_function import pathlib @@ -71,7 +72,8 @@ import numpy as np import gravity_toolkit as gravtk -def grace_months_index(base_dir, DREL=['RL06','rl06v2.0'], MODE=None): + +def grace_months_index(base_dir, DREL=['RL06', 'rl06v2.0'], MODE=None): """ Creates a file with the start and end days for each dataset @@ -114,18 +116,19 @@ def grace_months_index(base_dir, DREL=['RL06','rl06v2.0'], MODE=None): # read GRACE/GRACE-FO date ascii file grace_date_file = grace_dir.joinpath(f'{pr}_{rl}_DATES.txt') # names and formats of GRACE/GRACE-FO date ascii file - names = ('t','mon','styr','stday','endyr','endday','total') - formats = ('f','i','i','i','i','i','i') - dtype = np.dtype({'names':names, 'formats':formats}) + names = ('t', 'mon', 'styr', 'stday', 'endyr', 'endday', 'total') + formats = ('f', 'i', 'i', 'i', 'i', 'i', 'i') + dtype = np.dtype({'names': names, 'formats': formats}) # check that the GRACE/GRACE-FO date file exists if grace_date_file.exists(): # Setting the dictionary key e.g. 'CSR_RL04' var_name = f'{pr}_{rl}' # skip the header line - var_info[var_name] = np.loadtxt(grace_date_file, - skiprows=1, dtype=dtype) + var_info[var_name] = np.loadtxt( + grace_date_file, skiprows=1, dtype=dtype + ) # Finding the maximum month measured - if (var_info[var_name]['mon'].max() > max_mon): + if var_info[var_name]['mon'].max() > max_mon: # if the maximum month in this dataset is greater # than the previously read datasets max_mon = np.int64(var_info[var_name]['mon'].max()) @@ -139,9 +142,9 @@ def grace_months_index(base_dir, DREL=['RL06','rl06v2.0'], MODE=None): # for each possible month # GRACE starts at month 004 (April 2002) # max_mon+1 to include max_mon - for m in range(4, max_mon+1): + for m in range(4, max_mon + 1): # finding the month name e.g. Apr - calendar_year,calendar_month = gravtk.time.grace_to_calendar(m) + calendar_year, calendar_month = gravtk.time.grace_to_calendar(m) month_string = calendar.month_abbr[calendar_month] # create list object for output string output_string = [] @@ -150,20 +153,21 @@ def grace_months_index(base_dir, DREL=['RL06','rl06v2.0'], MODE=None): # find if the month of data exists # exists will be greater than 0 if there is a match exists = np.count_nonzero(var_info[var]['mon'] == m) - if (exists != 0): + if exists != 0: # if there is a matching month # indice of matching month - ind, = np.nonzero(var_info[var]['mon'] == m) + (ind,) = np.nonzero(var_info[var]['mon'] == m) # start date - st_yr, = var_info[var]['styr'][ind] - st_day, = var_info[var]['stday'][ind] + (st_yr,) = var_info[var]['styr'][ind] + (st_day,) = var_info[var]['stday'][ind] # end date - end_yr, = var_info[var]['endyr'][ind] - end_day, = var_info[var]['endday'][ind] + (end_yr,) = var_info[var]['endyr'][ind] + (end_day,) = var_info[var]['endday'][ind] # output string is the date range # string format: 2002_102--2002_120 - output_string.append(f'{st_yr:4d}_{st_day:03d}--' - f'{end_yr:4d}_{end_day:03d}') + output_string.append( + f'{st_yr:4d}_{st_day:03d}--{end_yr:4d}_{end_day:03d}' + ) else: # if there is no matching month = missing output_string.append(' ** missing ** ') @@ -180,6 +184,7 @@ def grace_months_index(base_dir, DREL=['RL06','rl06v2.0'], MODE=None): # set the permissions level of the output file grace_months_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -189,31 +194,45 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, nargs='+', - default=['RL06','rl06v2.0'], - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + nargs='+', + default=['RL06', 'rl06v2.0'], + help='GRACE/GRACE-FO Data Release', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run GRACE/GRACE-FO months program grace_months_index(args.directory, DREL=args.release, MODE=args.mode) + # run main program if __name__ == '__main__': main() diff --git a/gravity_toolkit/harmonic_gradients.py b/gravity_toolkit/harmonic_gradients.py index c78834d4..8b4704cd 100644 --- a/gravity_toolkit/harmonic_gradients.py +++ b/gravity_toolkit/harmonic_gradients.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" harmonic_gradients.py Original IDL code calc_grad.pro written by Sean Swenson Adapted by Tyler Sutterley (07/2026) @@ -39,6 +39,7 @@ Updated 05/2015: code updates Written 05/2013 """ + from __future__ import division import numpy as np from gravity_toolkit.fourier_legendre import legendre_gradient @@ -46,8 +47,8 @@ from gravity_toolkit.gauss_weights import gauss_weights from gravity_toolkit.units import units -def harmonic_gradients(clm1, slm1, lon, lat, - LMIN=0, LMAX=60, MMAX=None): + +def harmonic_gradients(clm1, slm1, lon, lat, LMIN=0, LMAX=60, MMAX=None): """ Calculates the gradient of a scalar field from a series of spherical harmonics :cite:p:`Driscoll:1994bp` @@ -76,8 +77,8 @@ def harmonic_gradients(clm1, slm1, lon, lat, """ # if LMAX is not specified, will use the size of the input harmonics - if (LMAX == 0): - LMAX = np.shape(clm1)[0]-1 + if LMAX == 0: + LMAX = np.shape(clm1)[0] - 1 # upper bound of spherical harmonic orders (default = LMAX) if MMAX is None: MMAX = np.copy(LMAX) @@ -89,41 +90,56 @@ def harmonic_gradients(clm1, slm1, lon, lat, thmax = len(th) # spherical harmonic degree and order - ll = np.arange(0,LMAX+1)# lmax+1 to include lmax - mm = np.arange(0,MMAX+1)# mmax+1 to include mmax + ll = np.arange(0, LMAX + 1) # lmax+1 to include lmax + mm = np.arange(0, MMAX + 1) # mmax+1 to include mmax # real (cosine) and imaginary (sine) components - Ylm = np.zeros((LMAX+1, MMAX+1), dtype=np.complex128) + Ylm = np.zeros((LMAX + 1, MMAX + 1), dtype=np.complex128) # Truncating harmonics to degree and order LMAX # removing coefficients below LMIN and above MMAX - Ylm.real[LMIN:LMAX+1,:MMAX+1] = clm1[LMIN:LMAX+1,:MMAX+1].copy() - Ylm.imag[LMIN:LMAX+1,:MMAX+1] = -slm1[LMIN:LMAX+1,:MMAX+1].copy() - dlm = np.einsum("l...,lm...->lm", np.sqrt((ll+1.0)*ll), -1j*Ylm) + Ylm.real[LMIN : LMAX + 1, : MMAX + 1] = clm1[ + LMIN : LMAX + 1, : MMAX + 1 + ].copy() + Ylm.imag[LMIN : LMAX + 1, : MMAX + 1] = -slm1[ + LMIN : LMAX + 1, : MMAX + 1 + ].copy() + dlm = np.einsum('l...,lm...->lm', np.sqrt((ll + 1.0) * ll), -1j * Ylm) # generate Vlm coefficients (vlm and wlm) Vlmk, Wlmk = legendre_gradient(LMAX, MMAX) # even and odd spherical harmonic orders - m_even = np.arange(0, MMAX+2, 2) + m_even = np.arange(0, MMAX + 2, 2) m_odd = np.arange(1, MMAX, 2) # Euler's formula for theta * k and m * phi - k_th = np.exp(1j * np.einsum("h...,k...->kh...", th, ll)) - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", mm, phi)) + k_th = np.exp(1j * np.einsum('h...,k...->kh...', th, ll)) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', mm, phi)) # Calculate fourier coefficients from legendre coefficients - d = np.zeros((LMAX+1, thmax, 2), dtype=np.complex128) - wtmp = np.einsum("lmk...,lm...->mk", Wlmk[:,:MMAX+1,:], dlm) - vtmp = np.einsum("lmk...,lm...->mk", Vlmk[:,:MMAX+1,:], dlm) - d[m_even,:,0] = np.einsum("mk...,kh...->mh", wtmp[m_even,:], k_th.imag) - d[m_even,:,1] = np.einsum("mk...,kh...->mh", vtmp[m_even,:], k_th.imag) - d[m_odd,:,0] = np.einsum("mk...,kh...->mh", wtmp[m_odd,:], k_th.real) - d[m_odd,:,1] = np.einsum("mk...,kh...->mh", vtmp[m_odd,:], k_th.real) + d = np.zeros((LMAX + 1, thmax, 2), dtype=np.complex128) + wtmp = np.einsum('lmk...,lm...->mk', Wlmk[:, : MMAX + 1, :], dlm) + vtmp = np.einsum('lmk...,lm...->mk', Vlmk[:, : MMAX + 1, :], dlm) + d[m_even, :, 0] = np.einsum('mk...,kh...->mh', wtmp[m_even, :], k_th.imag) + d[m_even, :, 1] = np.einsum('mk...,kh...->mh', vtmp[m_even, :], k_th.imag) + d[m_odd, :, 0] = np.einsum('mk...,kh...->mh', wtmp[m_odd, :], k_th.real) + d[m_odd, :, 1] = np.einsum('mk...,kh...->mh', vtmp[m_odd, :], k_th.real) # calculate the zonal and meridional gradients of the scalar field - gradients = np.einsum("mp...,mhd...->phd...", m_phi, d) + gradients = np.einsum('mp...,mhd...->phd...', m_phi, d) # return the gradient fields and drop imaginary component return gradients.real -def geostrophic_currents(clm1, slm1, lon, lat, - LMIN=0, LMAX=60, MMAX=None, RAD=0, - DENSITY=1.035, LOVE=None, PLM=None): + +def geostrophic_currents( + clm1, + slm1, + lon, + lat, + LMIN=0, + LMAX=60, + MMAX=None, + RAD=0, + DENSITY=1.035, + LOVE=None, + PLM=None, +): r""" Converts data from spherical harmonic coefficients to spatial fields of approximate ocean geostrophic currents following @@ -163,8 +179,8 @@ def geostrophic_currents(clm1, slm1, lon, lat, """ # if LMAX is not specified, will use the size of the input harmonics - if (LMAX == 0): - LMAX = np.shape(clm1)[0]-1 + if LMAX == 0: + LMAX = np.shape(clm1)[0] - 1 # upper bound of spherical harmonic orders (default = LMAX) if MMAX is None: MMAX = np.copy(LMAX) @@ -177,61 +193,76 @@ def geostrophic_currents(clm1, slm1, lon, lat, thmax = len(th) # Gaussian Smoothing - if (RAD != 0): - wl = 2.0*np.pi*gauss_weights(RAD, LMAX) + if RAD != 0: + wl = 2.0 * np.pi * gauss_weights(RAD, LMAX) else: # else = 1 - wl = np.ones((LMAX+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) - coeff = factors.g_wmo*factors.rho_e/(6.0*factors.omega*DENSITY) + coeff = factors.g_wmo * factors.rho_e / (6.0 * factors.omega * DENSITY) # if plms are not pre-computed: calculate Legendre polynomials if PLM is None: PLM, dPLM = plm_holmes(LMAX, np.cos(th)) # smooth harmonics and convert to output units - clm = np.zeros((LMAX+1, MMAX+1, 2)) - slm = np.zeros((LMAX+1, MMAX+1, 2)) + clm = np.zeros((LMAX + 1, MMAX + 1, 2)) + slm = np.zeros((LMAX + 1, MMAX + 1, 2)) # zonal flow harmonics (equation 3) # differentiating Legendre polynomials with respect to longitude for l in range(1, LMAX): # truncate to degree and order - mm = np.arange(0, np.min([l,MMAX])+1) - temp1 = (l - 1.0)/(1.0 + LOVE.kl[l-1]) * \ - np.sqrt((l**2 - mm**2)*(2.0*l - 1.0)/(2.0*l + 1)) - temp2 = (l + 2.0)/(1.0 + LOVE.kl[l+1]) * \ - np.sqrt(((l+1)**2 - mm**2)*(2.0*l + 3.0)/(2.0*l + 1)) - clm[l,mm,0] = coeff*wl[l]*(temp1*clm1[l-1,mm] - temp2*clm1[l+1,mm]) - slm[l,mm,0] = coeff*wl[l]*(temp1*slm1[l-1,mm] - temp2*slm1[l+1,mm]) + mm = np.arange(0, np.min([l, MMAX]) + 1) + temp1 = ( + (l - 1.0) + / (1.0 + LOVE.kl[l - 1]) + * np.sqrt((l**2 - mm**2) * (2.0 * l - 1.0) / (2.0 * l + 1)) + ) + temp2 = ( + (l + 2.0) + / (1.0 + LOVE.kl[l + 1]) + * np.sqrt(((l + 1) ** 2 - mm**2) * (2.0 * l + 3.0) / (2.0 * l + 1)) + ) + clm[l, mm, 0] = ( + coeff * wl[l] * (temp1 * clm1[l - 1, mm] - temp2 * clm1[l + 1, mm]) + ) + slm[l, mm, 0] = ( + coeff * wl[l] * (temp1 * slm1[l - 1, mm] - temp2 * slm1[l + 1, mm]) + ) # meridional flow harmonics (equation 4) # differentiating Legendre polynomials with respect to colatitude - for l in range(0, LMAX+1): + for l in range(0, LMAX + 1): # truncate to degree and order - mm = np.arange(0, np.min([l,MMAX])+1) - temp = mm*(2.0*l + 1.0)/(1.0 + LOVE.kl[l]) - clm[l,mm,1] = -coeff*wl[l]*temp*slm1[l,mm] - slm[l,mm,1] = coeff*wl[l]*temp*clm1[l,mm] + mm = np.arange(0, np.min([l, MMAX]) + 1) + temp = mm * (2.0 * l + 1.0) / (1.0 + LOVE.kl[l]) + clm[l, mm, 1] = -coeff * wl[l] * temp * slm1[l, mm] + slm[l, mm, 1] = coeff * wl[l] * temp * clm1[l, mm] # Truncating harmonics to degree and order LMAX # removing coefficients below LMIN and above MMAX - mm = np.arange(0, MMAX+1) + mm = np.arange(0, MMAX + 1) # real (cosine) and imaginary (sine) components - Ylm = clm[LMIN:LMAX+1,:MMAX+1,:] - 1j * slm[LMIN:LMAX+1,:MMAX+1,:] + Ylm = ( + clm[LMIN : LMAX + 1, : MMAX + 1, :] + - 1j * slm[LMIN : LMAX + 1, : MMAX + 1, :] + ) # convolve legendre polynomials and truncate to degree and order - iint = 1.0/(np.cos(th)*np.sin(th)) - plm = np.einsum("h...,lmh...->lmh...", iint, PLM[LMIN:LMAX+1,:MMAX+1,:]) + iint = 1.0 / (np.cos(th) * np.sin(th)) + plm = np.einsum( + 'h...,lmh...->lmh...', iint, PLM[LMIN : LMAX + 1, : MMAX + 1, :] + ) # summation over all spherical harmonic degrees - pconv = np.einsum("lmh...,lmd...->mhd...", plm, Ylm) + pconv = np.einsum('lmh...,lmd...->mhd...', plm, Ylm) # calculating cos(m*phi) and sin(m*phi) using Euler's formula - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", mm, phi)) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', mm, phi)) # output geostrophic current fields - currents = np.empty((phmax,thmax,2)) + currents = np.empty((phmax, thmax, 2)) # summation of cosine and sine harmonics - currents[:] = np.einsum("mp...,mhd...->phd...", m_phi, pconv) + currents[:] = np.einsum('mp...,mhd...->phd...', m_phi, pconv) # return the current fields and drop imaginary component return currents.real diff --git a/gravity_toolkit/harmonic_summation.py b/gravity_toolkit/harmonic_summation.py index 92d72224..47c92b8f 100755 --- a/gravity_toolkit/harmonic_summation.py +++ b/gravity_toolkit/harmonic_summation.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" harmonic_summation.py Written by Tyler Sutterley (07/2026) @@ -45,13 +45,16 @@ Updated 05/2015: added parameter MMAX for MMAX != LMAX. Written 05/2013 """ + import numpy as np from gravity_toolkit.associated_legendre import plm_holmes from gravity_toolkit.gauss_weights import gauss_weights from gravity_toolkit.units import units -def harmonic_summation(clm1, slm1, lon, lat, - LMIN=0, LMAX=60, MMAX=None, PLM=None): + +def harmonic_summation( + clm1, slm1, lon, lat, LMIN=0, LMAX=60, MMAX=None, PLM=None +): """ Converts data from spherical harmonic coefficients to a spatial field @@ -81,8 +84,8 @@ def harmonic_summation(clm1, slm1, lon, lat, """ # if LMAX is not specified, will use the size of the input harmonics - if (LMAX == 0): - LMAX = np.shape(clm1)[0]-1 + if LMAX == 0: + LMAX = np.shape(clm1)[0] - 1 # upper bound of spherical harmonic orders (default = LMAX) if MMAX is None: MMAX = np.copy(LMAX) @@ -97,25 +100,29 @@ def harmonic_summation(clm1, slm1, lon, lat, PLM, dPLM = plm_holmes(LMAX, np.cos(th)) # spherical harmonic order - mm = np.arange(0,MMAX+1)# mmax+1 to include mmax + mm = np.arange(0, MMAX + 1) # mmax+1 to include mmax # real (cosine) and imaginary (sine) components - Ylm = np.zeros((LMAX+1, MMAX+1), dtype=np.complex128) + Ylm = np.zeros((LMAX + 1, MMAX + 1), dtype=np.complex128) # Truncating harmonics to degree and order LMAX # removing coefficients below LMIN and above MMAX - Ylm.real[LMIN:LMAX+1,mm] = clm1[LMIN:LMAX+1,mm] - Ylm.imag[LMIN:LMAX+1,mm] = -slm1[LMIN:LMAX+1,mm] + Ylm.real[LMIN : LMAX + 1, mm] = clm1[LMIN : LMAX + 1, mm] + Ylm.imag[LMIN : LMAX + 1, mm] = -slm1[LMIN : LMAX + 1, mm] # Calculate fourier coefficients from legendre coefficients # summation over all spherical harmonic degrees - pconv = np.einsum("lmh...,lm...->mh...", PLM[:LMAX+1,:MMAX+1,:], Ylm) + pconv = np.einsum( + 'lmh...,lm...->mh...', PLM[: LMAX + 1, : MMAX + 1, :], Ylm + ) # calculating cos(m*phi) and sin(m*phi) using Euler's formula - m_phi = np.exp(1j * np.einsum("m...,p...->mp...", mm, phi)) + m_phi = np.exp(1j * np.einsum('m...,p...->mp...', mm, phi)) # summation of cosine and sine harmonics - spatial = np.einsum("mp...,mh...->ph...", m_phi, pconv) + spatial = np.einsum('mp...,mh...->ph...', m_phi, pconv) # return output data and drop imaginary component return spatial.real -def harmonic_transform(clm1, slm1, lon, lat, - LMIN=0, LMAX=60, MMAX=None, PLM=None): + +def harmonic_transform( + clm1, slm1, lon, lat, LMIN=0, LMAX=60, MMAX=None, PLM=None +): """ Converts data from spherical harmonic coefficients to a spatial field using Fast-Fourier Transforms @@ -145,8 +152,8 @@ def harmonic_transform(clm1, slm1, lon, lat, spatial field """ # if LMAX is not specified, will use the size of the input harmonics - if (LMAX == 0): - LMAX = np.shape(clm1)[0]-1 + if LMAX == 0: + LMAX = np.shape(clm1)[0] - 1 # upper bound of spherical harmonic orders (default = LMAX) if MMAX is None: MMAX = np.copy(LMAX) @@ -165,26 +172,40 @@ def harmonic_transform(clm1, slm1, lon, lat, PLM, _ = plm_holmes(LMAX, np.cos(th)) # real (cosine) and imaginary (sine) components - Ylm = np.zeros((LMAX+1, MMAX+1), dtype=np.complex128) + Ylm = np.zeros((LMAX + 1, MMAX + 1), dtype=np.complex128) # Truncating harmonics to degree and order LMAX # removing coefficients below LMIN and above MMAX - Ylm.real[LMIN:LMAX+1,:MMAX+1] = clm1[LMIN:LMAX+1,:MMAX+1] - Ylm.imag[LMIN:LMAX+1,:MMAX+1] = -slm1[LMIN:LMAX+1,:MMAX+1] + Ylm.real[LMIN : LMAX + 1, : MMAX + 1] = clm1[LMIN : LMAX + 1, : MMAX + 1] + Ylm.imag[LMIN : LMAX + 1, : MMAX + 1] = -slm1[LMIN : LMAX + 1, : MMAX + 1] # calculate Ylms summation for each theta band - d = np.einsum("lmh...,lm...->mh...", PLM[:LMAX+1,:MMAX+1,:], Ylm / 2.0) + d = np.einsum( + 'lmh...,lm...->mh...', PLM[: LMAX + 1, : MMAX + 1, :], Ylm / 2.0 + ) # output spatial field from FFT transformation s = np.zeros((phimax, thmax)) # calculate fft for each theta band (over phis with axis=0) - s[:-1,:] = 2.0*(phimax-1)*np.fft.ifft(d, n=phimax-1, axis=0).real + s[:-1, :] = 2.0 * (phimax - 1) * np.fft.ifft(d, n=phimax - 1, axis=0).real # complete sphere (values at 360 == values at 0) - s[-1,:] = s[0,:] + s[-1, :] = s[0, :] # return output data return s -def stokes_summation(clm1, slm1, lon, lat, - LMIN=0, LMAX=60, MMAX=None, RAD=0, UNITS=0, LOVE=None, PLM=None): + +def stokes_summation( + clm1, + slm1, + lon, + lat, + LMIN=0, + LMAX=60, + MMAX=None, + RAD=0, + UNITS=0, + LOVE=None, + PLM=None, +): r""" Converts data from spherical harmonic coefficients to a spatial field :cite:p:`Wahr:1998hy` @@ -230,23 +251,23 @@ def stokes_summation(clm1, slm1, lon, lat, spatial field """ # if LMAX is not specified, will use the size of the input harmonics - if (LMAX == 0): - LMAX = np.shape(clm1)[0]-1 + if LMAX == 0: + LMAX = np.shape(clm1)[0] - 1 # upper bound of spherical harmonic orders (default = LMAX) if MMAX is None: MMAX = np.copy(LMAX) # Gaussian Smoothing - if (RAD != 0): - wl = 2.0*np.pi*gauss_weights(RAD, LMAX) + if RAD != 0: + wl = 2.0 * np.pi * gauss_weights(RAD, LMAX) else: # else = 1 - wl = np.ones((LMAX+1)) + wl = np.ones((LMAX + 1)) # Setting units factor for output # dfactor is the degree dependent coefficients factors = units(lmax=LMAX) - if isinstance(UNITS, (list,np.ndarray)): + if isinstance(UNITS, (list, np.ndarray)): # custom units dfactor = np.copy(UNITS) elif isinstance(UNITS, str): @@ -259,11 +280,12 @@ def stokes_summation(clm1, slm1, lon, lat, raise ValueError(f'Unknown units {UNITS}') # spherical harmonic order - mm = np.arange(0,MMAX+1)# mmax+1 to include mmax + mm = np.arange(0, MMAX + 1) # mmax+1 to include mmax # smooth harmonics and convert to output units - clm = np.einsum("l,l,lm->lm", wl, dfactor, clm1[:, mm]) - slm = np.einsum("l,l,lm->lm", wl, dfactor, slm1[:, mm]) + clm = np.einsum('l,l,lm->lm', wl, dfactor, clm1[:, mm]) + slm = np.einsum('l,l,lm->lm', wl, dfactor, slm1[:, mm]) # return the spatial field - return harmonic_summation(clm, slm, lon, lat, - LMIN=LMIN, LMAX=LMAX, MMAX=MMAX, PLM=PLM) + return harmonic_summation( + clm, slm, lon, lat, LMIN=LMIN, LMAX=LMAX, MMAX=MMAX, PLM=PLM + ) diff --git a/gravity_toolkit/harmonics.py b/gravity_toolkit/harmonics.py index f90a1ad0..b90d090f 100644 --- a/gravity_toolkit/harmonics.py +++ b/gravity_toolkit/harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" harmonics.py Written by Tyler Sutterley (07/2026) Contributions by Hugo Lecomte @@ -92,6 +92,7 @@ add options to flatten and expand harmonics matrices or arrays Written 03/2020 """ + from __future__ import print_function, division import re @@ -105,7 +106,7 @@ import zipfile import numpy as np import gravity_toolkit.version -from gravity_toolkit.time import adjust_months,calendar_to_grace +from gravity_toolkit.time import adjust_months, calendar_to_grace from gravity_toolkit.destripe_harmonics import destripe_harmonics from gravity_toolkit.read_gfc_harmonics import read_gfc_harmonics from gravity_toolkit.read_GRACE_harmonics import read_GRACE_harmonics @@ -117,6 +118,7 @@ netCDF4 = import_dependency('netCDF4') sparse = import_dependency('sparse') + class harmonics(object): """ Data class for reading, writing and processing spherical harmonic data @@ -142,24 +144,26 @@ class harmonics(object): flattened: bool ``harmonics`` object is compressed into arrays """ + np.seterr(invalid='ignore') + def __init__(self, **kwargs): # set default keyword arguments - kwargs.setdefault('lmax',None) - kwargs.setdefault('mmax',None) + kwargs.setdefault('lmax', None) + kwargs.setdefault('mmax', None) # set default class attributes - self.clm=None - self.slm=None - self.time=None - self.month=None - self.lmax=kwargs['lmax'] - self.mmax=kwargs['mmax'] + self.clm = None + self.slm = None + self.time = None + self.month = None + self.lmax = kwargs['lmax'] + self.mmax = kwargs['mmax'] # calculate spherical harmonic degree and order (0 is falsy) - self.l=np.arange(self.lmax+1) if (self.lmax is not None) else None - self.m=np.arange(self.mmax+1) if (self.mmax is not None) else None - self.attributes=dict() - self.filename=None - self.flattened=False + self.l = np.arange(self.lmax + 1) if (self.lmax is not None) else None + self.m = np.arange(self.mmax + 1) if (self.mmax is not None) else None + self.attributes = dict() + self.filename = None + self.flattened = False # iterator self.__index__ = 0 @@ -183,8 +187,11 @@ def case_insensitive_filename(self, filename): # check if file presently exists with input case if not self.filename.exists(): # search for filename without case dependence - f = [f.name for f in self.filename.parent.iterdir() if - re.match(self.filename.name, f.name, re.I)] + f = [ + f.name + for f in self.filename.parent.iterdir() + if re.match(self.filename.name, f.name, re.I) + ] if not f: msg = f'{filename} not found in file system' raise FileNotFoundError(msg) @@ -235,21 +242,21 @@ def from_ascii(self, filename, **kwargs): # set filename self.case_insensitive_filename(filename) # set default parameters - kwargs.setdefault('date',True) - kwargs.setdefault('verbose',False) - kwargs.setdefault('compression',None) + kwargs.setdefault('date', True) + kwargs.setdefault('verbose', False) + kwargs.setdefault('compression', None) # open the ascii file and extract contents logging.info(self.filename) - if (kwargs['compression'] == 'gzip'): + if kwargs['compression'] == 'gzip': # read input ascii data from gzip compressed file and split lines with gzip.open(self.filename, mode='r') as f: file_contents = f.read().decode('ISO-8859-1').splitlines() - elif (kwargs['compression'] == 'zip'): + elif kwargs['compression'] == 'zip': # read input ascii data from zipped file and split lines stem = self.filename.stem with zipfile.ZipFile(self.filename) as z: file_contents = z.read(stem).decode('ISO-8859-1').splitlines() - elif (kwargs['compression'] == 'bytes'): + elif kwargs['compression'] == 'bytes': # read input file object and split lines file_contents = self.filename.read().splitlines() else: @@ -265,14 +272,14 @@ def from_ascii(self, filename, **kwargs): self.mmax = 0 # for each line in the file for line in file_contents: - l1,m1,clm1,slm1,*aux = rx.findall(line) + l1, m1, clm1, slm1, *aux = rx.findall(line) # convert line degree and order to integers - l1,m1 = np.array([l1,m1],dtype=np.int64) + l1, m1 = np.array([l1, m1], dtype=np.int64) self.lmax = np.copy(l1) if (l1 > self.lmax) else self.lmax self.mmax = np.copy(m1) if (m1 > self.mmax) else self.mmax # output spherical harmonics data - self.clm = np.zeros((self.lmax+1,self.mmax+1)) - self.slm = np.zeros((self.lmax+1,self.mmax+1)) + self.clm = np.zeros((self.lmax + 1, self.mmax + 1)) + self.slm = np.zeros((self.lmax + 1, self.mmax + 1)) # if the ascii file contains date variables if kwargs['date']: self.time = np.float64(aux[0]) @@ -282,12 +289,12 @@ def from_ascii(self, filename, **kwargs): # extract harmonics and convert to matrix # for each line in the file for line in file_contents: - l1,m1,clm1,slm1,*aux = rx.findall(line) + l1, m1, clm1, slm1, *aux = rx.findall(line) # convert line degree and order to integers - ll,mm = np.array([l1,m1],dtype=np.int64) + ll, mm = np.array([l1, m1], dtype=np.int64) # convert fortran exponentials if applicable - self.clm[ll,mm] = np.float64(clm1.replace('D','E')) - self.slm[ll,mm] = np.float64(slm1.replace('D','E')) + self.clm[ll, mm] = np.float64(clm1.replace('D', 'E')) + self.slm[ll, mm] = np.float64(slm1.replace('D', 'E')) # assign degree and order fields self.update_dimensions() return self @@ -314,27 +321,29 @@ def from_netCDF4(self, filename, **kwargs): # set filename self.case_insensitive_filename(filename) # set default parameters - kwargs.setdefault('date',True) - kwargs.setdefault('verbose',False) - kwargs.setdefault('compression',None) + kwargs.setdefault('date', True) + kwargs.setdefault('verbose', False) + kwargs.setdefault('compression', None) # Open the NetCDF4 file for reading - if (kwargs['compression'] == 'gzip'): + if kwargs['compression'] == 'gzip': # read as in-memory (diskless) netCDF4 dataset with gzip.open(self.filename, mode='r') as f: fileID = netCDF4.Dataset(uuid.uuid4().hex, memory=f.read()) - elif (kwargs['compression'] == 'zip'): + elif kwargs['compression'] == 'zip': # read zipped file and extract file into in-memory file object stem = self.filename.stem with zipfile.ZipFile(self.filename) as z: # first try finding a netCDF4 file with same base filename # if none found simply try searching for a netCDF4 file try: - f,=[f for f in z.namelist() if re.match(stem,f,re.I)] + (f,) = [f for f in z.namelist() if re.match(stem, f, re.I)] except: - f,=[f for f in z.namelist() if re.search(r'\.nc(4)?$',f)] + (f,) = [ + f for f in z.namelist() if re.search(r'\.nc(4)?$', f) + ] # read bytes from zipfile as in-memory (diskless) netCDF4 dataset fileID = netCDF4.Dataset(uuid.uuid4().hex, memory=z.read(f)) - elif (kwargs['compression'] == 'bytes'): + elif kwargs['compression'] == 'bytes': # read as in-memory (diskless) netCDF4 dataset fileID = netCDF4.Dataset(uuid.uuid4().hex, memory=filename.read()) else: @@ -347,10 +356,10 @@ def from_netCDF4(self, filename, **kwargs): temp = harmonics() temp.filename = copy.copy(self.filename) # create list of variables to retrieve - fields = ['l','m','clm','slm'] + fields = ['l', 'm', 'clm', 'slm'] # retrieve date variables if specified if kwargs['date']: - fields.extend(['time','month']) + fields.extend(['time', 'month']) # Getting the data from each NetCDF variable for field in fields: setattr(temp, field, fileID.variables[field][:].copy()) @@ -367,9 +376,9 @@ def from_netCDF4(self, filename, **kwargs): try: self.attributes[key] = [ fileID.variables[key].units, - fileID.variables[key].long_name - ] - except (KeyError,ValueError,AttributeError): + fileID.variables[key].long_name, + ] + except (KeyError, ValueError, AttributeError): pass # get global netCDF4 attributes self.attributes['ROOT'] = {} @@ -404,11 +413,11 @@ def from_HDF5(self, filename, **kwargs): # set filename self.case_insensitive_filename(filename) # set default parameters - kwargs.setdefault('date',True) - kwargs.setdefault('verbose',False) - kwargs.setdefault('compression',None) + kwargs.setdefault('date', True) + kwargs.setdefault('verbose', False) + kwargs.setdefault('compression', None) # Open the HDF5 file for reading - if (kwargs['compression'] == 'gzip'): + if kwargs['compression'] == 'gzip': # read gzip compressed file and extract into in-memory file object with gzip.open(self.filename, mode='r') as f: fid = io.BytesIO(f.read()) @@ -418,16 +427,20 @@ def from_HDF5(self, filename, **kwargs): fid.seek(0) # read as in-memory (diskless) HDF5 dataset from BytesIO object fileID = h5py.File(fid, mode='r') - elif (kwargs['compression'] == 'zip'): + elif kwargs['compression'] == 'zip': # read zipped file and extract file into in-memory file object stem = self.filename.stem with zipfile.ZipFile(self.filename) as z: # first try finding a HDF5 file with same base filename # if none found simply try searching for a HDF5 file try: - f,=[f for f in z.namelist() if re.match(stem,f,re.I)] + (f,) = [f for f in z.namelist() if re.match(stem, f, re.I)] except: - f,=[f for f in z.namelist() if re.search(r'\.H(DF)?5$',f,re.I)] + (f,) = [ + f + for f in z.namelist() + if re.search(r'\.H(DF)?5$', f, re.I) + ] # read bytes from zipfile into in-memory BytesIO object fid = io.BytesIO(z.read(f)) # set filename of BytesIO object @@ -436,7 +449,7 @@ def from_HDF5(self, filename, **kwargs): fid.seek(0) # read as in-memory (diskless) HDF5 dataset from BytesIO object fileID = h5py.File(fid, mode='r') - elif (kwargs['compression'] == 'bytes'): + elif kwargs['compression'] == 'bytes': # read as in-memory (diskless) HDF5 dataset fileID = h5py.File(self.filename, mode='r') else: @@ -449,10 +462,10 @@ def from_HDF5(self, filename, **kwargs): temp = harmonics() temp.filename = copy.copy(self.filename) # create list of variables to retrieve - fields = ['l','m','clm','slm'] + fields = ['l', 'm', 'clm', 'slm'] # retrieve date variables if specified if kwargs['date']: - fields.extend(['time','month']) + fields.extend(['time', 'month']) # Getting the data from each HDF5 variable for field in fields: setattr(temp, field, fileID[field][:].copy()) @@ -469,13 +482,13 @@ def from_HDF5(self, filename, **kwargs): try: self.attributes[key] = [ fileID[key].attrs['units'], - fileID[key].attrs['long_name'] - ] + fileID[key].attrs['long_name'], + ] except (KeyError, AttributeError): pass # get global HDF5 attributes self.attributes['ROOT'] = {} - for att_name,att_val in fileID.attrs.items(): + for att_name, att_val in fileID.attrs.items(): self.attributes['ROOT'][att_name] = att_val # Closing the HDF5 file fileID.close() @@ -506,16 +519,16 @@ def from_gfc(self, filename, **kwargs): # set filename self.case_insensitive_filename(filename) # set default parameters - kwargs.setdefault('date',False) - kwargs.setdefault('tide',None) - kwargs.setdefault('verbose',False) + kwargs.setdefault('date', False) + kwargs.setdefault('tide', None) + kwargs.setdefault('verbose', False) # read data from gfc file if kwargs['date']: - Ylms = read_gfc_harmonics(self.filename, - TIDE=kwargs['tide']) + Ylms = read_gfc_harmonics(self.filename, TIDE=kwargs['tide']) else: - Ylms = geoidtk.read_ICGEM_harmonics(self.filename, - TIDE=kwargs['tide']) + Ylms = geoidtk.read_ICGEM_harmonics( + self.filename, TIDE=kwargs['tide'] + ) # Output file information logging.info(self.filename) logging.info(list(Ylms.keys())) @@ -555,7 +568,7 @@ def from_SHM(self, filename, **kwargs): # set filename self.case_insensitive_filename(filename) # set default keyword arguments - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) # read data from SHM file Ylms = read_GRACE_harmonics(self.filename, self.lmax, **kwargs) # Output file information @@ -592,9 +605,9 @@ def from_index(self, filename, **kwargs): sort ``harmonics`` objects by date information """ # set default keyword arguments - kwargs.setdefault('format',None) - kwargs.setdefault('date',True) - kwargs.setdefault('sort',True) + kwargs.setdefault('format', None) + kwargs.setdefault('date', True) + kwargs.setdefault('sort', True) # set filename self.case_insensitive_filename(filename) # file parser for reading index files @@ -607,18 +620,18 @@ def from_index(self, filename, **kwargs): # create a list of harmonic objects h = [] # for each file in the index - for i,f in enumerate(file_list): - if (kwargs['format'] == 'ascii'): + for i, f in enumerate(file_list): + if kwargs['format'] == 'ascii': # ascii (.txt) h.append(harmonics().from_ascii(f, date=kwargs['date'])) - elif (kwargs['format'] == 'netCDF4'): + elif kwargs['format'] == 'netCDF4': # netcdf (.nc) h.append(harmonics().from_netCDF4(f, date=kwargs['date'])) - elif (kwargs['format'] == 'HDF5'): + elif kwargs['format'] == 'HDF5': # HDF5 (.H5) h.append(harmonics().from_HDF5(f, date=kwargs['date'])) # create a single harmonic object from the list - return self.from_list(h,date=kwargs['date'],sort=kwargs['sort']) + return self.from_list(h, date=kwargs['date'], sort=kwargs['sort']) def from_list(self, object_list, **kwargs): """ @@ -637,32 +650,36 @@ def from_list(self, object_list, **kwargs): clear the list of ``harmonics`` objects from memory """ # set default keyword arguments - kwargs.setdefault('date',True) - kwargs.setdefault('sort',True) - kwargs.setdefault('clear',False) + kwargs.setdefault('date', True) + kwargs.setdefault('sort', True) + kwargs.setdefault('clear', False) # number of harmonic objects in list n = len(object_list) # indices to sort data objects if harmonics list contain dates if kwargs['date'] and kwargs['sort']: - list_sort = np.argsort([d.time for d in object_list],axis=None) + list_sort = np.argsort([d.time for d in object_list], axis=None) else: list_sort = np.arange(n) # truncate to maximum degree and order self.lmax = np.min([d.lmax for d in object_list]) self.mmax = np.min([d.mmax for d in object_list]) # create output harmonics - self.clm = np.zeros((self.lmax+1,self.mmax+1,n)) - self.slm = np.zeros((self.lmax+1,self.mmax+1,n)) + self.clm = np.zeros((self.lmax + 1, self.mmax + 1, n)) + self.slm = np.zeros((self.lmax + 1, self.mmax + 1, n)) # create list of files self.filename = [] # output dates if kwargs['date']: self.time = np.zeros((n)) - self.month = np.zeros((n),dtype=np.int64) + self.month = np.zeros((n), dtype=np.int64) # for each indice - for t,i in enumerate(list_sort): - self.clm[:,:,t] = object_list[i].clm[:self.lmax+1,:self.mmax+1] - self.slm[:,:,t] = object_list[i].slm[:self.lmax+1,:self.mmax+1] + for t, i in enumerate(list_sort): + self.clm[:, :, t] = object_list[i].clm[ + : self.lmax + 1, : self.mmax + 1 + ] + self.slm[:, :, t] = object_list[i].slm[ + : self.lmax + 1, : self.mmax + 1 + ] if kwargs['date']: self.time[t] = np.atleast_1d(object_list[i].time) self.month[t] = np.atleast_1d(object_list[i].month) @@ -706,21 +723,21 @@ def from_file(self, filename, format=None, date=True, **kwargs): # set filename self.case_insensitive_filename(filename) # set default verbosity - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) # read from file - if (format == 'ascii'): + if format == 'ascii': # ascii (.txt) return harmonics().from_ascii(filename, date=date, **kwargs) - elif (format == 'netCDF4'): + elif format == 'netCDF4': # netcdf (.nc) return harmonics().from_netCDF4(filename, date=date, **kwargs) - elif (format == 'HDF5'): + elif format == 'HDF5': # HDF5 (.H5) return harmonics().from_HDF5(filename, date=date, **kwargs) - elif (format == 'gfc'): + elif format == 'gfc': # ICGEM gravity model (.gfc) return harmonics().from_gfc(filename, **kwargs) - elif (format == 'SHM'): + elif format == 'SHM': # spherical harmonic model return harmonics().from_SHM(filename, self.lmax, **kwargs) @@ -734,7 +751,7 @@ def from_dict(self, d, **kwargs): dictionary object to be converted """ # assign dictionary variables to self - for key in ['l','m','clm','slm','time','month']: + for key in ['l', 'm', 'clm', 'slm', 'time', 'month']: try: setattr(self, key, d[key].copy()) except (AttributeError, KeyError): @@ -763,7 +780,7 @@ def to_ascii(self, filename, date=True, **kwargs): """ self.filename = pathlib.Path(filename).expanduser().absolute() # set default verbosity - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) logging.info(self.filename) # open the output file fid = open(self.filename, mode='w', encoding='utf8') @@ -772,9 +789,9 @@ def to_ascii(self, filename, date=True, **kwargs): else: file_format = '{0:5d} {1:5d} {2:+21.12e} {3:+21.12e}' # write to file for each spherical harmonic degree and order - for m in range(0, self.mmax+1): - for l in range(m, self.lmax+1): - args = (l, m, self.clm[l,m], self.slm[l,m], self.time) + for m in range(0, self.mmax + 1): + for l in range(m, self.lmax + 1): + args = (l, m, self.clm[l, m], self.slm[l, m], self.time) print(file_format.format(*args), file=fid) # close the output file fid.close() @@ -817,26 +834,26 @@ def to_netCDF4(self, filename, **kwargs): Output file and variable information """ # set default keyword arguments - kwargs.setdefault('units','Geodesy_Normalization') - kwargs.setdefault('time_units','years') - kwargs.setdefault('time_longname','Date_in_Decimal_Years') - kwargs.setdefault('months_name','month') - kwargs.setdefault('months_units','number') - kwargs.setdefault('months_longname','GRACE_month') - kwargs.setdefault('field_mapping',{}) + kwargs.setdefault('units', 'Geodesy_Normalization') + kwargs.setdefault('time_units', 'years') + kwargs.setdefault('time_longname', 'Date_in_Decimal_Years') + kwargs.setdefault('months_name', 'month') + kwargs.setdefault('months_units', 'number') + kwargs.setdefault('months_longname', 'GRACE_month') + kwargs.setdefault('field_mapping', {}) attributes = self.attributes.get('ROOT') or {} - kwargs.setdefault('attributes',dict(ROOT=attributes)) - kwargs.setdefault('title',None) - kwargs.setdefault('source',None) - kwargs.setdefault('reference',None) - kwargs.setdefault('date',True) - kwargs.setdefault('clobber',True) - kwargs.setdefault('verbose',False) + kwargs.setdefault('attributes', dict(ROOT=attributes)) + kwargs.setdefault('title', None) + kwargs.setdefault('source', None) + kwargs.setdefault('reference', None) + kwargs.setdefault('date', True) + kwargs.setdefault('clobber', True) + kwargs.setdefault('verbose', False) # setting NetCDF clobber attribute clobber = 'w' if kwargs['clobber'] else 'a' # opening netCDF file for writing self.filename = pathlib.Path(filename).expanduser().absolute() - fileID = netCDF4.Dataset(self.filename, clobber, format="NETCDF4") + fileID = netCDF4.Dataset(self.filename, clobber, format='NETCDF4') # flatten harmonics temp = self.flatten(date=kwargs['date']) # mapping between output keys and netCDF4 variable names @@ -849,30 +866,56 @@ def to_netCDF4(self, filename, **kwargs): kwargs['field_mapping']['time'] = 'time' kwargs['field_mapping']['month'] = kwargs['months_name'] # create attributes dictionary for output variables - if not all(key in kwargs['attributes'] for key in kwargs['field_mapping']): + if not all( + key in kwargs['attributes'] for key in kwargs['field_mapping'] + ): # Defining attributes for degree and order kwargs['attributes'][kwargs['field_mapping']['l']] = {} - kwargs['attributes'][kwargs['field_mapping']['l']]['long_name'] = 'spherical_harmonic_degree' - kwargs['attributes'][kwargs['field_mapping']['l']]['units'] = 'Wavenumber' + kwargs['attributes'][kwargs['field_mapping']['l']]['long_name'] = ( + 'spherical_harmonic_degree' + ) + kwargs['attributes'][kwargs['field_mapping']['l']]['units'] = ( + 'Wavenumber' + ) kwargs['attributes'][kwargs['field_mapping']['m']] = {} - kwargs['attributes'][kwargs['field_mapping']['m']]['long_name'] = 'spherical_harmonic_order' - kwargs['attributes'][kwargs['field_mapping']['m']]['units'] = 'Wavenumber' + kwargs['attributes'][kwargs['field_mapping']['m']]['long_name'] = ( + 'spherical_harmonic_order' + ) + kwargs['attributes'][kwargs['field_mapping']['m']]['units'] = ( + 'Wavenumber' + ) # Defining attributes for dataset kwargs['attributes'][kwargs['field_mapping']['clm']] = {} - kwargs['attributes'][kwargs['field_mapping']['clm']]['long_name'] = 'cosine_spherical_harmonics' - kwargs['attributes'][kwargs['field_mapping']['clm']]['units'] = kwargs['units'] + kwargs['attributes'][kwargs['field_mapping']['clm']][ + 'long_name' + ] = 'cosine_spherical_harmonics' + kwargs['attributes'][kwargs['field_mapping']['clm']]['units'] = ( + kwargs['units'] + ) kwargs['attributes'][kwargs['field_mapping']['slm']] = {} - kwargs['attributes'][kwargs['field_mapping']['slm']]['long_name'] = 'sine_spherical_harmonics' - kwargs['attributes'][kwargs['field_mapping']['slm']]['units'] = kwargs['units'] + kwargs['attributes'][kwargs['field_mapping']['slm']][ + 'long_name' + ] = 'sine_spherical_harmonics' + kwargs['attributes'][kwargs['field_mapping']['slm']]['units'] = ( + kwargs['units'] + ) # Defining attributes for date if applicable if kwargs['date']: # attributes for date and month (or integer date) kwargs['attributes'][kwargs['field_mapping']['time']] = {} - kwargs['attributes'][kwargs['field_mapping']['time']]['long_name'] = kwargs['time_longname'] - kwargs['attributes'][kwargs['field_mapping']['time']]['units'] = kwargs['time_units'] + kwargs['attributes'][kwargs['field_mapping']['time']][ + 'long_name' + ] = kwargs['time_longname'] + kwargs['attributes'][kwargs['field_mapping']['time']][ + 'units' + ] = kwargs['time_units'] kwargs['attributes'][kwargs['field_mapping']['month']] = {} - kwargs['attributes'][kwargs['field_mapping']['month']]['long_name'] = kwargs['months_longname'] - kwargs['attributes'][kwargs['field_mapping']['month']]['units'] = kwargs['months_units'] + kwargs['attributes'][kwargs['field_mapping']['month']][ + 'long_name' + ] = kwargs['months_longname'] + kwargs['attributes'][kwargs['field_mapping']['month']][ + 'units' + ] = kwargs['months_units'] # add default global (file-level) attributes if kwargs['title']: kwargs['attributes']['ROOT']['title'] = kwargs['title'] @@ -891,11 +934,11 @@ def to_netCDF4(self, filename, **kwargs): fileID.createDimension('time', len(temp.time)) # defining and filling the netCDF variables nc = {} - for field,key in kwargs['field_mapping'].items(): + for field, key in kwargs['field_mapping'].items(): val = getattr(temp, field) - if field in ('l','m'): + if field in ('l', 'm'): dims = (dimensions[0],) - elif field in ('time','month'): + elif field in ('time', 'month'): dims = (dimensions[1],) else: dims = tuple(dimensions) @@ -903,21 +946,21 @@ def to_netCDF4(self, filename, **kwargs): nc[key] = fileID.createVariable(key, val.dtype, dims) nc[key][:] = val[:] # filling netCDF dataset attributes - for att_name,att_val in kwargs['attributes'][key].items(): + for att_name, att_val in kwargs['attributes'][key].items(): # skip variable attribute if None if not att_val: continue # skip variable attributes if in list - if att_name not in ('DIMENSION_LIST','CLASS','NAME'): + if att_name not in ('DIMENSION_LIST', 'CLASS', 'NAME'): nc[key].setncattr(att_name, att_val) # global attributes of NetCDF4 file - for att_name,att_val in kwargs['attributes']['ROOT'].items(): + for att_name, att_val in kwargs['attributes']['ROOT'].items(): fileID.setncattr(att_name, att_val) # add software information fileID.software_reference = gravity_toolkit.version.project_name fileID.software_version = gravity_toolkit.version.full_version # date created - fileID.date_created = time.strftime('%Y-%m-%d',time.localtime()) + fileID.date_created = time.strftime('%Y-%m-%d', time.localtime()) # Output netCDF structure information logging.info(self.filename) logging.info(list(fileID.variables.keys())) @@ -962,21 +1005,21 @@ def to_HDF5(self, filename, **kwargs): Output file and variable information """ # set default keyword arguments - kwargs.setdefault('units','Geodesy_Normalization') - kwargs.setdefault('time_units','years') - kwargs.setdefault('time_longname','Date_in_Decimal_Years') - kwargs.setdefault('months_name','month') - kwargs.setdefault('months_units','number') - kwargs.setdefault('months_longname','GRACE_month') - kwargs.setdefault('field_mapping',{}) + kwargs.setdefault('units', 'Geodesy_Normalization') + kwargs.setdefault('time_units', 'years') + kwargs.setdefault('time_longname', 'Date_in_Decimal_Years') + kwargs.setdefault('months_name', 'month') + kwargs.setdefault('months_units', 'number') + kwargs.setdefault('months_longname', 'GRACE_month') + kwargs.setdefault('field_mapping', {}) attributes = self.attributes.get('ROOT') or {} - kwargs.setdefault('attributes',dict(ROOT=attributes)) - kwargs.setdefault('title',None) - kwargs.setdefault('source',None) - kwargs.setdefault('reference',None) - kwargs.setdefault('date',True) - kwargs.setdefault('clobber',True) - kwargs.setdefault('verbose',False) + kwargs.setdefault('attributes', dict(ROOT=attributes)) + kwargs.setdefault('title', None) + kwargs.setdefault('source', None) + kwargs.setdefault('reference', None) + kwargs.setdefault('date', True) + kwargs.setdefault('clobber', True) + kwargs.setdefault('verbose', False) # setting HDF5 clobber attribute clobber = 'w' if kwargs['clobber'] else 'w-' # opening HDF5 file for writing @@ -996,30 +1039,56 @@ def to_HDF5(self, filename, **kwargs): kwargs['field_mapping']['time'] = 'time' kwargs['field_mapping']['month'] = kwargs['months_name'] # create attributes dictionary for output variables - if not all(key in kwargs['attributes'] for key in kwargs['field_mapping']): + if not all( + key in kwargs['attributes'] for key in kwargs['field_mapping'] + ): # Defining attributes for degree and order kwargs['attributes'][kwargs['field_mapping']['l']] = {} - kwargs['attributes'][kwargs['field_mapping']['l']]['long_name'] = 'spherical_harmonic_degree' - kwargs['attributes'][kwargs['field_mapping']['l']]['units'] = 'Wavenumber' + kwargs['attributes'][kwargs['field_mapping']['l']]['long_name'] = ( + 'spherical_harmonic_degree' + ) + kwargs['attributes'][kwargs['field_mapping']['l']]['units'] = ( + 'Wavenumber' + ) kwargs['attributes'][kwargs['field_mapping']['m']] = {} - kwargs['attributes'][kwargs['field_mapping']['m']]['long_name'] = 'spherical_harmonic_order' - kwargs['attributes'][kwargs['field_mapping']['m']]['units'] = 'Wavenumber' + kwargs['attributes'][kwargs['field_mapping']['m']]['long_name'] = ( + 'spherical_harmonic_order' + ) + kwargs['attributes'][kwargs['field_mapping']['m']]['units'] = ( + 'Wavenumber' + ) # Defining attributes for dataset kwargs['attributes'][kwargs['field_mapping']['clm']] = {} - kwargs['attributes'][kwargs['field_mapping']['clm']]['long_name'] = 'cosine_spherical_harmonics' - kwargs['attributes'][kwargs['field_mapping']['clm']]['units'] = kwargs['units'] + kwargs['attributes'][kwargs['field_mapping']['clm']][ + 'long_name' + ] = 'cosine_spherical_harmonics' + kwargs['attributes'][kwargs['field_mapping']['clm']]['units'] = ( + kwargs['units'] + ) kwargs['attributes'][kwargs['field_mapping']['slm']] = {} - kwargs['attributes'][kwargs['field_mapping']['slm']]['long_name'] = 'sine_spherical_harmonics' - kwargs['attributes'][kwargs['field_mapping']['slm']]['units'] = kwargs['units'] + kwargs['attributes'][kwargs['field_mapping']['slm']][ + 'long_name' + ] = 'sine_spherical_harmonics' + kwargs['attributes'][kwargs['field_mapping']['slm']]['units'] = ( + kwargs['units'] + ) # Defining attributes for date if applicable if kwargs['date']: # attributes for date and month (or integer date) kwargs['attributes'][kwargs['field_mapping']['time']] = {} - kwargs['attributes'][kwargs['field_mapping']['time']]['long_name'] = kwargs['time_longname'] - kwargs['attributes'][kwargs['field_mapping']['time']]['units'] = kwargs['time_units'] + kwargs['attributes'][kwargs['field_mapping']['time']][ + 'long_name' + ] = kwargs['time_longname'] + kwargs['attributes'][kwargs['field_mapping']['time']][ + 'units' + ] = kwargs['time_units'] kwargs['attributes'][kwargs['field_mapping']['month']] = {} - kwargs['attributes'][kwargs['field_mapping']['month']]['long_name'] = kwargs['months_longname'] - kwargs['attributes'][kwargs['field_mapping']['month']]['units'] = kwargs['months_units'] + kwargs['attributes'][kwargs['field_mapping']['month']][ + 'long_name' + ] = kwargs['months_longname'] + kwargs['attributes'][kwargs['field_mapping']['month']][ + 'units' + ] = kwargs['months_units'] # add default global (file-level) attributes if kwargs['title']: kwargs['attributes']['ROOT']['title'] = kwargs['title'] @@ -1029,26 +1098,31 @@ def to_HDF5(self, filename, **kwargs): kwargs['attributes']['ROOT']['reference'] = kwargs['reference'] # Defining the HDF5 dataset variables h5 = {} - for field,key in kwargs['field_mapping'].items(): + for field, key in kwargs['field_mapping'].items(): val = getattr(temp, field) - h5[key] = fileID.create_dataset(key, val.shape, - data=val, dtype=val.dtype, compression='gzip') + h5[key] = fileID.create_dataset( + key, val.shape, data=val, dtype=val.dtype, compression='gzip' + ) # filling HDF5 dataset attributes - for att_name,att_val in kwargs['attributes'][key].items(): + for att_name, att_val in kwargs['attributes'][key].items(): # skip variable attribute if None if not att_val: continue # skip variable attributes if in list - if att_name not in ('DIMENSION_LIST','CLASS','NAME'): + if att_name not in ('DIMENSION_LIST', 'CLASS', 'NAME'): h5[key].attrs[att_name] = att_val # global attributes of HDF5 file - for att_name,att_val in kwargs['attributes']['ROOT'].items(): + for att_name, att_val in kwargs['attributes']['ROOT'].items(): fileID.attrs[att_name] = att_val # add software information - fileID.attrs['software_reference'] = gravity_toolkit.version.project_name + fileID.attrs['software_reference'] = ( + gravity_toolkit.version.project_name + ) fileID.attrs['software_version'] = gravity_toolkit.version.full_version # date created - fileID.attrs['date_created'] = time.strftime('%Y-%m-%d',time.localtime()) + fileID.attrs['date_created'] = time.strftime( + '%Y-%m-%d', time.localtime() + ) # Output HDF5 structure information logging.info(self.filename) logging.info(list(fileID.keys())) @@ -1082,21 +1156,21 @@ def to_index(self, filename, file_list, format=None, date=True, **kwargs): self.filename = pathlib.Path(filename).expanduser().absolute() fid = open(self.filename, mode='w', encoding='utf8') # set default verbosity - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) # for each file to be in the index - for i,f in enumerate(file_list): + for i, f in enumerate(file_list): # print filename to index print(self.compressuser(f), file=fid) # index harmonics object at i h = self.index(i, date=date) # write to file - if (format == 'ascii'): + if format == 'ascii': # ascii (.txt) h.to_ascii(f, date=date, **kwargs) - elif (format == 'netCDF4'): + elif format == 'netCDF4': # netcdf (.nc) h.to_netCDF4(f, date=date, **kwargs) - elif (format == 'HDF5'): + elif format == 'HDF5': # HDF5 (.H5) h.to_HDF5(f, date=date, **kwargs) # close the index file @@ -1124,15 +1198,15 @@ def to_file(self, filename, format=None, date=True, **kwargs): keyword arguments for output writers """ # set default verbosity - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) # write to file - if (format == 'ascii'): + if format == 'ascii': # ascii (.txt) self.to_ascii(filename, date=date, **kwargs) - elif (format == 'netCDF4'): + elif format == 'netCDF4': # netcdf (.nc) self.to_netCDF4(filename, date=date, **kwargs) - elif (format == 'HDF5'): + elif format == 'HDF5': # HDF5 (.H5) self.to_HDF5(filename, date=date, **kwargs) @@ -1147,7 +1221,7 @@ def to_dict(self): """ # assign dictionary variables from self d = {} - for key in ['l','m','clm','slm','time','month','attributes']: + for key in ['l', 'm', 'clm', 'slm', 'time', 'month', 'attributes']: try: d[key] = getattr(self, key) except (AttributeError, KeyError): @@ -1164,21 +1238,21 @@ def to_masked_array(self): # verify dimensions and get shape ndim_prev = np.copy(self.ndim) self.expand_dims() - l1,m1,nt = self.shape + l1, m1, nt = self.shape # create single triangular matrices with harmonics - Ylms = np.ma.zeros((self.lmax+1,2*self.lmax+1,nt)) - Ylms.mask = np.ones((self.lmax+1,2*self.lmax+1,nt),dtype=bool) - for m in range(-self.mmax,self.mmax+1): + Ylms = np.ma.zeros((self.lmax + 1, 2 * self.lmax + 1, nt)) + Ylms.mask = np.ones((self.lmax + 1, 2 * self.lmax + 1, nt), dtype=bool) + for m in range(-self.mmax, self.mmax + 1): mm = np.abs(m) - for l in range(mm,self.lmax+1): - if (m < 0): - Ylms.data[l,self.lmax+m,:] = self.slm[l,mm,:] - Ylms.mask[l,self.lmax+m,:] = False + for l in range(mm, self.lmax + 1): + if m < 0: + Ylms.data[l, self.lmax + m, :] = self.slm[l, mm, :] + Ylms.mask[l, self.lmax + m, :] = False else: - Ylms.data[l,self.lmax+m,:] = self.clm[l,mm,:] - Ylms.mask[l,self.lmax+m,:] = False + Ylms.data[l, self.lmax + m, :] = self.clm[l, mm, :] + Ylms.mask[l, self.lmax + m, :] = False # reshape to previous - if (self.ndim != ndim_prev): + if self.ndim != ndim_prev: self.squeeze() # return the triangular matrix return Ylms @@ -1200,8 +1274,8 @@ def update_dimensions(self): Update the dimension variables of the ``harmonics`` object """ # calculate spherical harmonic degree and order (0 is falsy) - self.l=np.arange(self.lmax+1) if (self.lmax is not None) else None - self.m=np.arange(self.mmax+1) if (self.mmax is not None) else None + self.l = np.arange(self.lmax + 1) if (self.lmax is not None) else None + self.m = np.arange(self.mmax + 1) if (self.mmax is not None) else None return self def add(self, temp): @@ -1216,18 +1290,18 @@ def add(self, temp): # assign degree and order fields self.update_dimensions() temp.update_dimensions() - l1 = self.lmax+1 if (temp.lmax > self.lmax) else temp.lmax+1 - m1 = self.mmax+1 if (temp.mmax > self.mmax) else temp.mmax+1 - if (self.ndim == 2): - self.clm[:l1,:m1] += temp.clm[:l1,:m1] - self.slm[:l1,:m1] += temp.slm[:l1,:m1] + l1 = self.lmax + 1 if (temp.lmax > self.lmax) else temp.lmax + 1 + m1 = self.mmax + 1 if (temp.mmax > self.mmax) else temp.mmax + 1 + if self.ndim == 2: + self.clm[:l1, :m1] += temp.clm[:l1, :m1] + self.slm[:l1, :m1] += temp.slm[:l1, :m1] elif (self.ndim == 3) and (temp.ndim == 2): - for i,t in enumerate(self.time): - self.clm[:l1,:m1,i] += temp.clm[:l1,:m1] - self.slm[:l1,:m1,i] += temp.slm[:l1,:m1] + for i, t in enumerate(self.time): + self.clm[:l1, :m1, i] += temp.clm[:l1, :m1] + self.slm[:l1, :m1, i] += temp.slm[:l1, :m1] else: - self.clm[:l1,:m1,:] += temp.clm[:l1,:m1,:] - self.slm[:l1,:m1,:] += temp.slm[:l1,:m1,:] + self.clm[:l1, :m1, :] += temp.clm[:l1, :m1, :] + self.slm[:l1, :m1, :] += temp.slm[:l1, :m1, :] return self def subtract(self, temp): @@ -1242,18 +1316,18 @@ def subtract(self, temp): # assign degree and order fields self.update_dimensions() temp.update_dimensions() - l1 = self.lmax+1 if (temp.lmax > self.lmax) else temp.lmax+1 - m1 = self.mmax+1 if (temp.mmax > self.mmax) else temp.mmax+1 - if (self.ndim == 2): - self.clm[:l1,:m1] -= temp.clm[:l1,:m1] - self.slm[:l1,:m1] -= temp.slm[:l1,:m1] + l1 = self.lmax + 1 if (temp.lmax > self.lmax) else temp.lmax + 1 + m1 = self.mmax + 1 if (temp.mmax > self.mmax) else temp.mmax + 1 + if self.ndim == 2: + self.clm[:l1, :m1] -= temp.clm[:l1, :m1] + self.slm[:l1, :m1] -= temp.slm[:l1, :m1] elif (self.ndim == 3) and (temp.ndim == 2): - for i,t in enumerate(self.time): - self.clm[:l1,:m1,i] -= temp.clm[:l1,:m1] - self.slm[:l1,:m1,i] -= temp.slm[:l1,:m1] + for i, t in enumerate(self.time): + self.clm[:l1, :m1, i] -= temp.clm[:l1, :m1] + self.slm[:l1, :m1, i] -= temp.slm[:l1, :m1] else: - self.clm[:l1,:m1,:] -= temp.clm[:l1,:m1,:] - self.slm[:l1,:m1,:] -= temp.slm[:l1,:m1,:] + self.clm[:l1, :m1, :] -= temp.clm[:l1, :m1, :] + self.slm[:l1, :m1, :] -= temp.slm[:l1, :m1, :] return self def multiply(self, temp): @@ -1268,18 +1342,18 @@ def multiply(self, temp): # assign degree and order fields self.update_dimensions() temp.update_dimensions() - l1 = self.lmax+1 if (temp.lmax > self.lmax) else temp.lmax+1 - m1 = self.mmax+1 if (temp.mmax > self.mmax) else temp.mmax+1 - if (self.ndim == 2): - self.clm[:l1,:m1] *= temp.clm[:l1,:m1] - self.slm[:l1,:m1] *= temp.slm[:l1,:m1] + l1 = self.lmax + 1 if (temp.lmax > self.lmax) else temp.lmax + 1 + m1 = self.mmax + 1 if (temp.mmax > self.mmax) else temp.mmax + 1 + if self.ndim == 2: + self.clm[:l1, :m1] *= temp.clm[:l1, :m1] + self.slm[:l1, :m1] *= temp.slm[:l1, :m1] elif (self.ndim == 3) and (temp.ndim == 2): - for i,t in enumerate(self.time): - self.clm[:l1,:m1,i] *= temp.clm[:l1,:m1] - self.slm[:l1,:m1,i] *= temp.slm[:l1,:m1] + for i, t in enumerate(self.time): + self.clm[:l1, :m1, i] *= temp.clm[:l1, :m1] + self.slm[:l1, :m1, i] *= temp.slm[:l1, :m1] else: - self.clm[:l1,:m1,:] *= temp.clm[:l1,:m1,:] - self.slm[:l1,:m1,:] *= temp.slm[:l1,:m1,:] + self.clm[:l1, :m1, :] *= temp.clm[:l1, :m1, :] + self.slm[:l1, :m1, :] *= temp.slm[:l1, :m1, :] return self def divide(self, temp): @@ -1294,23 +1368,23 @@ def divide(self, temp): # assign degree and order fields self.update_dimensions() temp.update_dimensions() - l1 = self.lmax+1 if (temp.lmax > self.lmax) else temp.lmax+1 - m1 = self.mmax+1 if (temp.mmax > self.mmax) else temp.mmax+1 + l1 = self.lmax + 1 if (temp.lmax > self.lmax) else temp.lmax + 1 + m1 = self.mmax + 1 if (temp.mmax > self.mmax) else temp.mmax + 1 # indices for cosine spherical harmonics (including zonals) - lc,mc = np.tril_indices(l1, m=m1) + lc, mc = np.tril_indices(l1, m=m1) # indices for sine spherical harmonics (excluding zonals) m0 = np.nonzero(mc != 0) - ls,ms = (lc[m0],mc[m0]) - if (self.ndim == 2): - self.clm[lc,mc] /= temp.clm[lc,mc] - self.slm[ls,ms] /= temp.slm[ls,ms] + ls, ms = (lc[m0], mc[m0]) + if self.ndim == 2: + self.clm[lc, mc] /= temp.clm[lc, mc] + self.slm[ls, ms] /= temp.slm[ls, ms] elif (self.ndim == 3) and (temp.ndim == 2): - for i,t in enumerate(self.time): - self.clm[lc,mc,i] /= temp.clm[lc,mc] - self.slm[ls,ms,i] /= temp.slm[ls,ms] + for i, t in enumerate(self.time): + self.clm[lc, mc, i] /= temp.clm[lc, mc] + self.slm[ls, ms, i] /= temp.slm[ls, ms] else: - self.clm[lc,mc,:] /= temp.clm[lc,mc,:] - self.slm[ls,ms,:] /= temp.slm[ls,ms,:] + self.clm[lc, mc, :] /= temp.clm[lc, mc, :] + self.slm[ls, ms, :] /= temp.slm[ls, ms, :] return self def copy(self): @@ -1320,11 +1394,11 @@ def copy(self): temp = harmonics(lmax=self.lmax, mmax=self.mmax) # copy attributes or update attributes dictionary if isinstance(self.attributes, list): - setattr(temp,'attributes',self.attributes) + setattr(temp, 'attributes', self.attributes) elif isinstance(self.attributes, dict): temp.attributes.update(self.attributes) # try to assign variables to self - for key in ['clm','slm','time','month','filename']: + for key in ['clm', 'slm', 'time', 'month', 'filename']: try: val = getattr(self, key) setattr(temp, key, copy.copy(val)) @@ -1348,13 +1422,13 @@ def zeros(self, lmax=None, mmax=None, nt=None): self.mmax = np.copy(self.lmax) # assign variables to self if nt is not None: - self.clm = np.zeros((self.lmax+1, self.mmax+1, nt)) - self.slm = np.zeros((self.lmax+1, self.mmax+1, nt)) + self.clm = np.zeros((self.lmax + 1, self.mmax + 1, nt)) + self.slm = np.zeros((self.lmax + 1, self.mmax + 1, nt)) self.time = np.zeros((nt)) self.month = np.zeros((nt), dtype=int) else: - self.clm = np.zeros((self.lmax+1, self.mmax+1)) - self.slm = np.zeros((self.lmax+1, self.mmax+1)) + self.clm = np.zeros((self.lmax + 1, self.mmax + 1)) + self.slm = np.zeros((self.lmax + 1, self.mmax + 1)) # assign degree and order fields self.update_dimensions() return self @@ -1365,7 +1439,7 @@ def zeros_like(self): """ temp = harmonics(lmax=self.lmax, mmax=self.mmax) # assign variables to temp - for key in ['clm','slm','time','month']: + for key in ['clm', 'slm', 'time', 'month']: try: val = getattr(self, key) setattr(temp, key, np.zeros_like(val)) @@ -1389,11 +1463,11 @@ def expand_dims(self, update_dimensions=True): self.month = np.atleast_1d(self.month) # output harmonics with a third dimension if (self.ndim == 2) and not self.flattened: - self.clm = self.clm[:,:,None] - self.slm = self.slm[:,:,None] + self.clm = self.clm[:, :, None] + self.slm = self.slm[:, :, None] elif (self.ndim == 1) and self.flattened: - self.clm = self.clm[:,None] - self.slm = self.slm[:,None] + self.clm = self.clm[:, None] + self.slm = self.slm[:, None] # assign degree and order fields if update_dimensions: self.update_dimensions() @@ -1434,8 +1508,12 @@ def flatten(self, date=True): date: bool, default True ``harmonics`` objects contain date information """ - n_harm = (self.lmax**2 + 3*self.lmax - (self.lmax-self.mmax)**2 - - (self.lmax-self.mmax))//2 + 1 + n_harm = ( + self.lmax**2 + + 3 * self.lmax + - (self.lmax - self.mmax) ** 2 + - (self.lmax - self.mmax) + ) // 2 + 1 # restructured degree and order temp = harmonics(lmax=self.lmax, mmax=self.mmax) temp.l = np.zeros((n_harm,), dtype=np.int64) @@ -1448,25 +1526,25 @@ def flatten(self, date=True): temp.time = np.copy(self.time) temp.month = np.copy(self.month) # restructured spherical harmonic arrays - if (self.clm.ndim == 2): + if self.clm.ndim == 2: temp.clm = np.zeros((n_harm)) temp.slm = np.zeros((n_harm)) else: n = self.clm.shape[-1] - temp.clm = np.zeros((n_harm,n)) - temp.slm = np.zeros((n_harm,n)) + temp.clm = np.zeros((n_harm, n)) + temp.slm = np.zeros((n_harm, n)) # create counter variable lm lm = 0 - for m in range(0,self.mmax+1):# MMAX+1 to include MMAX - for l in range(m,self.lmax+1):# LMAX+1 to include LMAX + for m in range(0, self.mmax + 1): # MMAX+1 to include MMAX + for l in range(m, self.lmax + 1): # LMAX+1 to include LMAX temp.l[lm] = np.int64(l) temp.m[lm] = np.int64(m) - if (self.clm.ndim == 2): - temp.clm[lm] = self.clm[l,m] - temp.slm[lm] = self.slm[l,m] + if self.clm.ndim == 2: + temp.clm[lm] = self.clm[l, m] + temp.slm[lm] = self.slm[l, m] else: - temp.clm[lm,:] = self.clm[l,m,:] - temp.slm[lm,:] = self.slm[l,m,:] + temp.clm[lm, :] = self.clm[l, m, :] + temp.slm[lm, :] = self.slm[l, m, :] # add 1 to lm counter variable lm += 1 # update flattened attribute @@ -1495,23 +1573,23 @@ def expand(self, date=True): temp.time = np.copy(self.time) temp.month = np.copy(self.month) # restructured spherical harmonic matrices - if (self.clm.ndim == 1): - temp.clm = np.zeros((self.lmax+1,self.mmax+1)) - temp.slm = np.zeros((self.lmax+1,self.mmax+1)) + if self.clm.ndim == 1: + temp.clm = np.zeros((self.lmax + 1, self.mmax + 1)) + temp.slm = np.zeros((self.lmax + 1, self.mmax + 1)) else: n = self.clm.shape[-1] - temp.clm = np.zeros((self.lmax+1,self.mmax+1,n)) - temp.slm = np.zeros((self.lmax+1,self.mmax+1,n)) + temp.clm = np.zeros((self.lmax + 1, self.mmax + 1, n)) + temp.slm = np.zeros((self.lmax + 1, self.mmax + 1, n)) # create counter variable lm for lm in range(n_harm): l = self.l[lm] m = self.m[lm] - if (self.clm.ndim == 1): - temp.clm[l,m] = self.clm[lm] - temp.slm[l,m] = self.slm[lm] + if self.clm.ndim == 1: + temp.clm[l, m] = self.clm[lm] + temp.slm[l, m] = self.slm[lm] else: - temp.clm[l,m,:] = self.clm[lm,:] - temp.slm[l,m,:] = self.slm[lm,:] + temp.clm[l, m, :] = self.clm[lm, :] + temp.slm[l, m, :] = self.slm[lm, :] # update flattened attribute temp.flattened = False # assign degree and order fields @@ -1533,8 +1611,8 @@ def index(self, indice, date=True): # output harmonics object temp = harmonics(lmax=np.copy(self.lmax), mmax=np.copy(self.mmax)) # subset output harmonics - temp.clm = self.clm[:,:,indice].copy() - temp.slm = self.slm[:,:,indice].copy() + temp.clm = self.clm[:, :, indice].copy() + temp.slm = self.slm[:, :, indice].copy() # subset output dates if date: temp.time = self.time[indice].copy() @@ -1569,19 +1647,19 @@ def subset(self, months): m = ','.join([f'{m:03d}' for m in months_check]) raise IOError(f'GRACE/GRACE-FO months {m} not Found') # indices to sort data objects - months_list = [i for i,m in enumerate(self.month) if m in months] + months_list = [i for i, m in enumerate(self.month) if m in months] # output harmonics object temp = harmonics(lmax=np.copy(self.lmax), mmax=np.copy(self.mmax)) # create output harmonics - temp.clm = np.zeros((temp.lmax+1,temp.mmax+1,n)) - temp.slm = np.zeros((temp.lmax+1,temp.mmax+1,n)) + temp.clm = np.zeros((temp.lmax + 1, temp.mmax + 1, n)) + temp.slm = np.zeros((temp.lmax + 1, temp.mmax + 1, n)) temp.time = np.zeros((n)) - temp.month = np.zeros((n),dtype=np.int64) + temp.month = np.zeros((n), dtype=np.int64) temp.filename = [] # for each indice - for t,i in enumerate(months_list): - temp.clm[:,:,t] = self.clm[:,:,i].copy() - temp.slm[:,:,t] = self.slm[:,:,i].copy() + for t, i in enumerate(months_list): + temp.clm[:, :, t] = self.clm[:, :, i].copy() + temp.slm[:, :, t] = self.slm[:, :, i].copy() temp.time[t] = self.time[i].copy() temp.month[t] = self.month[i].copy() # subset filenames if applicable @@ -1617,21 +1695,21 @@ def truncate(self, lmax, lmin=0, mmax=None): self.lmax = np.copy(lmax) self.mmax = np.copy(mmax) if mmax else np.copy(lmax) # truncation levels - l1 = self.lmax+1 if (temp.lmax > self.lmax) else temp.lmax+1 - m1 = self.mmax+1 if (temp.mmax > self.mmax) else temp.mmax+1 + l1 = self.lmax + 1 if (temp.lmax > self.lmax) else temp.lmax + 1 + m1 = self.mmax + 1 if (temp.mmax > self.mmax) else temp.mmax + 1 # create output harmonics - if (temp.ndim == 3): + if temp.ndim == 3: # number of months n = temp.clm.shape[-1] - self.clm = np.zeros((self.lmax+1,self.mmax+1,n)) - self.slm = np.zeros((self.lmax+1,self.mmax+1,n)) - self.clm[lmin:l1,:m1,:] = temp.clm[lmin:l1,:m1,:].copy() - self.slm[lmin:l1,:m1,:] = temp.slm[lmin:l1,:m1,:].copy() + self.clm = np.zeros((self.lmax + 1, self.mmax + 1, n)) + self.slm = np.zeros((self.lmax + 1, self.mmax + 1, n)) + self.clm[lmin:l1, :m1, :] = temp.clm[lmin:l1, :m1, :].copy() + self.slm[lmin:l1, :m1, :] = temp.slm[lmin:l1, :m1, :].copy() else: - self.clm = np.zeros((self.lmax+1,self.mmax+1)) - self.slm = np.zeros((self.lmax+1,self.mmax+1)) - self.clm[lmin:l1,:m1] = temp.clm[lmin:l1,:m1].copy() - self.slm[lmin:l1,:m1] = temp.slm[lmin:l1,:m1].copy() + self.clm = np.zeros((self.lmax + 1, self.mmax + 1)) + self.slm = np.zeros((self.lmax + 1, self.mmax + 1)) + self.clm[lmin:l1, :m1] = temp.clm[lmin:l1, :m1].copy() + self.slm[lmin:l1, :m1] = temp.slm[lmin:l1, :m1].copy() # assign degree and order fields self.update_dimensions() # return the truncated or expanded harmonics object @@ -1650,21 +1728,21 @@ def mean(self, apply=False, indices=Ellipsis): """ temp = harmonics(lmax=np.copy(self.lmax), mmax=np.copy(self.mmax)) # allocate for mean field - temp.clm = np.zeros((temp.lmax+1,temp.mmax+1)) - temp.slm = np.zeros((temp.lmax+1,temp.mmax+1)) + temp.clm = np.zeros((temp.lmax + 1, temp.mmax + 1)) + temp.slm = np.zeros((temp.lmax + 1, temp.mmax + 1)) # Computes the mean for each spherical harmonic degree and order - for m in range(0,temp.mmax+1):# MMAX+1 to include l - for l in range(m,temp.lmax+1):# LMAX+1 to include LMAX + for m in range(0, temp.mmax + 1): # MMAX+1 to include l + for l in range(m, temp.lmax + 1): # LMAX+1 to include LMAX # calculate mean static field - temp.clm[l,m] = np.mean(self.clm[l,m,indices]) - temp.slm[l,m] = np.mean(self.slm[l,m,indices]) + temp.clm[l, m] = np.mean(self.clm[l, m, indices]) + temp.slm[l, m] = np.mean(self.slm[l, m, indices]) # calculating the time-variable gravity field by removing # the static component of the gravitational field if apply: - self.clm[l,m,:] -= temp.clm[l,m] - self.slm[l,m,:] -= temp.slm[l,m] + self.clm[l, m, :] -= temp.clm[l, m] + self.slm[l, m, :] -= temp.slm[l, m] # calculate mean of temporal variables - for key in ['time','month']: + for key in ['time', 'month']: try: val = getattr(self, key) setattr(temp, key, np.mean(val[indices])) @@ -1693,19 +1771,19 @@ def scale(self, var): if getattr(self, 'filename'): temp.filename = copy.copy(self.filename) # multiply by a single constant or a time-variable scalar - if (np.ndim(var) == 0): - temp.clm = var*self.clm - temp.slm = var*self.slm + if np.ndim(var) == 0: + temp.clm = var * self.clm + temp.slm = var * self.slm elif (np.ndim(var) == 1) and (self.ndim == 2): - temp.clm = np.zeros((temp.lmax+1,temp.mmax+1,len(var))) - temp.slm = np.zeros((temp.lmax+1,temp.mmax+1,len(var))) - for i,v in enumerate(var): - temp.clm[:,:,i] = v*self.clm - temp.slm[:,:,i] = v*self.slm + temp.clm = np.zeros((temp.lmax + 1, temp.mmax + 1, len(var))) + temp.slm = np.zeros((temp.lmax + 1, temp.mmax + 1, len(var))) + for i, v in enumerate(var): + temp.clm[:, :, i] = v * self.clm + temp.slm[:, :, i] = v * self.slm elif (np.ndim(var) == 1) and (self.ndim == 3): - for i,v in enumerate(var): - temp.clm[:,:,i] = v*self.clm[:,:,i] - temp.slm[:,:,i] = v*self.slm[:,:,i] + for i, v in enumerate(var): + temp.clm[:, :, i] = v * self.clm[:, :, i] + temp.slm[:, :, i] = v * self.slm[:, :, i] # assign degree and order fields temp.update_dimensions() return temp @@ -1727,9 +1805,9 @@ def power(self, power): # get filenames if applicable if getattr(self, 'filename'): temp.filename = copy.copy(self.filename) - for key in ['clm','slm']: + for key in ['clm', 'slm']: val = getattr(self, key) - setattr(temp, key, np.power(val,power)) + setattr(temp, key, np.power(val, power)) # assign degree and order fields temp.update_dimensions() return temp @@ -1749,8 +1827,8 @@ def drift(self, t, epoch=2003.3): self.update_dimensions() temp = harmonics(lmax=self.lmax, mmax=self.mmax) # allocate for drift field - temp.clm = np.zeros((temp.lmax+1,temp.mmax+1,len(t))) - temp.slm = np.zeros((temp.lmax+1,temp.mmax+1,len(t))) + temp.clm = np.zeros((temp.lmax + 1, temp.mmax + 1, len(t))) + temp.slm = np.zeros((temp.lmax + 1, temp.mmax + 1, len(t))) # copy time variables and calculate GRACE/GRACE-FO months temp.time = np.copy(t) temp.month = calendar_to_grace(temp.time) @@ -1760,9 +1838,9 @@ def drift(self, t, epoch=2003.3): if getattr(self, 'filename'): temp.filename = copy.copy(self.filename) # calculate drift - for i,ti in enumerate(t): - temp.clm[:,:,i] = self.clm*(ti - epoch) - temp.slm[:,:,i] = self.slm*(ti - epoch) + for i, ti in enumerate(t): + temp.clm[:, :, i] = self.clm * (ti - epoch) + temp.slm[:, :, i] = self.slm * (ti - epoch) # assign degree and order fields temp.update_dimensions() return temp @@ -1779,15 +1857,15 @@ def convolve(self, var): # assign degree and order fields self.update_dimensions() # check if a single field or a temporal field - if (self.ndim == 2): - for l in range(0,self.lmax+1):# LMAX+1 to include LMAX - self.clm[l,:] *= var[l] - self.slm[l,:] *= var[l] + if self.ndim == 2: + for l in range(0, self.lmax + 1): # LMAX+1 to include LMAX + self.clm[l, :] *= var[l] + self.slm[l, :] *= var[l] else: - for i,t in enumerate(self.time): - for l in range(0,self.lmax+1):# LMAX+1 to include LMAX - self.clm[l,:,i] *= var[l] - self.slm[l,:,i] *= var[l] + for i, t in enumerate(self.time): + for l in range(0, self.lmax + 1): # LMAX+1 to include LMAX + self.clm[l, :, i] *= var[l] + self.slm[l, :, i] *= var[l] # return the convolved field return self @@ -1810,20 +1888,32 @@ def destripe(self, **kwargs): if getattr(self, 'filename'): temp.filename = copy.copy(self.filename) # check if a single field or a temporal field - if (self.ndim == 2): - Ylms = destripe_harmonics(self.clm, self.slm, - LMIN=1, LMAX=self.lmax, MMAX=self.mmax, **kwargs) + if self.ndim == 2: + Ylms = destripe_harmonics( + self.clm, + self.slm, + LMIN=1, + LMAX=self.lmax, + MMAX=self.mmax, + **kwargs, + ) temp.clm = Ylms['clm'].copy() temp.slm = Ylms['slm'].copy() else: n = self.shape[-1] - temp.clm = np.zeros((self.lmax+1,self.mmax+1,n)) - temp.slm = np.zeros((self.lmax+1,self.mmax+1,n)) + temp.clm = np.zeros((self.lmax + 1, self.mmax + 1, n)) + temp.slm = np.zeros((self.lmax + 1, self.mmax + 1, n)) for i in range(n): - Ylms = destripe_harmonics(self.clm[:,:,i], self.slm[:,:,i], - LMIN=1, LMAX=self.lmax, MMAX=self.mmax, **kwargs) - temp.clm[:,:,i] = Ylms['clm'].copy() - temp.slm[:,:,i] = Ylms['slm'].copy() + Ylms = destripe_harmonics( + self.clm[:, :, i], + self.slm[:, :, i], + LMIN=1, + LMAX=self.lmax, + MMAX=self.mmax, + **kwargs, + ) + temp.clm[:, :, i] = Ylms['clm'].copy() + temp.slm[:, :, i] = Ylms['slm'].copy() # assign degree and order fields temp.update_dimensions() # return the destriped field @@ -1837,24 +1927,24 @@ def amplitude(self): # temporary matrix for squared harmonics temp = self.power(2) # check if a single field or a temporal field - if (self.ndim == 2): + if self.ndim == 2: # allocate for degree amplitudes - amp = np.zeros((self.lmax+1)) - for l in range(self.lmax+1): + amp = np.zeros((self.lmax + 1)) + for l in range(self.lmax + 1): # truncate at mmax - m = np.arange(0,temp.mmax+1) + m = np.arange(0, temp.mmax + 1) # degree amplitude of spherical harmonic degree - amp[l] = np.sqrt(np.sum(temp.clm[l,m] + temp.slm[l,m])) + amp[l] = np.sqrt(np.sum(temp.clm[l, m] + temp.slm[l, m])) else: # allocate for degree amplitudes n = self.shape[-1] - amp = np.zeros((self.lmax+1,n)) - for l in range(self.lmax+1): + amp = np.zeros((self.lmax + 1, n)) + for l in range(self.lmax + 1): # truncate at mmax - m = np.arange(0,temp.mmax+1) + m = np.arange(0, temp.mmax + 1) # degree amplitude of spherical harmonic degree - var = temp.clm[l,m,:] + temp.slm[l,m,:] - amp[l,:] = np.sqrt(np.sum(var, axis=0)) + var = temp.clm[l, m, :] + temp.slm[l, m, :] + amp[l, :] = np.sqrt(np.sum(var, axis=0)) # return the degree amplitudes return amp @@ -1865,14 +1955,12 @@ def dtype(self): @property def shape(self): - """Dimensions of ``harmonics`` object - """ + """Dimensions of ``harmonics`` object""" return np.shape(self.clm) @property def ndim(self): - """Number of dimensions in ``harmonics`` object - """ + """Number of dimensions in ``harmonics`` object""" return np.ndim(self.clm) @reify @@ -1880,18 +1968,17 @@ def ilm(self): """ Complex form of the spherical harmonics """ - return self.clm - self.slm*1j + return self.clm - self.slm * 1j def __str__(self): - """String representation of the ``harmonics`` object - """ + """String representation of the ``harmonics`` object""" properties = ['gravity_toolkit.harmonics'] - properties.append(f" max_degree: {self.lmax}") + properties.append(f' max_degree: {self.lmax}') if self.mmax and (self.mmax != self.lmax): - properties.append(f" max_order: {self.mmax}") + properties.append(f' max_order: {self.mmax}') if self.month: - properties.append(f" start_month: {min(self.month)}") - properties.append(f" end_month: {max(self.month)}") + properties.append(f' start_month: {min(self.month)}') + properties.append(f' end_month: {max(self.month)}') return '\n'.join(properties) def __add__(self, other): @@ -1940,12 +2027,12 @@ def __mul__(self, other): return temp.scale(other) else: return temp.multiply(other) - + def __pow__(self, other): """Raise values from a ``harmonics`` object to a power""" temp = self.copy() return temp.power(other) - + def __sub__(self, other): """Subtract values from a ``harmonics`` object""" temp = self.copy() @@ -1960,23 +2047,20 @@ def __truediv__(self, other): return temp.divide(other) def __len__(self): - """Number of months - """ + """Number of months""" return len(self.month) if np.any(self.month) else 0 def __iter__(self): - """Iterate over GRACE/GRACE-FO months - """ + """Iterate over GRACE/GRACE-FO months""" self.__index__ = 0 return self def __next__(self): - """Get the next month of data - """ + """Get the next month of data""" temp = harmonics(lmax=np.copy(self.lmax), mmax=np.copy(self.mmax)) try: - temp.clm = self.clm[:,:,self.__index__].copy() - temp.slm = self.slm[:,:,self.__index__].copy() + temp.clm = self.clm[:, :, self.__index__].copy() + temp.slm = self.slm[:, :, self.__index__].copy() except IndexError as exc: raise StopIteration from exc # subset output spatial time and month diff --git a/gravity_toolkit/legendre.py b/gravity_toolkit/legendre.py index af6b192c..12bb92cc 100644 --- a/gravity_toolkit/legendre.py +++ b/gravity_toolkit/legendre.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" legendre.py Written by Tyler Sutterley (03/2023) Computes associated Legendre functions of degree l evaluated for elements x @@ -40,8 +40,10 @@ Updated 03/2019: calculate twocot separately to avoid divide warning Written 08/2016 """ + import numpy as np + def legendre(l, x, NORMALIZE=False): """ Computes associated Legendre functions for a particular degree @@ -71,91 +73,93 @@ def legendre(l, x, NORMALIZE=False): nx = len(x) # for the l = 0 case - if (l == 0): - Pl = np.ones((1,nx), dtype=np.float64) + if l == 0: + Pl = np.ones((1, nx), dtype=np.float64) return Pl # for all other degrees greater than 0 - rootl = np.sqrt(np.arange(0,2*l+1))# +1 to include 2*l + rootl = np.sqrt(np.arange(0, 2 * l + 1)) # +1 to include 2*l # s is sine of colatitude (cosine of latitude) so that 0 <= s <= 1 - s = np.sqrt(1.0 - x**2)# for x=cos(th): s=sin(th) - P = np.zeros((l+3,nx), dtype=np.float64) + s = np.sqrt(1.0 - x**2) # for x=cos(th): s=sin(th) + P = np.zeros((l + 3, nx), dtype=np.float64) # Find values of x,s for which there will be underflow - sn = (-s)**l + sn = (-s) ** l tol = np.sqrt(np.finfo(np.float64).tiny) count = np.count_nonzero((s > 0) & (np.abs(sn) <= tol)) - if (count > 0): - ind, = np.nonzero((s > 0) & (np.abs(sn) <= tol)) + if count > 0: + (ind,) = np.nonzero((s > 0) & (np.abs(sn) <= tol)) # Approximate solution of x*ln(x) = Pl - v = 9.2 - np.log(tol)/(l*s[ind]) - w = 1.0/np.log(v) - m1 = 1+l*s[ind]*v*w*(1.0058+ w*(3.819 - w*12.173)) + v = 9.2 - np.log(tol) / (l * s[ind]) + w = 1.0 / np.log(v) + m1 = 1 + l * s[ind] * v * w * (1.0058 + w * (3.819 - w * 12.173)) m1 = np.where(l < np.floor(m1), l, np.floor(m1)).astype(np.int64) # Column-by-column recursion - for k,mm1 in enumerate(m1): + for k, mm1 in enumerate(m1): col = ind[k] # Calculate two*cotangent for underflow case - twocot = -2.0*x[col]/s[col] - P[mm1-1:l+1,col] = 0.0 + twocot = -2.0 * x[col] / s[col] + P[mm1 - 1 : l + 1, col] = 0.0 # Start recursion with proper sign tstart = np.finfo(np.float64).eps - P[mm1-1,col] = np.sign(np.fmod(mm1,2)-0.5)*tstart - if (x[col] < 0): - P[mm1-1,col] = np.sign(np.fmod(l+1,2)-0.5)*tstart + P[mm1 - 1, col] = np.sign(np.fmod(mm1, 2) - 0.5) * tstart + if x[col] < 0: + P[mm1 - 1, col] = np.sign(np.fmod(l + 1, 2) - 0.5) * tstart # Recur from m1 to m = 0, accumulating normalizing factor. sumsq = tol.copy() - for m in range(mm1-2,-1,-1): - P[m,col] = ((m+1)*twocot*P[m+1,col] - \ - rootl[l+m+2]*rootl[l-m-1]*P[m+2,col]) / \ - (rootl[l+m+1]*rootl[l-m]) - sumsq += P[m,col]**2 + for m in range(mm1 - 2, -1, -1): + P[m, col] = ( + (m + 1) * twocot * P[m + 1, col] + - rootl[l + m + 2] * rootl[l - m - 1] * P[m + 2, col] + ) / (rootl[l + m + 1] * rootl[l - m]) + sumsq += P[m, col] ** 2 # calculate scale - scale = 1.0/np.sqrt(2.0*sumsq - P[0,col]**2) - P[0:mm1+1,col] = scale*P[0:mm1+1,col] + scale = 1.0 / np.sqrt(2.0 * sumsq - P[0, col] ** 2) + P[0 : mm1 + 1, col] = scale * P[0 : mm1 + 1, col] # Find the values of x,s for which there is no underflow, and (x != +/-1) count = np.count_nonzero((x != 1) & (np.abs(sn) >= tol)) - if (count > 0): - nind, = np.nonzero((x != 1) & (np.abs(sn) >= tol)) + if count > 0: + (nind,) = np.nonzero((x != 1) & (np.abs(sn) >= tol)) # Calculate two*cotangent for normal case - twocot = -2.0*x[nind]/s[nind] + twocot = -2.0 * x[nind] / s[nind] # Produce normalization constant for the m = l function - d = np.arange(2,2*l+2,2) - c = np.prod(1.0 - 1.0/d) + d = np.arange(2, 2 * l + 2, 2) + c = np.prod(1.0 - 1.0 / d) # Use sn = (-s)**l (written above) to write the m = l function - P[l,nind] = np.sqrt(c)*sn[nind] - P[l-1,nind] = P[l,nind]*twocot*l/rootl[-1] + P[l, nind] = np.sqrt(c) * sn[nind] + P[l - 1, nind] = P[l, nind] * twocot * l / rootl[-1] # Recur downwards to m = 0 - for m in range(l-2,-1,-1): - P[m,nind] = (P[m+1,nind]*twocot*(m+1) - \ - P[m+2,nind]*rootl[l+m+2]*rootl[l-m-1]) / \ - (rootl[l+m+1]*rootl[l-m]) + for m in range(l - 2, -1, -1): + P[m, nind] = ( + P[m + 1, nind] * twocot * (m + 1) + - P[m + 2, nind] * rootl[l + m + 2] * rootl[l - m - 1] + ) / (rootl[l + m + 1] * rootl[l - m]) # calculate Pl from P - Pl = np.copy(P[0:l+1,:]) + Pl = np.copy(P[0 : l + 1, :]) # Polar argument (x == +/-1) count = np.count_nonzero(s == 0) - if (count > 0): - s0, = np.nonzero(s == 0) - Pl[0,s0] = x[s0]**l + if count > 0: + (s0,) = np.nonzero(s == 0) + Pl[0, s0] = x[s0] ** l # calculate Fully Normalized Associated Legendre functions if NORMALIZE: - norm = np.zeros((l+1)) - norm[0] = np.sqrt(2.0*l+1) - m = np.arange(1,l+1) - norm[1:] = (-1)**m*np.sqrt(2.0*(2.0*l+1.0)) - Pl *= np.kron(np.ones((1,nx)), norm[:,np.newaxis]) + norm = np.zeros((l + 1)) + norm[0] = np.sqrt(2.0 * l + 1) + m = np.arange(1, l + 1) + norm[1:] = (-1) ** m * np.sqrt(2.0 * (2.0 * l + 1.0)) + Pl *= np.kron(np.ones((1, nx)), norm[:, np.newaxis]) else: # Calculate the unnormalized Legendre functions by multiplying each row # by: sqrt((l+m)!/(l-m)!) == sqrt(prod(n-m+1:n+m)) # following Abramowitz and Stegun - for m in range(1,l): - Pl[m,:] *= np.prod(rootl[l-m+1:l+m+1]) + for m in range(1, l): + Pl[m, :] *= np.prod(rootl[l - m + 1 : l + m + 1]) # sectoral case (l = m) should be done separately to handle 0! - Pl[l,:] *= np.prod(rootl[1:]) + Pl[l, :] *= np.prod(rootl[1:]) return Pl diff --git a/gravity_toolkit/legendre_polynomials.py b/gravity_toolkit/legendre_polynomials.py index 332e6a00..63601cf0 100755 --- a/gravity_toolkit/legendre_polynomials.py +++ b/gravity_toolkit/legendre_polynomials.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" legendre_polynomials.py Written by Tyler Sutterley (11/2024) @@ -44,8 +44,10 @@ added option ASTYPE to output as different variable types e.g. np.float64 Written 03/2013 """ + import numpy as np + def legendre_polynomials(lmax, x, ASTYPE=np.float64): """ Computes fully-normalized Legendre polynomials and their first derivative @@ -76,36 +78,38 @@ def legendre_polynomials(lmax, x, ASTYPE=np.float64): # verify data type of spherical harmonic truncation lmax = np.int64(lmax) # output matrix of normalized legendre polynomials - pl = np.zeros((lmax+1,nx),dtype=ASTYPE) + pl = np.zeros((lmax + 1, nx), dtype=ASTYPE) # output matrix of First derivative of Legendre polynomials - dpl = np.zeros((lmax+1,nx),dtype=ASTYPE) + dpl = np.zeros((lmax + 1, nx), dtype=ASTYPE) # dummy matrix for the recurrence relation - ptemp = np.zeros((lmax+1,nx),dtype=ASTYPE) + ptemp = np.zeros((lmax + 1, nx), dtype=ASTYPE) # u is sine of colatitude (cosine of latitude) so that 0 <= s <= 1 # for x=cos(th): u=sin(th) u = np.sqrt(1.0 - x**2) # update where u==0 to eps of data type to prevent invalid divisions - u0, = np.nonzero(u == 0) + (u0,) = np.nonzero(u == 0) u[u0] = np.finfo(u.dtype).eps # Initialize the recurrence relation - ptemp[0,:] = 1.0 - ptemp[1,:] = x + ptemp[0, :] = 1.0 + ptemp[1, :] = x # Normalization is geodesy convention - pl[0,:] = ptemp[0,:] - pl[1,:] = np.sqrt(3.0)*ptemp[1,:] - for l in range(2,lmax+1): - ptemp[l,:] = (((2.0*l)-1.0)/l)*x*ptemp[l-1,:] - ((l-1.0)/l)*ptemp[l-2,:] + pl[0, :] = ptemp[0, :] + pl[1, :] = np.sqrt(3.0) * ptemp[1, :] + for l in range(2, lmax + 1): + ptemp[l, :] = (((2.0 * l) - 1.0) / l) * x * ptemp[l - 1, :] - ( + (l - 1.0) / l + ) * ptemp[l - 2, :] # Normalization is geodesy convention - pl[l,:] = np.sqrt((2.0*l)+1.0)*ptemp[l,:] + pl[l, :] = np.sqrt((2.0 * l) + 1.0) * ptemp[l, :] # Overwrite polar case (x == +/-1) - pl[l,u0] = np.sqrt((2.0*l)+1.0)*x[u0]**l + pl[l, u0] = np.sqrt((2.0 * l) + 1.0) * x[u0] ** l # First derivative of Legendre polynomials - for l in range(1,lmax+1): - fl = np.sqrt(((l**2.0) * (2.0*l + 1.0)) / (2.0*l - 1.0)) - dpl[l,:] = (1.0/u)*(l*x*pl[l,:] - fl*pl[l-1,:]) + for l in range(1, lmax + 1): + fl = np.sqrt(((l**2.0) * (2.0 * l + 1.0)) / (2.0 * l - 1.0)) + dpl[l, :] = (1.0 / u) * (l * x * pl[l, :] - fl * pl[l - 1, :]) # return the legendre polynomials and their first derivative return (pl, dpl) diff --git a/gravity_toolkit/mascons.py b/gravity_toolkit/mascons.py index 98a01c74..c1fa2e71 100644 --- a/gravity_toolkit/mascons.py +++ b/gravity_toolkit/mascons.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" mascons.py Written by Tyler Sutterley (07/2026) Conversion routines for publicly available GRACE/GRACE-FO mascon solutions @@ -26,10 +26,12 @@ Updated 12/2015: added TRANSPOSE option to output spatial routines Written 07/2013 """ + import copy import warnings import numpy as np + def to_gsfc(gdata, lon, lat, lon_center, lat_center, lon_span, lat_span): """ Converts an input gridded field to an output GSFC mascon array @@ -78,37 +80,41 @@ def to_gsfc(gdata, lon, lat, lon_center, lat_center, lon_span, lat_span): mascon_array['data'] = np.zeros((nmas)) mascon_array['lon_center'] = np.zeros((nmas)) mascon_array['lat_center'] = np.zeros((nmas)) - for k in range(0,nmas): + for k in range(0, nmas): # create latitudinal and longitudinal bounds for mascon k if (lat_center[k] == 90.0) | (lat_center[k] == -90.0): # NH and SH polar mascons - lon_bound = [0.0,360.0] - lat_bound = lat_center[k] + np.array([-1.0,1.0])*lat_span[k] + lon_bound = [0.0, 360.0] + lat_bound = lat_center[k] + np.array([-1.0, 1.0]) * lat_span[k] else: # convert from mascon centers to mascon bounds - lon_bound = lon_center[k] + np.array([-0.5,0.5])*lon_span[k] - lat_bound = lat_center[k] + np.array([-0.5,0.5])*lat_span[k] + lon_bound = lon_center[k] + np.array([-0.5, 0.5]) * lon_span[k] + lat_bound = lat_center[k] + np.array([-0.5, 0.5]) * lat_span[k] # if mascon is centered on +/-180: use 0:360 - if ((lon_bound[0] <= 180.0) & (lon_bound[1] >= 180.0)): + if (lon_bound[0] <= 180.0) & (lon_bound[1] >= 180.0): ilon = alon.copy() - elif ((lon_bound[0] <= -180.0) & (lon_bound[1] >= -180.0)): + elif (lon_bound[0] <= -180.0) & (lon_bound[1] >= -180.0): lon_bound += 360.0 ilon = alon.copy() else: ilon = lon.copy() # indices for grid points within the mascon - I, = np.flatnonzero((lat >= lat_bound[0]) & (lat < lat_bound[1])) - J, = np.flatnonzero((ilon >= lon_bound[0]) & (ilon < lon_bound[1])) - I,J = (I[np.newaxis,:], J[:,np.newaxis]) + (I,) = np.flatnonzero((lat >= lat_bound[0]) & (lat < lat_bound[1])) + (J,) = np.flatnonzero((ilon >= lon_bound[0]) & (ilon < lon_bound[1])) + I, J = (I[np.newaxis, :], J[:, np.newaxis]) # calculate average data for mascon bin - mascon_array['data'][k] = np.mean((np.cos(np.radians(lat[I])) / - np.mean(np.cos(np.radians(lat[I]))))*gdata[I,J]/len(I)) + mascon_array['data'][k] = np.mean( + (np.cos(np.radians(lat[I])) / np.mean(np.cos(np.radians(lat[I])))) + * gdata[I, J] + / len(I) + ) mascon_array['lat_center'][k] = lat_center[k] mascon_array['lon_center'][k] = lon_center[k] # return python dictionary with the mascon array data, lon and lat return mascon_array + def to_jpl(gdata, lon, lat, lon_bound, lat_bound): """ Converts an input gridded field to an output JPL mascon array @@ -139,7 +145,7 @@ def to_jpl(gdata, lon, lat, lon_bound, lat_bound): row vector of longitude values for mascons """ # mascon dimensions - nmas,nvar = lat_bound.shape + nmas, nvar = lat_bound.shape # remove singleton dimensions lat = np.squeeze(lat) lon = np.squeeze(lon) @@ -148,36 +154,47 @@ def to_jpl(gdata, lon, lat, lon_bound, lat_bound): # for that bin mascon_array = {} mascon_array['data'] = np.zeros((nmas)) - mascon_array['mask'] = np.zeros((nmas),dtype=bool) + mascon_array['mask'] = np.zeros((nmas), dtype=bool) mascon_array['lon'] = np.zeros((nmas)) mascon_array['lat'] = np.zeros((nmas)) - for k in range(0,nmas): + for k in range(0, nmas): # indices for grid points within the mascon - I, = np.flatnonzero((lat >= lat_bound[k,1]) & (lat < lat_bound[k,0])) - J, = np.flatnonzero((lon >= lon_bound[k,0]) & (lon < lon_bound[k,2])) - nlt = np.count_nonzero((lat >= lat_bound[k,1]) & (lat < lat_bound[k,0])) - I,J = (I[np.newaxis,:], J[:,np.newaxis]) + (I,) = np.flatnonzero( + (lat >= lat_bound[k, 1]) & (lat < lat_bound[k, 0]) + ) + (J,) = np.flatnonzero( + (lon >= lon_bound[k, 0]) & (lon < lon_bound[k, 2]) + ) + nlt = np.count_nonzero( + (lat >= lat_bound[k, 1]) & (lat < lat_bound[k, 0]) + ) + I, J = (I[np.newaxis, :], J[:, np.newaxis]) # calculate average data for mascon bin - mascon_array['data'][k] = np.mean((np.cos(np.radians(lat[I])) / - np.mean(np.cos(np.radians(lat[I]))))*gdata[I,J]/nlt) + mascon_array['data'][k] = np.mean( + (np.cos(np.radians(lat[I])) / np.mean(np.cos(np.radians(lat[I])))) + * gdata[I, J] + / nlt + ) # calculate coordinates of mascon center - mascon_array['lat'][k] = (lat_bound[k,1]+lat_bound[k,0])/2.0 - mascon_array['lon'][k] = (lon_bound[k,1]+lon_bound[k,2])/2.0 + mascon_array['lat'][k] = (lat_bound[k, 1] + lat_bound[k, 0]) / 2.0 + mascon_array['lon'][k] = (lon_bound[k, 1] + lon_bound[k, 2]) / 2.0 mascon_array['mask'][k] = bool(nlt == 0) # Do a check at the poles to make the lat/lon equal to +/-90/0 - if (np.abs(lat_bound[k,0]) == 90): - mascon_array['lat'][k] = lat_bound[k,0] + if np.abs(lat_bound[k, 0]) == 90: + mascon_array['lat'][k] = lat_bound[k, 0] mascon_array['lon'][k] = 0.0 - if (np.abs(lat_bound[k,1]) == 90): - mascon_array['lat'][k] = lat_bound[k,1] + if np.abs(lat_bound[k, 1]) == 90: + mascon_array['lat'][k] = lat_bound[k, 1] mascon_array['lon'][k] = 0.0 # replace invalid data with 0 mascon_array['data'][mascon_array['mask']] = 0.0 # return python dictionary with the mascon array data, lon and lat return mascon_array -def from_gsfc(mscdata, grid_spacing, lon_center, lat_center, lon_span, lat_span, - **kwargs): + +def from_gsfc( + mscdata, grid_spacing, lon_center, lat_center, lon_span, lat_span, **kwargs +): """ Converts an input GSFC mascon array to an output gridded field :cite:p:`Luthcke:2013ep` @@ -208,10 +225,13 @@ def from_gsfc(mscdata, grid_spacing, lon_center, lat_center, lon_span, lat_span, kwargs.setdefault('transpose', False) # raise warnings for deprecated keyword arguments deprecated_keywords = dict(TRANSPOSE='transpose') - for old,new in deprecated_keywords.items(): + for old, new in deprecated_keywords.items(): if old in kwargs.keys(): - warnings.warn(f"""Deprecated keyword argument {old}. - Changed to '{new}'""", DeprecationWarning) + warnings.warn( + f"""Deprecated keyword argument {old}. + Changed to '{new}'""", + DeprecationWarning, + ) # set renamed argument to not break workflows kwargs[new] = copy.copy(kwargs[old]) @@ -221,9 +241,13 @@ def from_gsfc(mscdata, grid_spacing, lon_center, lat_center, lon_span, lat_span, lon_center = np.where(lon_center > 180, lon_center - 360.0, lon_center) # Define output latitude and longitude grids - lon = np.arange(-180.0+grid_spacing/2.0,180.0+grid_spacing/2.0,grid_spacing) - lat = np.arange(90.0-grid_spacing/2.0,-90.0-grid_spacing/2.0,-grid_spacing) - nlon, nlat = (len(lon),len(lat)) + lon = np.arange( + -180.0 + grid_spacing / 2.0, 180.0 + grid_spacing / 2.0, grid_spacing + ) + lat = np.arange( + 90.0 - grid_spacing / 2.0, -90.0 - grid_spacing / 2.0, -grid_spacing + ) + nlon, nlat = (len(lon), len(lat)) # for mascons centered on 180: use 0:360 alon = np.copy(lon) alon = np.where(alon < 0, alon + 360.0, alon) @@ -234,25 +258,25 @@ def from_gsfc(mscdata, grid_spacing, lon_center, lat_center, lon_span, lat_span, # create latitudinal and longitudinal bounds for mascon k if (lat_center[k] == 90.0) | (lat_center[k] == -90.0): # NH and SH polar mascons - lon_bound = [0.0,360.0] - lat_bound = lat_center[k] + np.array([-1.0,1.0])*lat_span[k] + lon_bound = [0.0, 360.0] + lat_bound = lat_center[k] + np.array([-1.0, 1.0]) * lat_span[k] else: # convert from mascon centers to mascon bounds - lon_bound = lon_center[k] + np.array([-0.5,0.5])*lon_span[k] - lat_bound = lat_center[k] + np.array([-0.5,0.5])*lat_span[k] + lon_bound = lon_center[k] + np.array([-0.5, 0.5]) * lon_span[k] + lat_bound = lat_center[k] + np.array([-0.5, 0.5]) * lat_span[k] # if mascon is centered on +/-180: use 0:360 - if ((lon_bound[0] <= 180.0) & (lon_bound[1] >= 180.0)): + if (lon_bound[0] <= 180.0) & (lon_bound[1] >= 180.0): ilon = alon.copy() - elif ((lon_bound[0] <= -180.0) & (lon_bound[1] >= -180.0)): + elif (lon_bound[0] <= -180.0) & (lon_bound[1] >= -180.0): lon_bound += 360.0 ilon = alon.copy() else: ilon = lon.copy() # indices for grid points within the mascon - I, = np.flatnonzero((lat >= lat_bound[0]) & (lat < lat_bound[1])) - J, = np.flatnonzero((ilon >= lon_bound[0]) & (ilon < lon_bound[1])) - I,J = (I[np.newaxis,:], J[:,np.newaxis]) - mdata[I,J] = mscdata[k] + (I,) = np.flatnonzero((lat >= lat_bound[0]) & (lat < lat_bound[1])) + (J,) = np.flatnonzero((ilon >= lon_bound[0]) & (ilon < lon_bound[1])) + I, J = (I[np.newaxis, :], J[:, np.newaxis]) + mdata[I, J] = mscdata[k] # return array if kwargs['transpose']: @@ -260,6 +284,7 @@ def from_gsfc(mscdata, grid_spacing, lon_center, lat_center, lon_span, lat_span, else: return mdata + def from_jpl(mscdata, grid_spacing, lon_bound, lat_bound, **kwargs): """ Converts an input JPL mascon array to an output gridded field @@ -287,30 +312,41 @@ def from_jpl(mscdata, grid_spacing, lon_bound, lat_bound, **kwargs): kwargs.setdefault('transpose', False) # raise warnings for deprecated keyword arguments deprecated_keywords = dict(TRANSPOSE='transpose') - for old,new in deprecated_keywords.items(): + for old, new in deprecated_keywords.items(): if old in kwargs.keys(): - warnings.warn(f"""Deprecated keyword argument {old}. - Changed to '{new}'""", DeprecationWarning) + warnings.warn( + f"""Deprecated keyword argument {old}. + Changed to '{new}'""", + DeprecationWarning, + ) # set renamed argument to not break workflows kwargs[new] = copy.copy(kwargs[old]) # mascon dimensions - nmas,nvar = lat_bound.shape + nmas, nvar = lat_bound.shape # Define latitude and longitude grids # output lon will not include 360 # output lat will not include 90 - lon = np.arange(grid_spacing/2.0, 360.0+grid_spacing/2.0, grid_spacing) - lat = np.arange(-90.0+grid_spacing/2.0, 90.0+grid_spacing/2.0, grid_spacing) - nlon, nlat = (len(lon),len(lat)) + lon = np.arange( + grid_spacing / 2.0, 360.0 + grid_spacing / 2.0, grid_spacing + ) + lat = np.arange( + -90.0 + grid_spacing / 2.0, 90.0 + grid_spacing / 2.0, grid_spacing + ) + nlon, nlat = (len(lon), len(lat)) # loop over each mascon bin and assign value to grid points inside bin: mdata = np.zeros((nlat, nlon)) for k in range(0, nmas): - I, = np.flatnonzero((lat >= lat_bound[k,1]) & (lat < lat_bound[k,0])) - J, = np.flatnonzero((lon >= lon_bound[k,0]) & (lon < lon_bound[k,2])) - I,J = (I[np.newaxis,:], J[:,np.newaxis]) - mdata[I,J] = mscdata[k] + (I,) = np.flatnonzero( + (lat >= lat_bound[k, 1]) & (lat < lat_bound[k, 0]) + ) + (J,) = np.flatnonzero( + (lon >= lon_bound[k, 0]) & (lon < lon_bound[k, 2]) + ) + I, J = (I[np.newaxis, :], J[:, np.newaxis]) + mdata[I, J] = mscdata[k] # return array if kwargs['transpose']: diff --git a/gravity_toolkit/ocean_stokes.py b/gravity_toolkit/ocean_stokes.py index 1d723329..ab2ace73 100644 --- a/gravity_toolkit/ocean_stokes.py +++ b/gravity_toolkit/ocean_stokes.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" ocean_stokes.py Written by Tyler Sutterley (08/2023) @@ -59,13 +59,16 @@ Updated 05/2015: added parameter MMAX for MMAX != LMAX Written 03/2015 """ + import pathlib import numpy as np from gravity_toolkit.spatial import spatial from gravity_toolkit.gen_stokes import gen_stokes -def ocean_stokes(LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', - SIMPLIFY=False): + +def ocean_stokes( + LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', SIMPLIFY=False +): """ Reads a land-sea mask and converts to a series of spherical harmonics for ocean areas @@ -105,27 +108,36 @@ def ocean_stokes(LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = spatial().from_netCDF4(LANDMASK, - date=False, varname=VARNAME) + landsea = spatial().from_netCDF4(LANDMASK, date=False, varname=VARNAME) # create land function - nth,nphi = landsea.shape - land_function = np.zeros((nth,nphi), dtype=np.float64) + nth, nphi = landsea.shape + land_function = np.zeros((nth, nphi), dtype=np.float64) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + land_function[indx, indy] = 1.0 # remove isolated points if specified if SIMPLIFY: land_function -= find_isolated_points(land_function) # ocean function reciprocal of land function ocean_function = 1.0 - land_function # convert to spherical harmonics (1 cm w.e.) - Ylms = gen_stokes(ocean_function.T, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=LMAX, MMAX=MMAX, LOVE=LOVE) + Ylms = gen_stokes( + ocean_function.T, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=LMAX, + MMAX=MMAX, + LOVE=LOVE, + ) # return the spherical harmonic coefficients return Ylms -def land_stokes(LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', - SIMPLIFY=False): + +def land_stokes( + LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', SIMPLIFY=False +): """ Reads a land-sea mask and converts to a series of spherical harmonics for land areas @@ -165,23 +177,31 @@ def land_stokes(LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = spatial().from_netCDF4(LANDMASK, - date=False, varname=VARNAME) + landsea = spatial().from_netCDF4(LANDMASK, date=False, varname=VARNAME) # create land function - nth,nphi = landsea.shape - land_function = np.zeros((nth,nphi), dtype=np.float64) + nth, nphi = landsea.shape + land_function = np.zeros((nth, nphi), dtype=np.float64) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + land_function[indx, indy] = 1.0 # remove isolated points if specified if SIMPLIFY: land_function -= find_isolated_points(land_function) # convert to spherical harmonics (1 cm w.e.) - Ylms = gen_stokes(land_function.T, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=LMAX, MMAX=MMAX, LOVE=LOVE) + Ylms = gen_stokes( + land_function.T, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=LMAX, + MMAX=MMAX, + LOVE=LOVE, + ) # return the spherical harmonic coefficients return Ylms + def find_isolated_points(mask): """ Simplify a mask by removing isolated points @@ -196,16 +216,16 @@ def find_isolated_points(mask): isolated: np.ndarray simplified land-sea mask """ - nth,_ = mask.shape - laplacian = -4.0*np.copy(mask) - laplacian += mask*np.roll(mask,1,axis=1) - laplacian += mask*np.roll(mask,-1,axis=1) - temp = np.roll(mask,1,axis=0) - temp[0,:] = mask[1,:] - laplacian += mask*temp - temp = np.roll(mask,-1,axis=0) - temp[nth-1,:] = mask[nth-2,:] - laplacian += mask*temp + nth, _ = mask.shape + laplacian = -4.0 * np.copy(mask) + laplacian += mask * np.roll(mask, 1, axis=1) + laplacian += mask * np.roll(mask, -1, axis=1) + temp = np.roll(mask, 1, axis=0) + temp[0, :] = mask[1, :] + laplacian += mask * temp + temp = np.roll(mask, -1, axis=0) + temp[nth - 1, :] = mask[nth - 2, :] + laplacian += mask * temp # create mask of isolated points isolated = np.where(np.abs(laplacian) >= 3, 1, 0) return isolated diff --git a/gravity_toolkit/read_GIA_model.py b/gravity_toolkit/read_GIA_model.py index 39da6de0..1ef260c4 100755 --- a/gravity_toolkit/read_GIA_model.py +++ b/gravity_toolkit/read_GIA_model.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" read_GIA_model.py Written by Tyler Sutterley (05/2023) @@ -141,6 +141,7 @@ Updated 12/2012: changed the naming scheme for Simpson and Whitehouse Updated 09/2012: combined several GIA read programs into this standard """ + import re import copy import logging @@ -148,8 +149,21 @@ import numpy as np from gravity_toolkit.harmonics import harmonics -_known_gia_types = ['IJ05-R2', 'W12a', 'SM09', 'ICE6G', 'ICE6G-D', 'Wu10', - 'AW13-ICE6G', 'AW13-IJ05', 'Caron', 'ascii', 'netCDF4', 'HDF5'] +_known_gia_types = [ + 'IJ05-R2', + 'W12a', + 'SM09', + 'ICE6G', + 'ICE6G-D', + 'Wu10', + 'AW13-ICE6G', + 'AW13-IJ05', + 'Caron', + 'ascii', + 'netCDF4', + 'HDF5', +] + def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): """ @@ -221,46 +235,52 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): gia_Ylms['title'] = None # GIA model citations and references - if (GIA == 'IJ05-R2'): + if GIA == 'IJ05-R2': # IJ05-R2: Ivins R2 GIA Models prefix = 'IJ05_R2' gia_Ylms['citation'] = 'Ivins_et_al._(2013)' - gia_Ylms['reference'] = ('E. R. Ivins, T. S. James, J. Wahr, ' + gia_Ylms['reference'] = ( + 'E. R. Ivins, T. S. James, J. Wahr, ' 'E. J. O. Schrama, F. W. Landerer, and K. M. Simon, "Antarctic ' 'contribution to sea level rise observed by GRACE with improved ' 'GIA correction", Journal of Geophysical Research: Solid Earth, ' - '118(6), 3126-3141, (2013). https://doi.org/10.1002/jgrb.50208') + '118(6), 3126-3141, (2013). https://doi.org/10.1002/jgrb.50208' + ) gia_Ylms['url'] = 'https://doi.org/10.1002/jgrb.50208' # regular expression file pattern file_pattern = r'Stokes.R2_(.*?)_L120' # default degree of truncation LMAX = 120 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'ICE6G'): + elif GIA == 'ICE6G': # ICE6G: ICE-6G VM5 GIA Models prefix = 'ICE6G' gia_Ylms['citation'] = 'Peltier_et_al._(2015)' - gia_Ylms['reference'] = ('W. R. Peltier, D. F. Argus, and R. Drummond, ' + gia_Ylms['reference'] = ( + 'W. R. Peltier, D. F. Argus, and R. Drummond, ' '"Space geodesy constrains ice age terminal deglaciation: The ' 'global ICE-6G_C (VM5a) model", Journal of Geophysical Research: ' 'Solid Earth, 120(1), 450-487, (2015). ' - 'https://doi.org/10.1002/2014JB011176') + 'https://doi.org/10.1002/2014JB011176' + ) gia_Ylms['url'] = 'https://doi.org/10.1002/2014JB011176' # regular expression file pattern for test cases - #file_pattern = r'Stokes_G_Rot_60_I6_A_(.*?)_L90' + # file_pattern = r'Stokes_G_Rot_60_I6_A_(.*?)_L90' # regular expression file pattern for VM5 file_pattern = r'Stokes_G_Rot_60_I6_A_(.*)' # default degree of truncation LMAX = 60 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'W12a'): + elif GIA == 'W12a': # W12a: Whitehouse GIA Models prefix = 'W12a' gia_Ylms['citation'] = 'Whitehouse_et_al._(2012)' - gia_Ylms['reference'] = ('P. L. Whitehouse, M. J. Bentley, G. A. Milne, ' + gia_Ylms['reference'] = ( + 'P. L. Whitehouse, M. J. Bentley, G. A. Milne, ' 'M. A. King, I. D. Thomas, "A new glacial isostatic adjustment ' 'model for Antarctica: calibrated and tested using observations ' 'of relative sea-level change and present-day uplift rates", ' 'Geophysical Journal International, 190(3), 1464-1482, (2012). ' - 'https://doi.org/10.1111/j.1365-246X.2012.05557.x') + 'https://doi.org/10.1111/j.1365-246X.2012.05557.x' + ) gia_Ylms['url'] = 'https://doi.org/10.1111/j.1365-246X.2012.05557.x' # for Whitehouse W12a (BEST, LOWER, UPPER): parameters = dict(B='Best', L='Lower', U='Upper') @@ -268,80 +288,92 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): file_pattern = r'grate_(B|L|U).clm' # default degree of truncation LMAX = 120 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'SM09'): + elif GIA == 'SM09': # SM09: Simpson/Milne GIA Models prefix = 'SM09_Huy2' gia_Ylms['citation'] = 'Simpson_et_al._(2009)' - gia_Ylms['reference'] = ('M. J. R. Simpson, G. A. Milne, P. Huybrechts, ' + gia_Ylms['reference'] = ( + 'M. J. R. Simpson, G. A. Milne, P. Huybrechts, ' 'A. J. Long, "Calibrating a glaciological model of the Greenland ' 'ice sheet from the Last Glacial Maximum to present-day using ' 'field observations of relative sea level and ice extent", ' 'Quaternary Science Reviews, 28(17-18), 1631-1657, (2009). ' - 'https://doi.org/10.1016/j.quascirev.2009.03.004') + 'https://doi.org/10.1016/j.quascirev.2009.03.004' + ) gia_Ylms['url'] = 'https://doi.org/10.1016/j.quascirev.2009.03.004' # regular expression file pattern file_pattern = r'grate_(\d+)p(\d)(\d+).clm' # default degree of truncation LMAX = 120 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'Wu10'): + elif GIA == 'Wu10': # Wu10: Wu (2010) GIA Correction gia_Ylms['citation'] = 'Wu_et_al._(2010)' - gia_Ylms['reference'] = ('X. Wu, M. B. Heflin, H. Schotman, B. L. A. ' + gia_Ylms['reference'] = ( + 'X. Wu, M. B. Heflin, H. Schotman, B. L. A. ' 'Vermeersen, D. Dong, R. S. Gross, E. R. Ivins, A. W. Moore, S. E. ' 'Owen, "Simultaneous estimation of global present-day water ' 'transport and glacial isostatic adjustment", Nature Geoscience, ' - '3(9), 642-646, (2010). https://doi.org/10.1038/ngeo938') + '3(9), 642-646, (2010). https://doi.org/10.1038/ngeo938' + ) gia_Ylms['url'] = 'https://doi.org/10.1038/ngeo938' # default degree of truncation LMAX = 60 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'Caron'): + elif GIA == 'Caron': # Caron: Caron JPL GIA Assimilation gia_Ylms['citation'] = 'Caron_et_al._(2018)' - gia_Ylms['reference'] = ('L. Caron, E. R. Ivins, E. Larour, S. Adhikari, ' + gia_Ylms['reference'] = ( + 'L. Caron, E. R. Ivins, E. Larour, S. Adhikari, ' 'J. Nilsson and G. Blewitt, "GIA Model Statistics for GRACE ' 'Hydrology, Cryosphere, and Ocean Science", Geophysical Research ' 'Letters, 45(5), 2203-2212, (2018). ' - 'https://doi.org/10.1002/2017GL076644') + 'https://doi.org/10.1002/2017GL076644' + ) gia_Ylms['url'] = 'https://doi.org/10.1002/2017GL076644' # default degree of truncation LMAX = 89 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'ICE6G-D'): + elif GIA == 'ICE6G-D': # ICE6G-D: ICE-6G Version-D GIA Models prefix = 'ICE6G-D' gia_Ylms['citation'] = 'Peltier_et_al._(2018)' - gia_Ylms['reference'] = ('W. R. Peltier, D. F. Argus, and R. Drummond, ' + gia_Ylms['reference'] = ( + 'W. R. Peltier, D. F. Argus, and R. Drummond, ' '"Comment on "An assessment of the ICE-6G_C (VM5a) glacial ' 'isostatic adjustment model" by Purcell et al.", Journal of ' 'Geophysical Research: Solid Earth, 123(2), 2019-2028, (2018). ' - 'https://doi.org/10.1002/2016JB013844') + 'https://doi.org/10.1002/2016JB013844' + ) gia_Ylms['url'] = 'https://doi.org/10.1002/2016JB013844' # regular expression file pattern for Version-D file_pattern = r'(ICE-6G_)?(.*?)[_]?Stokes_trend[_]?(.*?)\.txt$' # default degree of truncation LMAX = 256 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'AW13-ICE6G'): + elif GIA == 'AW13-ICE6G': # AW13-ICE6G: Geruo A ICE-6G GIA Models prefix = 'AW13' gia_Ylms['citation'] = 'A_et_al._(2013)' - gia_Ylms['reference'] = ('G. A, J. Wahr, and S. Zhong, "Computations of ' + gia_Ylms['reference'] = ( + 'G. A, J. Wahr, and S. Zhong, "Computations of ' 'the viscoelastic response of a 3-D compressible Earth to surface ' 'loading; an application to Glacial Isostatic Adjustment in ' 'Antarctica and Canada", Geophysical Journal International, ' - '192(2), 557-572, (2013). https://doi.org/10.1093/gji/ggs030') + '192(2), 557-572, (2013). https://doi.org/10.1093/gji/ggs030' + ) gia_Ylms['url'] = 'https://doi.org/10.1093/gji/ggs030' # regular expressions file pattern file_pattern = r'stokes\.(ice6g)[\.\_](.*?)(\.txt)?$' # default degree of truncation LMAX = 100 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'AW13-IJ05'): + elif GIA == 'AW13-IJ05': # AW13-IJ05: Geruo A IJ05-R2 GIA Models prefix = 'AW13_IJ05' gia_Ylms['citation'] = 'A_et_al._(2013)' - gia_Ylms['reference'] = ('G. A, J. Wahr, and S. Zhong, "Computations of ' + gia_Ylms['reference'] = ( + 'G. A, J. Wahr, and S. Zhong, "Computations of ' 'the viscoelastic response of a 3-D compressible Earth to surface ' 'loading; an application to Glacial Isostatic Adjustment in ' 'Antarctica and Canada", Geophysical Journal International, ' - '192(2), 557-572, (2013). https://doi.org/10.1093/gji/ggs030') + '192(2), 557-572, (2013). https://doi.org/10.1093/gji/ggs030' + ) gia_Ylms['url'] = 'https://doi.org/10.1093/gji/ggs030' # regular expressions file pattern file_pattern = r'stokes\.(R2)_(.*?)(\_ANT)?$' @@ -360,7 +392,7 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): start = 0 # scale factor for geodesy normalization scale = 1e-11 - elif (GIA == 'ICE6G-D'): + elif GIA == 'ICE6G-D': # ICE-6G Version-D # scale factor for geodesy normalization scale = 1.0 @@ -370,10 +402,10 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): # initially read for spherical harmonic degree up to LMAX # will truncate to MMAX before exiting program - gia_Ylms['clm'] = np.zeros((LMAX+1,LMAX+1)) - gia_Ylms['slm'] = np.zeros((LMAX+1,LMAX+1)) + gia_Ylms['clm'] = np.zeros((LMAX + 1, LMAX + 1)) + gia_Ylms['slm'] = np.zeros((LMAX + 1, LMAX + 1)) # output spherical harmonic degree and order - gia_Ylms['l'],gia_Ylms['m'] = (np.arange(LMAX+1),np.arange(LMAX+1)) + gia_Ylms['l'], gia_Ylms['m'] = (np.arange(LMAX + 1), np.arange(LMAX + 1)) # if reading a GIA model if GIA in _known_gia_types: @@ -398,23 +430,23 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): gia_lines = len(gia_data) # Skipping file header for geruo files with header - for ii in range(start,gia_lines): + for ii in range(start, gia_lines): # check if contents in line - flag = bool(rx.search(gia_data[ii].replace('D','E'))) + flag = bool(rx.search(gia_data[ii].replace('D', 'E'))) if flag: # find numerical instances in line including exponents, # decimal points and negatives # Replacing Double Exponent with Standard Exponent - line = rx.findall(gia_data[ii].replace('D','E')) + line = rx.findall(gia_data[ii].replace('D', 'E')) l1 = np.int64(line[0]) m1 = np.int64(line[1]) # truncate to LMAX if (l1 <= LMAX) and (m1 <= LMAX): # scaling to geodesy normalization - gia_Ylms['clm'][l1,m1] = np.float64(line[2])*scale - gia_Ylms['slm'][l1,m1] = np.float64(line[3])*scale + gia_Ylms['clm'][l1, m1] = np.float64(line[2]) * scale + gia_Ylms['slm'][l1, m1] = np.float64(line[3]) * scale - elif (GIA == 'ICE6G'): + elif GIA == 'ICE6G': # ICE-6G VM5 notes # need to scale by 1e-11 for geodesy-normalization # spherical harmonic degrees listed only on order 0 @@ -426,9 +458,9 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): # counter variable ii = 0 - for l in range(0, LMAX+1): - for m in range(0, l+1): - if ((m % 2) == 0): + for l in range(0, LMAX + 1): + for m in range(0, l + 1): + if (m % 2) == 0: # reading gia line if the order is even # find numerical instances in line including exponents, # decimal points and negatives @@ -437,22 +469,22 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): ii += 1 # if m is even: clm column = 1, slm column = 2 c = 0 - else: # if m is odd: clm column = 3, slm column = 4 + else: # if m is odd: clm column = 3, slm column = 4 c = 2 - if ((m == 0) or (m == 1)): + if (m == 0) or (m == 1): # l is column 1 if m == 0 or 1 # degree is not listed for other SHd: column 1 = clm c += 1 - if (len(line) > 0): + if len(line) > 0: # no empty lines # convert to float and scale - gia_Ylms['clm'][l,m] = np.float64(line[0+c])*scale - gia_Ylms['slm'][l,m] = np.float64(line[1+c])*scale + gia_Ylms['clm'][l, m] = np.float64(line[0 + c]) * scale + gia_Ylms['slm'][l, m] = np.float64(line[1 + c]) * scale - elif (GIA == 'Wu10'): + elif GIA == 'Wu10': # Wu (2010) notes: # Need to convert from mm geoid to fully normalized - rad_e = 6.371e9# Average Radius of the Earth [mm] + rad_e = 6.371e9 # Average Radius of the Earth [mm] # note: the GIA file starts with a header # converting to numerical array (note 64 bit floating point) @@ -467,26 +499,26 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): # 1 1 s # 2 0 c # 2 1 c - for l in range(1, LMAX+1): - for m in range(0, l+1): + for l in range(1, LMAX + 1): + for m in range(0, l + 1): for cs in range(0, 2): # unwrapping GIA file and converting to geoid # Clm - if (cs == 0): - gia_Ylms['clm'][l,m] = gia_data[ii]/rad_e + if cs == 0: + gia_Ylms['clm'][l, m] = gia_data[ii] / rad_e ii += 1 # Slm if (m != 0) and (cs == 1): - gia_Ylms['slm'][l,m] = gia_data[ii]/rad_e + gia_Ylms['slm'][l, m] = gia_data[ii] / rad_e ii += 1 - elif (GIA == 'Caron'): + elif GIA == 'Caron': # Caron et al. (2018) # note: the GIA file starts with a header. # converting to numerical array (note 64 bit floating point) - dtype = {'names':('l','m','Ylms'),'formats':('i','i','f8')} - gia_data=np.loadtxt(input_file, skiprows=4, dtype=dtype) + dtype = {'names': ('l', 'm', 'Ylms'), 'formats': ('i', 'i', 'f8')} + gia_data = np.loadtxt(input_file, skiprows=4, dtype=dtype) # Order of harmonics in the file # 0 0 c # 1 1 s @@ -497,16 +529,15 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): # 2 0 c # 2 1 c # 2 2 c - for l,m,Ylm in zip(gia_data['l'],gia_data['m'],gia_data['Ylms']): + for l, m, Ylm in zip(gia_data['l'], gia_data['m'], gia_data['Ylms']): # unwrapping GIA file - if (m >= 0) and (l <= LMAX) and (m <= LMAX):# Clm - gia_Ylms['clm'][l,m] = Ylm.copy() - elif (m < 0) and (l <= LMAX) and (m <= LMAX):# Slm - gia_Ylms['slm'][l,np.abs(m)] = Ylm.copy() + if (m >= 0) and (l <= LMAX) and (m <= LMAX): # Clm + gia_Ylms['clm'][l, m] = Ylm.copy() + elif (m < 0) and (l <= LMAX) and (m <= LMAX): # Slm + gia_Ylms['slm'][l, np.abs(m)] = Ylm.copy() # Reading ICE-6G Version-D GIA files - elif (GIA == 'ICE6G-D'): - + elif GIA == 'ICE6G-D': # opening GIA data file and read contents with input_file.open(mode='r', encoding='utf8') as f: gia_data = f.read().splitlines() @@ -517,48 +548,47 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): h1 = r'^GRACE Approximation for degrees 0 to 2' h2 = r'^GRACE Approximation\/Absolute Sea-level Values for degrees \> 2' # header lines to skip - header, = [(i+1) for i,l in enumerate(gia_data) if re.match(h1,l)] - start, = [(i+1) for i,l in enumerate(gia_data) if re.match(h2,l)] + (header,) = [(i + 1) for i, l in enumerate(gia_data) if re.match(h1, l)] + (start,) = [(i + 1) for i, l in enumerate(gia_data) if re.match(h2, l)] # Calculating number of cos and sin harmonics to read from header - n_harm = (2**2 + 3*2)//2 + 1 + n_harm = (2**2 + 3 * 2) // 2 + 1 # extract header for GRACE approximation - for ii in range(header,header+n_harm): + for ii in range(header, header + n_harm): # check if contents in line - flag = bool(rx.search(gia_data[ii].replace('D','E'))) + flag = bool(rx.search(gia_data[ii].replace('D', 'E'))) if flag: # find numerical instances in line including exponents, # decimal points and negatives # Replacing Double Exponent with Standard Exponent - line = rx.findall(gia_data[ii].replace('D','E')) + line = rx.findall(gia_data[ii].replace('D', 'E')) l1 = np.int64(line[0]) m1 = np.int64(line[1]) # truncate to LMAX if (l1 <= LMAX) and (m1 <= LMAX): # scaling to geodesy normalization - gia_Ylms['clm'][l1,m1] = np.float64(line[2])*scale - gia_Ylms['slm'][l1,m1] = np.float64(line[3])*scale + gia_Ylms['clm'][l1, m1] = np.float64(line[2]) * scale + gia_Ylms['slm'][l1, m1] = np.float64(line[3]) * scale # Skipping rest of file header - for ii in range(start,gia_lines): + for ii in range(start, gia_lines): # check if contents in line - flag = bool(rx.search(gia_data[ii].replace('D','E'))) + flag = bool(rx.search(gia_data[ii].replace('D', 'E'))) if flag: # find numerical instances in line including exponents, # decimal points and negatives # Replacing Double Exponent with Standard Exponent - line = rx.findall(gia_data[ii].replace('D','E')) + line = rx.findall(gia_data[ii].replace('D', 'E')) l1 = np.int64(line[0]) m1 = np.int64(line[1]) # truncate to LMAX if (l1 <= LMAX) and (m1 <= LMAX): # scaling to geodesy normalization - gia_Ylms['clm'][l1,m1] = np.float64(line[2])*scale - gia_Ylms['slm'][l1,m1] = np.float64(line[3])*scale - + gia_Ylms['clm'][l1, m1] = np.float64(line[2]) * scale + gia_Ylms['slm'][l1, m1] = np.float64(line[3]) * scale # ascii: reformatted GIA in ascii format - elif (GIA == 'ascii'): + elif GIA == 'ascii': # reading GIA data from reformatted (simplified) ascii files Ylms = harmonics().from_ascii(input_file, date=False) Ylms.truncate(LMAX) @@ -571,65 +601,68 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): # netCDF4: reformatted GIA in netCDF4 format # HDF5: reformatted GIA in HDF5 format - elif GIA in ('netCDF4','HDF5'): + elif GIA in ('netCDF4', 'HDF5'): # reading GIA data from reformatted netCDF4 and HDF5 files Ylms = harmonics().from_file(input_file, format=GIA, date=False) Ylms.truncate(LMAX) gia_Ylms.update(Ylms.to_dict()) # copy title and reference for model - for att_name in ('title','citation','reference','url'): + for att_name in ('title', 'citation', 'reference', 'url'): try: - s, = [s for s in Ylms.attributes['ROOT'].keys() if - re.match(att_name,s,re.I)] + (s,) = [ + s + for s in Ylms.attributes['ROOT'].keys() + if re.match(att_name, s, re.I) + ] gia_Ylms[att_name] = Ylms.attributes['ROOT'][s] except (ValueError, KeyError, AttributeError): gia_Ylms[att_name] = None # GIA model parameter strings # extract rheology from the file name - if (GIA == 'IJ05-R2'): + if GIA == 'IJ05-R2': # IJ05-R2: Ivins R2 GIA Models # adding file specific earth parameters - parameters, = re.findall(file_pattern, input_file.name) + (parameters,) = re.findall(file_pattern, input_file.name) gia_Ylms['title'] = f'{prefix}_{parameters}' - elif (GIA == 'ICE6G'): + elif GIA == 'ICE6G': # ICE6G: ICE-6G GIA Models # adding file specific earth parameters - parameters, = re.findall(file_pattern, input_file.name) + (parameters,) = re.findall(file_pattern, input_file.name) gia_Ylms['title'] = f'{prefix}_{parameters}' - elif (GIA == 'W12a'): + elif GIA == 'W12a': # W12a: Whitehouse GIA Models # for Whitehouse W12a (BEST, LOWER, UPPER): model = re.findall(file_pattern, input_file.name).pop() gia_Ylms['title'] = f'{prefix}_{parameters[model]}' - elif (GIA == 'SM09'): + elif GIA == 'SM09': # SM09: Simpson/Milne GIA Models # making parameters in the file similar to IJ05 # split rheological parameters between lithospheric thickness, # upper mantle viscosity and lower mantle viscosity - LTh,UMV,LMV = re.findall(file_pattern, input_file.name).pop() + LTh, UMV, LMV = re.findall(file_pattern, input_file.name).pop() # formatting rheology parameters similar to IJ05 models gia_Ylms['title'] = f'{prefix}_{LTh}_.{UMV}_{LMV}' - elif (GIA == 'Wu10'): + elif GIA == 'Wu10': # Wu10: Wu (2010) GIA Correction gia_Ylms['title'] = 'Wu_2010' - elif (GIA == 'Caron'): + elif GIA == 'Caron': # Caron: Caron JPL GIA Assimilation gia_Ylms['title'] = 'Caron_expt' - elif (GIA == 'ICE6G-D'): + elif GIA == 'ICE6G-D': # ICE6G-D: ICE-6G Version-D GIA Models # adding file specific earth parameters - m1,p1,p2 = re.findall(file_pattern, input_file.name).pop() + m1, p1, p2 = re.findall(file_pattern, input_file.name).pop() gia_Ylms['title'] = f'{prefix}_{p1}{p2}' - elif (GIA == 'AW13-ICE6G'): + elif GIA == 'AW13-ICE6G': # AW13-ICE6G: Geruo A ICE-6G GIA Models # extract the ice history and case flags - hist,case,sf=re.findall(file_pattern, input_file.name).pop() + hist, case, sf = re.findall(file_pattern, input_file.name).pop() gia_Ylms['title'] = f'{prefix}_{hist}_{case}' - elif (GIA == 'AW13-IJ05'): + elif GIA == 'AW13-IJ05': # AW13-IJ05: Geruo A IJ05-R2 GIA Models # adding file specific earth parameters - vrs,param,aux=re.findall(file_pattern, input_file.name).pop() + vrs, param, aux = re.findall(file_pattern, input_file.name).pop() gia_Ylms['title'] = f'{prefix}_{vrs}_{param}' # output harmonics to ascii, netCDF4 or HDF5 file @@ -641,22 +674,28 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') filename = f'stokes_{title}_L{LMAX:d}.{suffix[DATAFORM]}' output_file = input_file.with_name(filename) - Ylms.to_file(output_file, format=DATAFORM, date=False, - title=title, reference=gia_Ylms['reference']) + Ylms.to_file( + output_file, + format=DATAFORM, + date=False, + title=title, + reference=gia_Ylms['reference'], + ) # set permissions level of output file output_file.chmod(mode=kwargs['MODE']) # truncate to MMAX if specified if MMAX is not None: # spherical harmonic variables - gia_Ylms['clm'] = gia_Ylms['clm'][:,:MMAX+1] - gia_Ylms['slm'] = gia_Ylms['slm'][:,:MMAX+1] + gia_Ylms['clm'] = gia_Ylms['clm'][:, : MMAX + 1] + gia_Ylms['slm'] = gia_Ylms['slm'][:, : MMAX + 1] # spherical harmonic order - gia_Ylms['m'] = gia_Ylms['m'][:MMAX+1] + gia_Ylms['m'] = gia_Ylms['m'][: MMAX + 1] # return the harmonics and the parameters return gia_Ylms + class gia(harmonics): """ Inheritance of ``harmonics`` class for reading Glacial @@ -677,7 +716,9 @@ class gia(harmonics): filename: str input or output filename """ + np.seterr(invalid='ignore') + # inherit harmonics class to read GIA models def __init__(self, **kwargs): super().__init__(**kwargs) @@ -714,15 +755,19 @@ def from_GIA(self, filename, **kwargs): # set filename self.case_insensitive_filename(filename) # set default keyword arguments - kwargs.setdefault('GIA',None) - kwargs.setdefault('mmax',None) - kwargs.setdefault('verbose',False) + kwargs.setdefault('GIA', None) + kwargs.setdefault('mmax', None) + kwargs.setdefault('verbose', False) # catch case where MMAX is entered - if ('MMAX' in kwargs.keys()): + if 'MMAX' in kwargs.keys(): kwargs['mmax'] = np.copy(kwargs['mmax']) # read data from GIA file - Ylms = read_GIA_model(self.filename, GIA=kwargs['GIA'], - LMAX=self.lmax, MMAX=kwargs['mmax']) + Ylms = read_GIA_model( + self.filename, + GIA=kwargs['GIA'], + LMAX=self.lmax, + MMAX=kwargs['mmax'], + ) # Output file information logging.info(self.filename) logging.info(list(Ylms.keys())) diff --git a/gravity_toolkit/read_GRACE_harmonics.py b/gravity_toolkit/read_GRACE_harmonics.py index eeb6cc77..70868ad8 100644 --- a/gravity_toolkit/read_GRACE_harmonics.py +++ b/gravity_toolkit/read_GRACE_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" read_GRACE_harmonics.py Written by Tyler Sutterley (11/2024) Contributions by Hugo Lecomte @@ -67,6 +67,7 @@ output file headers and parse new YAML headers for RL06 and GRACE-FO Written 10/2017 for public release """ + import re import io import gzip @@ -75,6 +76,7 @@ import numpy as np import gravity_toolkit.time + # PURPOSE: read Level-2 GRACE and GRACE-FO spherical harmonic files def read_GRACE_harmonics(input_file, LMAX, **kwargs): """ @@ -119,9 +121,9 @@ def read_GRACE_harmonics(input_file, LMAX, **kwargs): kwargs.setdefault('POLE_TIDE', False) # parse filename - PFX,SY,SD,EY,ED,N,PRC,F1,DRL,F2,SFX = parse_file(input_file) + PFX, SY, SD, EY, ED, N, PRC, F1, DRL, F2, SFX = parse_file(input_file) # check if file is compressed - compressed = (SFX == '.gz') + compressed = SFX == '.gz' # extract file contents file_contents = extract_file(input_file, compressed) @@ -141,12 +143,12 @@ def read_GRACE_harmonics(input_file, LMAX, **kwargs): # GFC solutions from the GFZ ICGEM # https://icgem.gfz-potsdam.de/sl/temporal elif PRC in ('COSTG',) or SFX in ('.gfc',): - DSET, = re.findall(r'(GSM|GAA|GAB|GAC|GAD)', PFX) + (DSET,) = re.findall(r'(GSM|GAA|GAB|GAC|GAD)', PFX) DREL = np.int64(DRL) FLAG = r'gfc' # Standard GRACE/GRACE-FO Level-2 solutions else: - DSET, = re.findall(r'(GSM|GAA|GAB|GAC|GAD)', PFX) + (DSET,) = re.findall(r'(GSM|GAA|GAB|GAC|GAD)', PFX) DREL = np.int64(DRL) FLAG = r'GRCOF2' @@ -162,51 +164,71 @@ def read_GRACE_harmonics(input_file, LMAX, **kwargs): dpy = gravity_toolkit.time.calendar_days(start_yr).sum() # For data that crosses years (end_yr - start_yr should be at most 1) - end_cyclic = ((end_yr - start_yr)*dpy+end_day) + end_cyclic = (end_yr - start_yr) * dpy + end_day # Calculate mid-month value mid_day = np.mean([start_day, end_cyclic]) # Calculating the mid-month date in decimal form - grace_L2_input['time'] = start_yr + mid_day/dpy + grace_L2_input['time'] = start_yr + mid_day / dpy # Calculating the Julian dates of the start and end date - grace_L2_input['start'] = 2400000.5 + \ - gravity_toolkit.time.convert_calendar_dates(start_yr,1.0,start_day, - epoch=(1858,11,17,0,0,0)) - grace_L2_input['end'] = 2400000.5 + \ - gravity_toolkit.time.convert_calendar_dates(end_yr,1.0,end_day, - epoch=(1858,11,17,0,0,0)) + grace_L2_input['start'] = ( + 2400000.5 + + gravity_toolkit.time.convert_calendar_dates( + start_yr, 1.0, start_day, epoch=(1858, 11, 17, 0, 0, 0) + ) + ) + grace_L2_input['end'] = ( + 2400000.5 + + gravity_toolkit.time.convert_calendar_dates( + end_yr, 1.0, end_day, epoch=(1858, 11, 17, 0, 0, 0) + ) + ) # set maximum spherical harmonic order - MMAX = np.copy(LMAX) if (kwargs['MMAX'] is None) else np.copy(kwargs['MMAX']) + MMAX = ( + np.copy(LMAX) if (kwargs['MMAX'] is None) else np.copy(kwargs['MMAX']) + ) # output dimensions - grace_L2_input['l'] = np.arange(LMAX+1) - grace_L2_input['m'] = np.arange(MMAX+1) + grace_L2_input['l'] = np.arange(LMAX + 1) + grace_L2_input['m'] = np.arange(MMAX + 1) # Spherical harmonic coefficient matrices to be filled from data file - grace_L2_input['clm'] = np.zeros((LMAX+1, MMAX+1)) - grace_L2_input['slm'] = np.zeros((LMAX+1, MMAX+1)) + grace_L2_input['clm'] = np.zeros((LMAX + 1, MMAX + 1)) + grace_L2_input['slm'] = np.zeros((LMAX + 1, MMAX + 1)) # spherical harmonic uncalibrated standard deviations - grace_L2_input['eclm'] = np.zeros((LMAX+1, MMAX+1)) - grace_L2_input['eslm'] = np.zeros((LMAX+1, MMAX+1)) - if ((DREL == 4) and (DSET == 'GSM')): + grace_L2_input['eclm'] = np.zeros((LMAX + 1, MMAX + 1)) + grace_L2_input['eslm'] = np.zeros((LMAX + 1, MMAX + 1)) + if (DREL == 4) and (DSET == 'GSM'): # clm and slm drift rates for RL04 - drift_c = np.zeros((LMAX+1, MMAX+1)) - drift_s = np.zeros((LMAX+1, MMAX+1)) + drift_c = np.zeros((LMAX + 1, MMAX + 1)) + drift_s = np.zeros((LMAX + 1, MMAX + 1)) # set default degree 0 harmonics for intercomparability between centers grace_L2_input['clm'][0, 0] = 1.0 # extract GRACE and GRACE-FO file headers # replace colons in header if within quotations - head = [re.sub(r'\"(.*?)\:\s(.*?)\"',r'"\1, \2"',l) for l in file_contents - if not re.match(rf'{FLAG}|GRDOTA',l)] + head = [ + re.sub(r'\"(.*?)\:\s(.*?)\"', r'"\1, \2"', l) + for l in file_contents + if not re.match(rf'{FLAG}|GRDOTA', l) + ] if SFX in ('.gfc',): # extract parameters from header - header_parameters = ['modelname','earth_gravity_constant','radius', - 'max_degree','errors','norm','tide_system'] + header_parameters = [ + 'modelname', + 'earth_gravity_constant', + 'radius', + 'max_degree', + 'errors', + 'norm', + 'tide_system', + ] header_regex = re.compile(r'(' + r'|'.join(header_parameters) + r')') header = [l.split(maxsplit=1) for l in head if header_regex.match(l)] - grace_L2_input['header'] = {i[0]:i[1] for i in header} + grace_L2_input['header'] = {i[0]: i[1] for i in header} elif ((N == 'GRAC') and (DREL >= 6)) or (N == 'GRFO'): # parse the YAML header for RL06 or GRACE-FO (specifying yaml loader) - grace_L2_input.update(yaml.load('\n'.join(head),Loader=yaml.BaseLoader)) + grace_L2_input.update( + yaml.load('\n'.join(head), Loader=yaml.BaseLoader) + ) else: # save lines of the GRACE file header removing empty lines grace_L2_input['header'] = [l.rstrip() for l in head if l] @@ -214,80 +236,85 @@ def read_GRACE_harmonics(input_file, LMAX, **kwargs): # for each line in the GRACE/GRACE-FO file for line in file_contents: # find if line starts with data marker flag (e.g. GRCOF2) - if bool(re.match(FLAG,line)): + if bool(re.match(FLAG, line)): # split the line into individual components line_contents = line.split() # degree and order for the line l1 = np.int64(line_contents[1]) m1 = np.int64(line_contents[2]) # if degree and order are below the truncation limits - if ((l1 <= LMAX) and (m1 <= MMAX)): - grace_L2_input['clm'][l1,m1] = np.float64(line_contents[3]) - grace_L2_input['slm'][l1,m1] = np.float64(line_contents[4]) - grace_L2_input['eclm'][l1,m1] = np.float64(line_contents[5]) - grace_L2_input['eslm'][l1,m1] = np.float64(line_contents[6]) + if (l1 <= LMAX) and (m1 <= MMAX): + grace_L2_input['clm'][l1, m1] = np.float64(line_contents[3]) + grace_L2_input['slm'][l1, m1] = np.float64(line_contents[4]) + grace_L2_input['eclm'][l1, m1] = np.float64(line_contents[5]) + grace_L2_input['eslm'][l1, m1] = np.float64(line_contents[6]) # find if line starts with drift rate flag - elif bool(re.match(r'GRDOTA',line)): + elif bool(re.match(r'GRDOTA', line)): # split the line into individual components line_contents = line.split() l1 = np.int64(line_contents[1]) m1 = np.int64(line_contents[2]) # Reading Drift rates for low degree harmonics - drift_c[l1,m1] = np.float64(line_contents[3]) - drift_s[l1,m1] = np.float64(line_contents[4]) + drift_c[l1, m1] = np.float64(line_contents[3]) + drift_s[l1, m1] = np.float64(line_contents[4]) # Adding drift rates to clm and slm for RL04 # if drift rates exist at any time, will add to harmonics # Will convert the secular rates into a stokes contribution # Currently removes 2003.3 to get the temporal average close to 0. - if ((DREL == 4) and (DSET == 'GSM')): + if (DREL == 4) and (DSET == 'GSM'): # time since 2003.3 - dt = (grace_L2_input['time'] - 2003.3) - grace_L2_input['clm'][:,:] += dt*drift_c[:,:] - grace_L2_input['slm'][:,:] += dt*drift_s[:,:] + dt = grace_L2_input['time'] - 2003.3 + grace_L2_input['clm'][:, :] += dt * drift_c[:, :] + grace_L2_input['slm'][:, :] += dt * drift_s[:, :] # Correct Pole Tide following Wahr et al. (2015) 10.1002/2015JB011986 if kwargs['POLE_TIDE'] and (DSET == 'GSM'): # time since 2000.0 - dt = (grace_L2_input['time']-2000.0) + dt = grace_L2_input['time'] - 2000.0 # CSR and JPL Pole Tide Correction - if PRC in ('UTCSR','JPLEM','JPLMSC'): + if PRC in ('UTCSR', 'JPLEM', 'JPLMSC'): # values for IERS mean pole [2010] - if (grace_L2_input['time'] < 2010.0): - a = np.array([0.055974,1.8243e-3,1.8413e-4,7.024e-6]) - b = np.array([-0.346346,-1.7896e-3,1.0729e-4,0.908e-6]) - elif (grace_L2_input['time'] >= 2010.0): - a = np.array([0.023513,7.6141e-3,0.0,0.0]) - b = np.array([-0.358891,0.6287e-3,0.0,0.0]) + if grace_L2_input['time'] < 2010.0: + a = np.array([0.055974, 1.8243e-3, 1.8413e-4, 7.024e-6]) + b = np.array([-0.346346, -1.7896e-3, 1.0729e-4, 0.908e-6]) + elif grace_L2_input['time'] >= 2010.0: + a = np.array([0.023513, 7.6141e-3, 0.0, 0.0]) + b = np.array([-0.358891, 0.6287e-3, 0.0, 0.0]) # calculate m1 and m2 values m1 = np.copy(a[0]) m2 = np.copy(b[0]) - for x in range(1,4): - m1 += a[x]*dt**x - m2 += b[x]*dt**x + for x in range(1, 4): + m1 += a[x] * dt**x + m2 += b[x] * dt**x # pole tide values for CSR and JPL # CSR and JPL both remove the IERS mean pole from m1 and m2 # before computing their harmonic solutions - C21_PT = -1.551e-9*(m1 - 0.62e-3*dt) - 0.012e-9*(m2 + 3.48e-3*dt) - S21_PT = 0.021e-9*(m1 - 0.62e-3*dt) - 1.505e-9*(m2 + 3.48e-3*dt) + C21_PT = -1.551e-9 * (m1 - 0.62e-3 * dt) - 0.012e-9 * ( + m2 + 3.48e-3 * dt + ) + S21_PT = 0.021e-9 * (m1 - 0.62e-3 * dt) - 1.505e-9 * ( + m2 + 3.48e-3 * dt + ) # correct GRACE/GRACE-FO spherical harmonics for pole tide - grace_L2_input['clm'][2,1] -= C21_PT - grace_L2_input['slm'][2,1] -= S21_PT + grace_L2_input['clm'][2, 1] -= C21_PT + grace_L2_input['slm'][2, 1] -= S21_PT # GFZ Pole Tide Correction - elif PRC in ('EIGEN','GFZOP'): + elif PRC in ('EIGEN', 'GFZOP'): # pole tide values for GFZ # GFZ removes only a constant pole position - C21_PT = -1.551e-9*(-0.62e-3*dt) - 0.012e-9*(3.48e-3*dt) - S21_PT = 0.021e-9*(-0.62e-3*dt) - 1.505e-9*(3.48e-3*dt) + C21_PT = -1.551e-9 * (-0.62e-3 * dt) - 0.012e-9 * (3.48e-3 * dt) + S21_PT = 0.021e-9 * (-0.62e-3 * dt) - 1.505e-9 * (3.48e-3 * dt) # correct GRACE/GRACE-FO spherical harmonics for pole tide - grace_L2_input['clm'][2,1] -= C21_PT - grace_L2_input['slm'][2,1] -= S21_PT + grace_L2_input['clm'][2, 1] -= C21_PT + grace_L2_input['slm'][2, 1] -= S21_PT # return the header data, GRACE/GRACE-FO data # GRACE/GRACE-FO date (mid-month in decimal) # and the start and end days as Julian dates return grace_L2_input + # PURPOSE: extract parameters from filename def parse_file(input_file): """ @@ -309,8 +336,10 @@ def parse_file(input_file): # GRGS: CNES Groupe de Recherche de Geodesie Spatiale centers = r'UTCSR|EIGEN|GFZOP|JPLEM|JPLMSC|GRGS|COSTG|GRGS' suffixes = r'\.gz|\.gfc|\.txt' - regex_pattern = (r'(.*?)-2_(\d{4})(\d{3})-(\d{4})(\d{3})_' - rf'(.*?)_({centers})_(.*?)_(\d+)(.*?)({suffixes})?$') + regex_pattern = ( + r'(.*?)-2_(\d{4})(\d{3})-(\d{4})(\d{3})_' + rf'(.*?)_({centers})_(.*?)_(\d+)(.*?)({suffixes})?$' + ) rx = re.compile(regex_pattern, re.VERBOSE) # extract parameters from input filename if isinstance(input_file, io.IOBase): @@ -318,6 +347,7 @@ def parse_file(input_file): else: return rx.findall(pathlib.Path(input_file).name).pop() + # PURPOSE: read input file and extract contents def extract_file(input_file, compressed): """ diff --git a/gravity_toolkit/read_SLR_harmonics.py b/gravity_toolkit/read_SLR_harmonics.py index 37430e6e..dc575eb7 100644 --- a/gravity_toolkit/read_SLR_harmonics.py +++ b/gravity_toolkit/read_SLR_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" read_SLR_harmonics.py Written by Tyler Sutterley (05/2023) @@ -68,6 +68,7 @@ Updated 10/2017: include the 6,0 and 6,1 coefficients in output Ylms Written 10/2017 """ + from __future__ import division import re @@ -75,6 +76,7 @@ import numpy as np import gravity_toolkit.time + # PURPOSE: wrapper function for calling individual readers def read_SLR_harmonics(SLR_file, **kwargs): """ @@ -90,11 +92,14 @@ def read_SLR_harmonics(SLR_file, **kwargs): """ if bool(re.search(r'gsfc_slr_5x5c61s61', SLR_file.name, re.I)): return read_GSFC_weekly_6x1(SLR_file, **kwargs) - elif bool(re.search(r'CSR_Monthly_5x5_Gravity_Harmonics', SLR_file.name, re.I)): + elif bool( + re.search(r'CSR_Monthly_5x5_Gravity_Harmonics', SLR_file.name, re.I) + ): return read_CSR_monthly_6x1(SLR_file, **kwargs) else: raise Exception(f'Unknown SLR file format {SLR_file}') + # PURPOSE: read monthly degree harmonic data from Satellite Laser Ranging (SLR) def read_CSR_monthly_6x1(SLR_file, SCALE=1e-10, HEADER=True): """ @@ -139,7 +144,7 @@ def read_CSR_monthly_6x1(SLR_file, SCALE=1e-10, HEADER=True): # new 5x5 fields no longer include geocenter components LMIN = 2 LMAX = 6 - n_harm = (LMAX**2 + 3*LMAX - LMIN**2 - LMIN)//2 - 5 + n_harm = (LMAX**2 + 3 * LMAX - LMIN**2 - LMIN) // 2 - 5 # counts the number of lines in the header count = 0 @@ -149,8 +154,8 @@ def read_CSR_monthly_6x1(SLR_file, SCALE=1e-10, HEADER=True): # file line at count line = file_contents[count] # find end within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'end\sof\sheader',line)) - if bool(re.match(80*r'=',line)): + HEADER = not bool(re.match(r'end\sof\sheader', line)) + if bool(re.match(80 * r'=', line)): indice = count + 1 # add 1 to counter count += 1 @@ -160,55 +165,55 @@ def read_CSR_monthly_6x1(SLR_file, SCALE=1e-10, HEADER=True): raise Exception('Mean field header not found') # number of dates within the file - n_dates = (file_lines - count)//(n_harm + 1) + n_dates = (file_lines - count) // (n_harm + 1) # read mean fields from the header mean_Ylms = {} mean_Ylm_error = {} - mean_Ylms['clm'] = np.zeros((LMAX+1,LMAX+1)) - mean_Ylms['slm'] = np.zeros((LMAX+1,LMAX+1)) - mean_Ylm_error['clm'] = np.zeros((LMAX+1,LMAX+1)) - mean_Ylm_error['slm'] = np.zeros((LMAX+1,LMAX+1)) + mean_Ylms['clm'] = np.zeros((LMAX + 1, LMAX + 1)) + mean_Ylms['slm'] = np.zeros((LMAX + 1, LMAX + 1)) + mean_Ylm_error['clm'] = np.zeros((LMAX + 1, LMAX + 1)) + mean_Ylm_error['slm'] = np.zeros((LMAX + 1, LMAX + 1)) for i in range(n_harm): # split the line into individual components - line = file_contents[indice+i].split() + line = file_contents[indice + i].split() # degree and order for the line l1 = np.int64(line[0]) m1 = np.int64(line[1]) # fill mean field Ylms - mean_Ylms['clm'][l1,m1] = np.float64(line[2].replace('D','E')) - mean_Ylms['slm'][l1,m1] = np.float64(line[3].replace('D','E')) - mean_Ylm_error['clm'][l1,m1] = np.float64(line[4].replace('D','E')) - mean_Ylm_error['slm'][l1,m1] = np.float64(line[5].replace('D','E')) + mean_Ylms['clm'][l1, m1] = np.float64(line[2].replace('D', 'E')) + mean_Ylms['slm'][l1, m1] = np.float64(line[3].replace('D', 'E')) + mean_Ylm_error['clm'][l1, m1] = np.float64(line[4].replace('D', 'E')) + mean_Ylm_error['slm'][l1, m1] = np.float64(line[5].replace('D', 'E')) # output spherical harmonic fields Ylms = {} Ylms['error'] = {} Ylms['MJD'] = np.zeros((n_dates)) Ylms['time'] = np.zeros((n_dates)) - Ylms['clm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylms['slm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylms['error']['clm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylms['error']['slm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) + Ylms['clm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylms['slm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylms['error']['clm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylms['error']['slm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) # input spherical harmonic anomalies and errors Ylm_anomalies = {} Ylm_anomaly_error = {} - Ylm_anomalies['clm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylm_anomalies['slm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylm_anomaly_error['clm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylm_anomaly_error['slm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) + Ylm_anomalies['clm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylm_anomalies['slm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylm_anomaly_error['clm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylm_anomaly_error['slm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) # for each date for d in range(n_dates): # split the date line into individual components line_contents = file_contents[count].split() # verify arc number from iteration and file IARC = int(line_contents[0]) - assert (IARC == (d+1)) + assert IARC == (d + 1) # modified Julian date of the middle of the month - Ylms['MJD'][d] = np.mean(np.array(line_contents[5:7],dtype=np.float64)) + Ylms['MJD'][d] = np.mean(np.array(line_contents[5:7], dtype=np.float64)) # date of the mid-point of the arc given in years - YY,MM = np.array(line_contents[3:5]) - Ylms['time'][d] = gravity_toolkit.time.convert_calendar_decimal(YY,MM) + YY, MM = np.array(line_contents[3:5]) + Ylms['time'][d] = gravity_toolkit.time.convert_calendar_decimal(YY, MM) # add 1 to counter count += 1 @@ -220,24 +225,33 @@ def read_CSR_monthly_6x1(SLR_file, SCALE=1e-10, HEADER=True): l1 = np.int64(line[0]) m1 = np.int64(line[1]) # fill anomaly field Ylms and rescale to output - Ylm_anomalies['clm'][l1,m1,d] = np.float64(line[2])*SCALE - Ylm_anomalies['slm'][l1,m1,d] = np.float64(line[3])*SCALE - Ylm_anomaly_error['clm'][l1,m1,d] = np.float64(line[6])*SCALE - Ylm_anomaly_error['slm'][l1,m1,d] = np.float64(line[7])*SCALE + Ylm_anomalies['clm'][l1, m1, d] = np.float64(line[2]) * SCALE + Ylm_anomalies['slm'][l1, m1, d] = np.float64(line[3]) * SCALE + Ylm_anomaly_error['clm'][l1, m1, d] = np.float64(line[6]) * SCALE + Ylm_anomaly_error['slm'][l1, m1, d] = np.float64(line[7]) * SCALE # add 1 to counter count += 1 # calculate full coefficients and full errors - Ylms['clm'][:,:,d] = Ylm_anomalies['clm'][:,:,d] + mean_Ylms['clm'][:,:] - Ylms['slm'][:,:,d] = Ylm_anomalies['slm'][:,:,d] + mean_Ylms['slm'][:,:] - Ylms['error']['clm'][:,:,d]=np.sqrt(Ylm_anomaly_error['clm'][:,:,d]**2 + - mean_Ylm_error['clm'][:,:]**2) - Ylms['error']['slm'][:,:,d]=np.sqrt(Ylm_anomaly_error['slm'][:,:,d]**2 + - mean_Ylm_error['slm'][:,:]**2) + Ylms['clm'][:, :, d] = ( + Ylm_anomalies['clm'][:, :, d] + mean_Ylms['clm'][:, :] + ) + Ylms['slm'][:, :, d] = ( + Ylm_anomalies['slm'][:, :, d] + mean_Ylms['slm'][:, :] + ) + Ylms['error']['clm'][:, :, d] = np.sqrt( + Ylm_anomaly_error['clm'][:, :, d] ** 2 + + mean_Ylm_error['clm'][:, :] ** 2 + ) + Ylms['error']['slm'][:, :, d] = np.sqrt( + Ylm_anomaly_error['slm'][:, :, d] ** 2 + + mean_Ylm_error['slm'][:, :] ** 2 + ) # return spherical harmonic fields and date information return Ylms + # PURPOSE: read weekly degree harmonic data from Satellite Laser Ranging (SLR) def read_GSFC_weekly_6x1(SLR_file, SCALE=1.0, HEADER=True): r""" @@ -278,7 +292,7 @@ def read_GSFC_weekly_6x1(SLR_file, SCALE=1.0, HEADER=True): # spherical harmonic degree range (5x5 with 6,1) LMIN = 2 LMAX = 6 - n_harm = (LMAX**2 + 3*LMAX - LMIN**2 - LMIN)//2 - 5 + n_harm = (LMAX**2 + 3 * LMAX - LMIN**2 - LMIN) // 2 - 5 # counts the number of lines in the header count = 0 @@ -288,18 +302,18 @@ def read_GSFC_weekly_6x1(SLR_file, SCALE=1.0, HEADER=True): line = file_contents[count] # find the final line within the header text # to set HEADER flag to False when found - HEADER = not bool(re.search(r'Product:',line)) + HEADER = not bool(re.search(r'Product:', line)) # add 1 to counter count += 1 # number of dates within the file - n_dates = (file_lines - count)//(n_harm + 1) + n_dates = (file_lines - count) // (n_harm + 1) # output spherical harmonic fields Ylms = {} Ylms['MJD'] = np.zeros((n_dates)) Ylms['time'] = np.zeros((n_dates)) - Ylms['clm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylms['slm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) + Ylms['clm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylms['slm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) # for each date for d in range(n_dates): # split the date line into individual components @@ -319,14 +333,15 @@ def read_GSFC_weekly_6x1(SLR_file, SCALE=1.0, HEADER=True): l1 = np.int64(line_contents[0]) m1 = np.int64(line_contents[1]) # Spherical Harmonic data rescaled to output - Ylms['clm'][l1,m1,d] = np.float64(line_contents[2])*SCALE - Ylms['slm'][l1,m1,d] = np.float64(line_contents[3])*SCALE + Ylms['clm'][l1, m1, d] = np.float64(line_contents[2]) * SCALE + Ylms['slm'][l1, m1, d] = np.float64(line_contents[3]) * SCALE # add 1 to counter count += 1 # return spherical harmonic fields and date information return Ylms + # PURPOSE: interpolate harmonics from 7-day to monthly def convert_weekly(t_in, d_in, DATE=[], NEIGHBORS=28): """ @@ -356,15 +371,15 @@ def convert_weekly(t_in, d_in, DATE=[], NEIGHBORS=28): tdec = np.repeat(t_in, 7) data = np.repeat(d_in, 7) # calculate daily dates to use in centered moving average - tdec += (np.mod(np.arange(len(tdec)),7) - 3.5)/365.25 + tdec += (np.mod(np.arange(len(tdec)), 7) - 3.5) / 365.25 # calculate moving-average solution from 7-day arcs dinput = {} dinput['time'] = np.zeros_like(DATE) - dinput['data'] = np.zeros_like(DATE,dtype='f8') + dinput['data'] = np.zeros_like(DATE, dtype='f8') # for each output monthly date - for i,D in enumerate(DATE): + for i, D in enumerate(DATE): # find all dates within NEIGHBORS days of mid-point - isort = np.argsort((tdec - D)**2)[:NEIGHBORS] + isort = np.argsort((tdec - D) ** 2)[:NEIGHBORS] # calculate monthly mean of date and data dinput['time'][i] = np.mean(tdec[isort]) dinput['data'][i] = np.mean(data[isort]) diff --git a/gravity_toolkit/read_gfc_harmonics.py b/gravity_toolkit/read_gfc_harmonics.py index d3444887..314a0da9 100644 --- a/gravity_toolkit/read_gfc_harmonics.py +++ b/gravity_toolkit/read_gfc_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" read_gfc_harmonics.py Written by Tyler Sutterley (06/2023) Contributions by Hugo Lecomte @@ -68,6 +68,7 @@ Updated 07/2017: include parameters to change the tide system Written 12/2015 """ + import re import pathlib import numpy as np @@ -77,6 +78,7 @@ # attempt imports geoidtk = import_dependency('geoid_toolkit') + # PURPOSE: read spherical harmonic coefficients of a gravity model def read_gfc_harmonics(input_file, TIDE=None, FLAG='gfc'): """ @@ -148,8 +150,10 @@ def read_gfc_harmonics(input_file, TIDE=None, FLAG='gfc'): itsg_products.append(r'Grace2016') itsg_products.append(r'Grace2018') itsg_products.append(r'Grace_operational') - itsg_pattern = (r'(AOD1B_RL\d+|model|ITSG)[-_]({0})(_n\d+)?_' - r'(\d+)-(\d+)(\.gfc)').format(r'|'.join(itsg_products)) + itsg_pattern = ( + r'(AOD1B_RL\d+|model|ITSG)[-_]({0})(_n\d+)?_' + r'(\d+)-(\d+)(\.gfc)' + ).format(r'|'.join(itsg_products)) # regular expression operators for Swarm data and models swarm_data = r'(SW)_(.*?)_(EGF_SHA_2)__(.*?)_(.*?)_(.*?)(\.gfc|\.ZIP)' swarm_model = r'(GAA|GAB|GAC|GAD)_Swarm_(\d+)_(\d{2})_(\d{4})(\.gfc|\.ZIP)' @@ -159,20 +163,22 @@ def read_gfc_harmonics(input_file, TIDE=None, FLAG='gfc'): # GRAZ: Institute of Geodesy from GRAZ University of Technology rx = re.compile(itsg_pattern, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - PFX,PRD,trunc,year,month,SFX = rx.findall(input_file.name).pop() + PFX, PRD, trunc, year, month, SFX = rx.findall(input_file.name).pop() # number of days in each month for the calendar year dpm = gravity_toolkit.time.calendar_days(int(year)) # create start and end date lists - start_date = [int(year),int(month),1,0,0,0] - end_date = [int(year),int(month),dpm[int(month)-1],23,59,59] + start_date = [int(year), int(month), 1, 0, 0, 0] + end_date = [int(year), int(month), dpm[int(month) - 1], 23, 59, 59] elif re.match(swarm_data, input_file.name): # compile numerical expression operator for parameters from files # Swarm: data from Swarm satellite rx = re.compile(swarm_data, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - SAT,tmp,PROD,starttime,endtime,RL,SFX = rx.findall(input_file.name).pop() - start_date,_ = gravity_toolkit.time.parse_date_string(starttime) - end_date,_ = gravity_toolkit.time.parse_date_string(endtime) + SAT, tmp, PROD, starttime, endtime, RL, SFX = rx.findall( + input_file.name + ).pop() + start_date, _ = gravity_toolkit.time.parse_date_string(starttime) + end_date, _ = gravity_toolkit.time.parse_date_string(endtime) # number of days in each month for the calendar year dpm = gravity_toolkit.time.calendar_days(start_date[0]) elif re.match(swarm_model, input_file.name): @@ -180,38 +186,65 @@ def read_gfc_harmonics(input_file, TIDE=None, FLAG='gfc'): # Swarm: dealiasing products for Swarm data rx = re.compile(swarm_data, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - PROD,trunc,month,year,SFX = rx.findall(input_file.name).pop() + PROD, trunc, month, year, SFX = rx.findall(input_file.name).pop() # number of days in each month for the calendar year dpm = gravity_toolkit.time.calendar_days(int(year)) # create start and end date lists - start_date = [int(year),int(month),1,0,0,0] - end_date = [int(year),int(month),dpm[int(month)-1],23,59,59] + start_date = [int(year), int(month), 1, 0, 0, 0] + end_date = [int(year), int(month), dpm[int(month) - 1], 23, 59, 59] # python dictionary with model input and headers ZIP = bool(re.search('ZIP', SFX, re.IGNORECASE)) - model_input = geoidtk.read_ICGEM_harmonics(input_file, TIDE=TIDE, - FLAG=FLAG, ZIP=ZIP) + model_input = geoidtk.read_ICGEM_harmonics( + input_file, TIDE=TIDE, FLAG=FLAG, ZIP=ZIP + ) # start and end day of the year - start_day = np.sum(dpm[:start_date[1]-1]) + start_date[2] + \ - start_date[3]/24.0 + start_date[4]/1440.0 + start_date[5]/86400.0 - end_day = np.sum(dpm[:end_date[1]-1]) + end_date[2] + \ - end_date[3]/24.0 + end_date[4]/1440.0 + end_date[5]/86400.0 + start_day = ( + np.sum(dpm[: start_date[1] - 1]) + + start_date[2] + + start_date[3] / 24.0 + + start_date[4] / 1440.0 + + start_date[5] / 86400.0 + ) + end_day = ( + np.sum(dpm[: end_date[1] - 1]) + + end_date[2] + + end_date[3] / 24.0 + + end_date[4] / 1440.0 + + end_date[5] / 86400.0 + ) # end date taking into account measurements taken on different years - end_cyclic = (end_date[0]-start_date[0])*np.sum(dpm) + end_day + end_cyclic = (end_date[0] - start_date[0]) * np.sum(dpm) + end_day # calculate mid-month value mid_day = np.mean([start_day, end_cyclic]) # Calculating the mid-month date in decimal form - model_input['time'] = start_date[0] + mid_day/np.sum(dpm) + model_input['time'] = start_date[0] + mid_day / np.sum(dpm) # Calculating the Julian dates of the start and end date - model_input['start'] = 2400000.5 + \ - gravity_toolkit.time.convert_calendar_dates(start_date[0], - start_date[1],start_date[2],hour=start_date[3],minute=start_date[4], - second=start_date[5],epoch=(1858,11,17,0,0,0)) - model_input['end'] = 2400000.5 + \ - gravity_toolkit.time.convert_calendar_dates(end_date[0], - end_date[1],end_date[2],hour=end_date[3],minute=end_date[4], - second=end_date[5],epoch=(1858,11,17,0,0,0)) + model_input['start'] = ( + 2400000.5 + + gravity_toolkit.time.convert_calendar_dates( + start_date[0], + start_date[1], + start_date[2], + hour=start_date[3], + minute=start_date[4], + second=start_date[5], + epoch=(1858, 11, 17, 0, 0, 0), + ) + ) + model_input['end'] = ( + 2400000.5 + + gravity_toolkit.time.convert_calendar_dates( + end_date[0], + end_date[1], + end_date[2], + hour=end_date[3], + minute=end_date[4], + second=end_date[5], + epoch=(1858, 11, 17, 0, 0, 0), + ) + ) # return the spherical harmonics and parameters return model_input diff --git a/gravity_toolkit/read_love_numbers.py b/gravity_toolkit/read_love_numbers.py index bb8c9e02..47db2ccf 100755 --- a/gravity_toolkit/read_love_numbers.py +++ b/gravity_toolkit/read_love_numbers.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" read_love_numbers.py Written by Tyler Sutterley (11/2024) @@ -88,6 +88,7 @@ Updated 05/2013: python updates and comment updates Written 01/2012 """ + import io import re import logging @@ -98,9 +99,16 @@ # default maximum degree and order in case of infinite _default_max_degree = 100000 + # PURPOSE: read load Love/Shida numbers from PREM -def read_love_numbers(love_numbers_file, LMAX=None, HEADER=2, - COLUMNS=['l','hl','kl','ll'], REFERENCE='CE', FORMAT='tuple'): +def read_love_numbers( + love_numbers_file, + LMAX=None, + HEADER=2, + COLUMNS=['l', 'hl', 'kl', 'll'], + REFERENCE='CE', + FORMAT='tuple', +): """ Reads PREM load Love/Shida numbers file and applies isomorphic parameters :cite:p:`Dziewonski:1981bz,Blewitt:2003bz` @@ -163,78 +171,80 @@ def read_love_numbers(love_numbers_file, LMAX=None, HEADER=2, # dictionary of output Love/Shida numbers love = {} # spherical harmonic degree - love['l'] = np.arange(LMAX+1) + love['l'] = np.arange(LMAX + 1) # vertical displacement hl # gravitational potential kl # horizontal displacement ll (Shida number) - for n in ('hl','kl','ll'): - love[n] = np.zeros((LMAX+1)) + for n in ('hl', 'kl', 'll'): + love[n] = np.zeros((LMAX + 1)) # check if needing to interpolate between degrees - flag = np.ones((LMAX+1),dtype=bool) + flag = np.ones((LMAX + 1), dtype=bool) # for each line in the file (skipping header lines) for file_line in file_contents[HEADER:]: # find numerical instances in line # replacing fortran double precision exponential - love_numbers = rx.findall(file_line.replace('D','E')) + love_numbers = rx.findall(file_line.replace('D', 'E')) # spherical harmonic degree degree = love_numbers[COLUMNS.index('l')] l = _default_max_degree if (degree == 'inf') else int(degree) # truncate to spherical harmonic degree LMAX - if (l <= LMAX): + if l <= LMAX: # convert Love/Shida numbers to float # vertical displacement hl # gravitational potential kl # horizontal displacement ll (Shida number) - for n in ('hl','kl','ll'): + for n in ('hl', 'kl', 'll'): love[n][l] = np.float64(love_numbers[COLUMNS.index(n)]) # set interpolation flag for degree flag[l] = False # return Love/Shida numbers in output format - if (LMAX == 0): + if LMAX == 0: return love_number_formatter(love, FORMAT=FORMAT) # if needing to linearly interpolate Love/Shida numbers if np.any(flag): # linearly interpolate following Wahr (1998) - for n in ('hl','kl','ll'): - love[n][flag] = np.interp(love['l'][flag], - love['l'][~flag], love[n][~flag]) + for n in ('hl', 'kl', 'll'): + love[n][flag] = np.interp( + love['l'][flag], love['l'][~flag], love[n][~flag] + ) # if needing to linearly extrapolate Love/Shida numbers # NOTE: use caution if extrapolating far beyond the # maximum degree of the Love/Shida numbers dataset - for lint in range(l,LMAX+1): + for lint in range(l, LMAX + 1): # linearly extrapolate to maximum degree - for n in ('hl','kl','ll'): - love[n][lint] = 2.0*love[n][lint-1] - love[n][lint-2] + for n in ('hl', 'kl', 'll'): + love[n][lint] = 2.0 * love[n][lint - 1] - love[n][lint - 2] # calculate isomorphic parameters for different reference frames # From Blewitt (2003), Wahr (1998), Trupin (1992) and Farrell (1972) - if (REFERENCE.upper() == 'CF'): + if REFERENCE.upper() == 'CF': # Center of Surface Figure - alpha = (love['hl'][1] + 2.0*love['ll'][1])/3.0 - elif (REFERENCE.upper() == 'CL'): + alpha = (love['hl'][1] + 2.0 * love['ll'][1]) / 3.0 + elif REFERENCE.upper() == 'CL': # Center of Surface Lateral Figure alpha = love['ll'][1].copy() - elif (REFERENCE.upper() == 'CH'): + elif REFERENCE.upper() == 'CH': # Center of Surface Height Figure alpha = love['hl'][1].copy() - elif (REFERENCE.upper() == 'CM'): + elif REFERENCE.upper() == 'CM': # Center of Mass of Earth System alpha = 1.0 - elif (REFERENCE.upper() == 'CE'): + elif REFERENCE.upper() == 'CE': # Center of Mass of Solid Earth alpha = 0.0 else: raise Exception(f'Invalid Reference Frame {REFERENCE}') # apply isomorphic parameters - for n in ('hl','kl','ll'): + for n in ('hl', 'kl', 'll'): love[n][1] -= alpha # return Love/Shida numbers in output format return love_number_formatter(love, FORMAT=FORMAT) + # PURPOSE: return load Love/Shida numbers in a particular format def love_number_formatter(love, FORMAT='tuple'): """ @@ -262,15 +272,16 @@ def love_number_formatter(love, FORMAT='tuple'): ll: np.ndarray Love (Shida) number of Horizontal Displacement """ - if (FORMAT == 'dict'): + if FORMAT == 'dict': return love - elif (FORMAT == 'tuple'): + elif FORMAT == 'tuple': return (love['hl'], love['kl'], love['ll']) - elif (FORMAT == 'zip'): + elif FORMAT == 'zip': return zip(love['hl'], love['kl'], love['ll']) - elif (FORMAT == 'class'): + elif FORMAT == 'class': return love_numbers().from_dict(love) + # PURPOSE: read input file and extract contents def extract_love_numbers(love_numbers_file): """ @@ -284,7 +295,9 @@ def extract_love_numbers(love_numbers_file): # check if input Love/Shida numbers are a string or bytesIO object if isinstance(love_numbers_file, (str, pathlib.Path)): # tilde expansion of load love number data file - love_numbers_file = pathlib.Path(love_numbers_file).expanduser().absolute() + love_numbers_file = ( + pathlib.Path(love_numbers_file).expanduser().absolute() + ) # check that load Love/Shida number data file is present in file system if not love_numbers_file.exists(): raise FileNotFoundError(f'{str(love_numbers_file)} not found') @@ -297,6 +310,7 @@ def extract_love_numbers(love_numbers_file): else: raise ValueError('Invalid Love/Shida numbers file input') + # PURPOSE: read load Love/Shida numbers for a range of spherical harmonic degrees def load_love_numbers(LMAX, LOVE_NUMBERS=0, REFERENCE='CF', FORMAT='tuple'): """ @@ -340,48 +354,52 @@ def load_love_numbers(LMAX, LOVE_NUMBERS=0, REFERENCE='CF', FORMAT='tuple'): Love (Shida) number of Horizontal Displacement """ # load Love/Shida numbers file - if (LOVE_NUMBERS == 0): + if LOVE_NUMBERS == 0: # PREM outputs from Han and Wahr (1995) # https://doi.org/10.1111/j.1365-246X.1995.tb01819.x - love_numbers_file = get_data_path(['data','love_numbers']) + love_numbers_file = get_data_path(['data', 'love_numbers']) model = 'PREM' citation = 'Han and Wahr (1995)' header = 2 - columns = ['l','hl','kl','ll'] - elif (LOVE_NUMBERS == 1): + columns = ['l', 'hl', 'kl', 'll'] + elif LOVE_NUMBERS == 1: # PREM outputs from Gegout (2005) # http://gemini.gsfc.nasa.gov/aplo/ - love_numbers_file = get_data_path(['data','Load_Love2_CE.dat']) + love_numbers_file = get_data_path(['data', 'Load_Love2_CE.dat']) model = 'PREM' citation = 'Gegout et al. (2010)' header = 3 - columns = ['l','hl','ll','kl'] - elif (LOVE_NUMBERS == 2): + columns = ['l', 'hl', 'll', 'kl'] + elif LOVE_NUMBERS == 2: # PREM outputs from Wang et al. (2012) # https://doi.org/10.1016/j.cageo.2012.06.022 - love_numbers_file = get_data_path(['data','PREM-LLNs-truncated.dat']) + love_numbers_file = get_data_path(['data', 'PREM-LLNs-truncated.dat']) model = 'PREM' citation = 'Wang et al. (2012)' header = 1 - columns = ['l','hl','ll','kl','nl','nk'] - elif (LOVE_NUMBERS == 3): + columns = ['l', 'hl', 'll', 'kl', 'nl', 'nk'] + elif LOVE_NUMBERS == 3: # PREM hard outputs from Wang et al. (2012) # case with 0.46 kilometers thick hard sediment # https://doi.org/10.1016/j.cageo.2012.06.022 - love_numbers_file = get_data_path(['data','PREMhard-LLNs-truncated.dat']) + love_numbers_file = get_data_path( + ['data', 'PREMhard-LLNs-truncated.dat'] + ) model = 'PREMhard' citation = 'Wang et al. (2012)' header = 1 - columns = ['l','hl','ll','kl','nl','nk'] - elif (LOVE_NUMBERS == 4): + columns = ['l', 'hl', 'll', 'kl', 'nl', 'nk'] + elif LOVE_NUMBERS == 4: # PREM soft outputs from Wang et al. (2012) # case with 0.52 kilometers thick soft sediment # https://doi.org/10.1016/j.cageo.2012.06.022 - love_numbers_file = get_data_path(['data','PREMsoft-LLNs-truncated.dat']) + love_numbers_file = get_data_path( + ['data', 'PREMsoft-LLNs-truncated.dat'] + ) model = 'PREMsoft' citation = 'Wang et al. (2012)' header = 1 - columns = ['l','hl','ll','kl','nl','nk'] + columns = ['l', 'hl', 'll', 'kl', 'nl', 'nk'] else: raise ValueError(f'Unknown Love Numbers Type {LOVE_NUMBERS:d}') # validate as pathlib object @@ -393,10 +411,16 @@ def load_love_numbers(LMAX, LOVE_NUMBERS=0, REFERENCE='CF', FORMAT='tuple'): # however, as we are linearly extrapolating out, do not make # LMAX too much larger than 696 # read arrays of kl, hl, and ll Love/Shida Numbers - love = read_love_numbers(love_numbers_file, LMAX=LMAX, HEADER=header, - COLUMNS=columns, REFERENCE=REFERENCE, FORMAT=FORMAT) + love = read_love_numbers( + love_numbers_file, + LMAX=LMAX, + HEADER=header, + COLUMNS=columns, + REFERENCE=REFERENCE, + FORMAT=FORMAT, + ) # append model and filename attributes to class - if (FORMAT == 'class'): + if FORMAT == 'class': love.filename = love_numbers_file.name love.reference = REFERENCE love.model = model @@ -404,6 +428,7 @@ def load_love_numbers(LMAX, LOVE_NUMBERS=0, REFERENCE='CF', FORMAT='tuple'): # return the load love numbers return love + class love_numbers(object): """ Data class for Load Love/Shida numbers @@ -434,21 +459,23 @@ class love_numbers(object): filename: str input filename of Load Love/Shida Numbers """ + np.seterr(invalid='ignore') + def __init__(self, **kwargs): # set default keyword arguments - kwargs.setdefault('lmax',None) + kwargs.setdefault('lmax', None) # set default class attributes - self.hl=[] - self.kl=[] - self.ll=[] - self.lmax=kwargs['lmax'] + self.hl = [] + self.kl = [] + self.ll = [] + self.lmax = kwargs['lmax'] # calculate spherical harmonic degree (0 is falsy) - self.l=np.arange(self.lmax+1) if (self.lmax is not None) else None - self.reference=None - self.model=None - self.citation=None - self.filename=None + self.l = np.arange(self.lmax + 1) if (self.lmax is not None) else None + self.reference = None + self.model = None + self.citation = None + self.filename = None def from_dict(self, d): """ @@ -460,7 +487,7 @@ def from_dict(self, d): dictionary object to be converted """ # retrieve each Load Love/Shida Number - for key in ('hl','kl','ll'): + for key in ('hl', 'kl', 'll'): setattr(self, key, d.get(key)) self.lmax = len(self.hl) - 1 # calculate spherical harmonic degree @@ -478,7 +505,7 @@ def to_dict(self): """ # retrieve each Load Love/Shida Number d = {} - for key in ('hl','kl','ll'): + for key in ('hl', 'kl', 'll'): d[key] = getattr(self, key) return d @@ -513,19 +540,19 @@ def transform(self, reference): """ # calculate isomorphic parameters for different reference frames # From Blewitt (2003), Wahr (1998), Trupin (1992) and Farrell (1972) - if (reference.upper() == 'CF'): + if reference.upper() == 'CF': # Center of Surface Figure - alpha = (self.hl[1] + 2.0*self.ll[1])/3.0 - elif (reference.upper() == 'CL'): + alpha = (self.hl[1] + 2.0 * self.ll[1]) / 3.0 + elif reference.upper() == 'CL': # Center of Surface Lateral Figure alpha = self.ll[1].copy() - elif (reference.upper() == 'CH'): + elif reference.upper() == 'CH': # Center of Surface Height Figure alpha = self.hl[1].copy() - elif (reference.upper() == 'CM'): + elif reference.upper() == 'CM': # Center of Mass of Earth System alpha = 1.0 - elif (reference.upper() == 'CE'): + elif reference.upper() == 'CE': # Center of Mass of Solid Earth alpha = 0.0 else: @@ -543,27 +570,24 @@ def update_dimensions(self): Update the dimensions of the ``love_numbers`` object """ # calculate spherical harmonic degree (0 is falsy) - self.l=np.arange(self.lmax+1) if (self.lmax is not None) else None + self.l = np.arange(self.lmax + 1) if (self.lmax is not None) else None return self def __str__(self): - """String representation of the ``love_numbers`` object - """ + """String representation of the ``love_numbers`` object""" properties = ['gravity_toolkit.love_numbers'] - properties.append(f" citation: {self.citation}") - properties.append(f" earth_model: {self.model}") - properties.append(f" max_degree: {self.lmax}") - properties.append(f" reference: {self.reference}") + properties.append(f' citation: {self.citation}') + properties.append(f' earth_model: {self.model}') + properties.append(f' max_degree: {self.lmax}') + properties.append(f' reference: {self.reference}') return '\n'.join(properties) def __len__(self): - """Number of degrees - """ + """Number of degrees""" return len(self.l) def __iter__(self): - """Iterate over load Love/Shida numbers variables - """ + """Iterate over load Love/Shida numbers variables""" yield self.hl yield self.kl yield self.ll diff --git a/gravity_toolkit/sea_level_equation.py b/gravity_toolkit/sea_level_equation.py index 1e1e3906..88a10ddb 100644 --- a/gravity_toolkit/sea_level_equation.py +++ b/gravity_toolkit/sea_level_equation.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" sea_level_equation.py (07/2026) Solves the sea level equation with the option of including polar motion feedback Uses a Clenshaw summation to calculate the spherical harmonic summation @@ -123,16 +123,33 @@ set the permissions mode of the output files with --mode Written 09/2016 """ + import logging import numpy as np from gravity_toolkit.gen_harmonics import gen_harmonics from gravity_toolkit.associated_legendre import plm_holmes from gravity_toolkit.units import units + # PURPOSE: Computes Sea Level Fingerprints including polar motion feedback -def sea_level_equation(loadClm, loadSlm, glon, glat, land_function, LMAX=0, - LOVE=None, BODY_TIDE_LOVE=0, FLUID_LOVE=0, DENSITY=1.0, POLAR=True, - ITERATIONS=6, PLM=None, FILL_VALUE=0, SCALE=1e-280, **kwargs): +def sea_level_equation( + loadClm, + loadSlm, + glon, + glat, + land_function, + LMAX=0, + LOVE=None, + BODY_TIDE_LOVE=0, + FLUID_LOVE=0, + DENSITY=1.0, + POLAR=True, + ITERATIONS=6, + PLM=None, + FILL_VALUE=0, + SCALE=1e-280, + **kwargs, +): r""" Solves the sea level equation with the option of including polar motion feedback :cite:p:`Farrell:1976hm,Kendall:2005ds,Mitrovica:2003cq` @@ -197,7 +214,7 @@ def sea_level_equation(loadClm, loadSlm, glon, glat, land_function, LMAX=0, # calculate ocean function from land function ocean_function = 1.0 - land_function # indices of the ocean function - ii,jj = np.nonzero(ocean_function) + ii, jj = np.nonzero(ocean_function) # extract arrays of kl, hl, and ll Love Numbers hl, kl, ll = LOVE @@ -211,86 +228,88 @@ def sea_level_equation(loadClm, loadSlm, glon, glat, land_function, LMAX=0, rad_e = factors.rad_e # different treatments of the body tide Love numbers of degree 2 - if isinstance(BODY_TIDE_LOVE,(list,tuple)): + if isinstance(BODY_TIDE_LOVE, (list, tuple)): # use custom defined values - k2b,h2b = BODY_TIDE_LOVE - elif (BODY_TIDE_LOVE == 0): + k2b, h2b = BODY_TIDE_LOVE + elif BODY_TIDE_LOVE == 0: # Wahr (1981) and Wahr (1985) values from PREM k2b = 0.298 h2b = 0.604 - elif (BODY_TIDE_LOVE == 1): + elif BODY_TIDE_LOVE == 1: # Farrell (1972) values from Gutenberg-Bullen oceanic mantle model k2b = 0.3055 h2b = 0.6149 # different treatments of the fluid Love number of gravitational potential - if isinstance(FLUID_LOVE,(list,tuple)): + if isinstance(FLUID_LOVE, (list, tuple)): # use custom defined value - klf, = FLUID_LOVE - elif (FLUID_LOVE == 0): + (klf,) = FLUID_LOVE + elif FLUID_LOVE == 0: # Han and Wahr (1989) fluid love number # klf = 3.0*G*(C-A)/(rad_e**5*omega**2) # klf = 3.0*G*H0*A/(rad_e**5*omega**2) - G = 6.6740e-11# gravitational constant [m^3/(kg*s^2)] - Re = 6.371e6# mean radius of the Earth [m] - A_moi = 8.0077e+37# mean equatorial moment of inertia [kg m^2] - omega = 7.292115e-5# mean rotation rate of the Earth [radians/s] - H0 = 0.00328475# dynamical ellipticity (C_moi-A_moi)/A_moi - klf = 3.0*G*H0*A_moi*(Re**-5)*(omega**-2) - klf = 0.00328475/0.00348118 - if (FLUID_LOVE == 1): + G = 6.6740e-11 # gravitational constant [m^3/(kg*s^2)] + Re = 6.371e6 # mean radius of the Earth [m] + A_moi = 8.0077e37 # mean equatorial moment of inertia [kg m^2] + omega = 7.292115e-5 # mean rotation rate of the Earth [radians/s] + H0 = 0.00328475 # dynamical ellipticity (C_moi-A_moi)/A_moi + klf = 3.0 * G * H0 * A_moi * (Re**-5) * (omega**-2) + klf = 0.00328475 / 0.00348118 + if FLUID_LOVE == 1: # Munk and MacDonald (1960) secular love number with IERS and PREM values - GM = 3.98004418e14# geocentric gravitational constant [m^3/s^2] - Re = 6.371e6# mean radius of the Earth [m] - omega = 7.292115e-5# mean rotation rate of the Earth [radians/s] - C_moi = 0.33068# reduced polar moment of inertia (C/Ma^2) - H = 1.0/305.51# precessional constant (C_moi-A_moi)/C_moi - klf = 3.0*GM*H*C_moi/(Re**3*omega**2) - elif (FLUID_LOVE == 2): + GM = 3.98004418e14 # geocentric gravitational constant [m^3/s^2] + Re = 6.371e6 # mean radius of the Earth [m] + omega = 7.292115e-5 # mean rotation rate of the Earth [radians/s] + C_moi = 0.33068 # reduced polar moment of inertia (C/Ma^2) + H = 1.0 / 305.51 # precessional constant (C_moi-A_moi)/C_moi + klf = 3.0 * GM * H * C_moi / (Re**3 * omega**2) + elif FLUID_LOVE == 2: # Munk and MacDonald (1960) fluid love number with IERS and WGS84 values - flat = 1.0/298.257223563# flattening of the WGS84 ellipsoid - Re = 6.371e6# mean radius of the Earth [m] - omega = 7.292115e-5# mean rotation rate of the Earth [radians/s] - ge = 9.80665# standard gravity (mean gravitational acceleration) [m/s^2] - klf = 2.0*flat*ge/(omega**2*Re) - 1.0 - elif (FLUID_LOVE == 3): + flat = 1.0 / 298.257223563 # flattening of the WGS84 ellipsoid + Re = 6.371e6 # mean radius of the Earth [m] + omega = 7.292115e-5 # mean rotation rate of the Earth [radians/s] + ge = 9.80665 # standard gravity (mean gravitational acceleration) [m/s^2] + klf = 2.0 * flat * ge / (omega**2 * Re) - 1.0 + elif FLUID_LOVE == 3: # Fluid love number from Lambeck (1980) # klf = 3.0*(C-A)*G/(omega**2*rad_e**5) = 3.0*GM*C20/(omega**2*rad_e**3) - G = 6.672e-11# gravitational constant [m^3/(kg*s^2)] - M = 5.974e+24# mass of the Earth [kg] - R = 6.378140e6# equatorial radius of the Earth [m] - Re = 6.3710121e6# mean radius of the Earth [m] - omega = 7.292115e-5# mean rotation rate of the Earth [radians/s] - A_moi = 0.3295*M*R**2# mean equatorial moment of inertia [kg m^2] - H = 0.003275# precessional constant (C_moi-A_moi)/C_moi - C_moi = -A_moi/(H-1.0)# mean polar moment of inertia [kg m^2] - klf = 3.0*(C_moi-A_moi)*G*(omega**-2)*(Re**-5) + G = 6.672e-11 # gravitational constant [m^3/(kg*s^2)] + M = 5.974e24 # mass of the Earth [kg] + R = 6.378140e6 # equatorial radius of the Earth [m] + Re = 6.3710121e6 # mean radius of the Earth [m] + omega = 7.292115e-5 # mean rotation rate of the Earth [radians/s] + A_moi = 0.3295 * M * R**2 # mean equatorial moment of inertia [kg m^2] + H = 0.003275 # precessional constant (C_moi-A_moi)/C_moi + C_moi = -A_moi / (H - 1.0) # mean polar moment of inertia [kg m^2] + klf = 3.0 * (C_moi - A_moi) * G * (omega**-2) * (Re**-5) klf = 0.942 # calculate coefh and coefp for each degree and order # see equation 11 from Tamisiea et al (2010) - coefh = np.zeros((LMAX+1, LMAX+1)) - coefp = np.zeros((LMAX+1, LMAX+1)) - for l in range(LMAX+1): - m = np.arange(0, l+1) + coefh = np.zeros((LMAX + 1, LMAX + 1)) + coefp = np.zeros((LMAX + 1, LMAX + 1)) + for l in range(LMAX + 1): + m = np.arange(0, l + 1) # tilt factor for degree l - gamma_l = (1.0 + kl[l] - hl[l]) + gamma_l = 1.0 + kl[l] - hl[l] # coefh and coefp will be the same for all orders except for degree 2 # and order 1 (if POLAR motion feedback is included) - coefh[l,m] = 3.0*rho_water*gamma_l/rho_e/np.float64(2*l+1) - coefp[l,m] = gamma_l/(kl[l] + 1.0) + coefh[l, m] = 3.0 * rho_water * gamma_l / rho_e / np.float64(2 * l + 1) + coefp[l, m] = gamma_l / (kl[l] + 1.0) # if degree 2 and POLAR parameter is set if (l == 2) and POLAR: # tilt factor for body tides - gamma_2b = (1.0 + k2b - h2b) + gamma_2b = 1.0 + k2b - h2b # calculate coefficient for polar motion feedback and add to coefs # For small perturbations in rotation vector: driving potential # will be dominated by degree two and order one polar wander # effects (quadrantal geometry effects) (Kendall et al., 2005) - coefpmf = gamma_2b*(1.0 + kl[l])/(klf - k2b) + coefpmf = gamma_2b * (1.0 + kl[l]) / (klf - k2b) # add effects of polar motion feedback to order 1 coefficients - coefh[l,1] += 3.0*rho_water*coefpmf/rho_e/np.float64(2*l+1) - coefp[l,1] += coefpmf/(kl[l] + 1.0) + coefh[l, 1] += ( + 3.0 * rho_water * coefpmf / rho_e / np.float64(2 * l + 1) + ) + coefp[l, 1] += coefpmf / (kl[l] + 1.0) # added option to precompute plms to improve computational speed if PLM is None: @@ -298,21 +317,21 @@ def sea_level_equation(loadClm, loadSlm, glon, glat, land_function, LMAX=0, PLM, dPLM = plm_holmes(LMAX, np.cos(th)) # calculate sin of colatitudes gth, gphi = np.meshgrid(th, phi) - u = np.sin(gth[ii,jj]) + u = np.sin(gth[ii, jj]) # indices of spherical harmonics for calculating eps - l1, m1 = np.tril_indices(LMAX+1) + l1, m1 = np.tril_indices(LMAX + 1) # total mass of the surface mass load [g] from harmonics - tmass = 4.0*np.pi*(rad_e**3.0)*rho_e*loadClm[0,0]/3.0 + tmass = 4.0 * np.pi * (rad_e**3.0) * rho_e * loadClm[0, 0] / 3.0 # convert ocean function into a series of spherical harmonics ocean_Ylms = gen_harmonics(ocean_function, glon, glat, LMAX=LMAX, PLM=PLM) # total area of ocean calculated by integrating the ocean function - ocean_area = 4.0*np.pi*ocean_Ylms.clm[0,0] + ocean_area = 4.0 * np.pi * ocean_Ylms.clm[0, 0] # uniform distribution as initial guess of the ocean change following # Mitrovica and Peltier (1991) doi:10.1029/91JB01284 # sea level height change - sea_height = -tmass/rho_water/rad_e**2/ocean_area + sea_height = -tmass / rho_water / rad_e**2 / ocean_area # if verbose output: print ocean area and uniform sea level height logging.info(f'Total Ocean Area: {ocean_area:0.10g}') @@ -321,9 +340,13 @@ def sea_level_equation(loadClm, loadSlm, glon, glat, land_function, LMAX=0, # allocate for output sea level field sea_level = np.empty((nphi, nth)) # complex load spherical harmonics - loadYlms = loadClm - 1j*loadSlm + loadYlms = loadClm - 1j * loadSlm # distribute sea height over ocean harmonics height_Ylms = ocean_Ylms * sea_height + # calculating cos(m*phi) and sin(m*phi) using Euler's formula + mm = np.arange(0, LMAX + 1) + m_phi = np.exp(1j * np.einsum('m...,p...->pm...', mm, phi)) + # iterate solutions until convergence or reaching total iterations n_iter = 1 # use maximum eps values from Mitrovica and Peltier (1991) @@ -332,47 +355,35 @@ def sea_level_equation(loadClm, loadSlm, glon, glat, land_function, LMAX=0, eps_max = 1e-4 while (eps > eps_max) and (n_iter <= ITERATIONS): # zero out the sea level field for this iteration - sea_level[:,:] = 0.0 + sea_level[:, :] = 0.0 # calculate combined spherical harmonics for Clenshaw summation - Ylm1 = coefh*height_Ylms.ilm + rad_e*coefp*loadYlms - # calculate clenshaw summations over colatitudes - cs_m = np.zeros((nth, LMAX+1), dtype=np.clongdouble) - for m in range(LMAX, -1, -1): - cs_m[:,m] = _clenshaw(np.cos(th), m, Ylm1, LMAX, SCALE=SCALE) - - # calculate cos(phi) - cos_phi_2 = 2.0*np.cos(phi) - # matrix of cos/sin m*phi summation - m_phi = np.zeros((nphi, LMAX+2), dtype=np.clongdouble) - # initialize matrix with values at lmax+1 and lmax - m_phi[:,LMAX+1] = np.exp(1j * (LMAX + 1) * phi) - m_phi[:,LMAX] = np.exp(1j * LMAX*phi) - # calculate summation - g = np.einsum("h...,p...->ph...", cs_m[:,LMAX], m_phi[:,LMAX]) - # discard imaginary component - s_m = g[ii,jj].real + Ylm1 = coefh * height_Ylms.ilm + rad_e * coefp * loadYlms + + # initate summation + s_m = 0.0 # iterate to calculate complete summation - for m in range(LMAX-1, 0, -1): + for m in range(LMAX, 0, -1): # calculate summation for order m - m_phi[:,m] = cos_phi_2*m_phi[:,m+1] - m_phi[:,m+2] - a_m = np.sqrt((2.0*m + 3.0)/(2.0*m + 2.0)) - g = np.einsum("h...,p...->ph...", cs_m[:,m], m_phi[:,m]) + a_m = np.sqrt((2.0 * m + 3.0) / (2.0 * m + 2.0)) + cs_m = _clenshaw(np.cos(th), m, Ylm1, LMAX, SCALE=SCALE) + g = np.einsum('h...,p...->ph...', cs_m, m_phi[:, m]) # update summation and discard imaginary component - s_m = a_m*u*s_m + g[ii,jj].real + s_m = a_m * u * s_m + g[ii, jj].real # add the l=0/m=0 term - gs_m = np.kron(np.ones((nphi, 1)), cs_m.real[:, 0]) + cs_m = _clenshaw(np.cos(th), 0, Ylm1, LMAX, SCALE=SCALE) + gs_m = np.kron(np.ones((nphi, 1)), cs_m.real) # calculate new sea level for iteration - sea_level[ii,jj] = np.sqrt(3.0)*u*s_m + gs_m[ii,jj] + sea_level[ii, jj] = np.sqrt(3.0) * u * s_m + gs_m[ii, jj] # calculate spherical harmonic field for iteration Ylms = gen_harmonics(sea_level, glon, glat, LMAX=LMAX, PLM=PLM) # total sea level height for iteration # integrated total rmass will differ as sea_level is only over ocean # whereas the crustal and gravitational effects are global - rmass = 4.0*np.pi*Ylms.clm[0,0] + rmass = 4.0 * np.pi * Ylms.clm[0, 0] # mass anomaly converted to ocean height to ensure mass conservation # (this is the gravitational perturbation (Delta Phi)/g) - sea_height = (-tmass/rho_water/rad_e**2 - rmass)/ocean_area + sea_height = (-tmass / rho_water / rad_e**2 - rmass) / ocean_area # if verbose output: print iteration, mass and anomaly for convergence logging.info(f'Iteration: {n_iter:d}') @@ -384,31 +395,32 @@ def sea_level_equation(loadClm, loadSlm, glon, glat, land_function, LMAX=0, # constrained by invoking conservation of mass of the surface load # Equation 48 of Mitrovica and Peltier (1991) # add difference to total sea level field to force mass conservation - sea_level += sea_height*ocean_function[:,:] + sea_level += sea_height * ocean_function[:, :] Ylms += ocean_Ylms * sea_height # calculate eps to determine if solution is appropriately converged mod1 = np.hypot(height_Ylms.clm, height_Ylms.slm) mod2 = np.hypot(Ylms.clm, Ylms.slm) - eps = np.abs(np.sum(mod2[l1,m1] - mod1[l1,m1])/np.sum(mod1[l1,m1])) + eps = np.abs(np.sum(mod2[l1, m1] - mod1[l1, m1]) / np.sum(mod1[l1, m1])) # save height harmonics for use in the next iteration height_Ylms = Ylms.copy() # add 1 to n_iter n_iter += 1 # calculate final total mass for sanity check - omass = 4.0*np.pi*(rad_e**2.0)*rho_water*height_Ylms.clm[0,0] + omass = 4.0 * np.pi * (rad_e**2.0) * rho_water * height_Ylms.clm[0, 0] # if verbose output: sanity check of masses - logging.info(f'Original Total Ocean Mass: {-tmass/1e15:0.10g}') - logging.info(f'Final Iterated Ocean Mass: {omass/1e15:0.10g}') + logging.info(f'Original Total Ocean Mass: {-tmass / 1e15:0.10g}') + logging.info(f'Final Iterated Ocean Mass: {omass / 1e15:0.10g}') # set final invalid points to fill value if applicable - if (FILL_VALUE != 0): - ii,jj = np.nonzero(land_function) - sea_level[ii,jj] = FILL_VALUE + if FILL_VALUE != 0: + ii, jj = np.nonzero(land_function) + sea_level[ii, jj] = FILL_VALUE # return the sea level spatial field return sea_level + # PURPOSE: compute Clenshaw summation of the fully normalized associated # Legendre's function for constant order m def _clenshaw(t, m, Ylm1, lmax, SCALE=1e-280): @@ -438,38 +450,66 @@ def _clenshaw(t, m, Ylm1, lmax, SCALE=1e-280): N = len(t) s_m = np.zeros((N), dtype=np.clongdouble) # scaling to prevent overflow - ylm = SCALE*Ylm1.astype(np.clongdouble) + ylm = SCALE * Ylm1.astype(np.clongdouble) # convert lmax and m to float lm = np.float64(lmax) mm = np.float64(m) - if (m == lmax): - s_m[:] = np.copy(ylm[lmax,lmax]) - elif (m == (lmax-1)): - a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/((lm-mm)*(lm+mm)))*t - s_m[:] = a_lm*ylm[lmax,lmax-1] + ylm[lmax-1,lmax-1] - elif ((m <= (lmax-2)) and (m >= 1)): - s_mm_minus_2 = np.copy(ylm[lmax,m]) - a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/((lm-mm)*(lm+mm)))*t - s_mm_minus_1 = a_lm*s_mm_minus_2 + ylm[lmax-1,m] - for l in range(lmax-2, m-1, -1): + if m == lmax: + s_m[:] = np.copy(ylm[lmax, lmax]) + elif m == (lmax - 1): + a_lm = ( + np.sqrt( + ((2.0 * lm - 1.0) * (2.0 * lm + 1.0)) / ((lm - mm) * (lm + mm)) + ) + * t + ) + s_m[:] = a_lm * ylm[lmax, lmax - 1] + ylm[lmax - 1, lmax - 1] + elif (m <= (lmax - 2)) and (m >= 1): + s_mm_minus_2 = np.copy(ylm[lmax, m]) + a_lm = ( + np.sqrt( + ((2.0 * lm - 1.0) * (2.0 * lm + 1.0)) / ((lm - mm) * (lm + mm)) + ) + * t + ) + s_mm_minus_1 = a_lm * s_mm_minus_2 + ylm[lmax - 1, m] + for l in range(lmax - 2, m - 1, -1): ll = np.float64(l) - a_lm=np.sqrt(((2.0*ll+1.0)*(2.0*ll+3.0))/((ll+1.0-mm)*(ll+1.0+mm)))*t - b_lm=np.sqrt(((2.*ll+5.)*(ll+mm+1.)*(ll-mm+1.))/((ll+2.-mm)*(ll+2.+mm)*(2.*ll+1.))) - s_mm_l = a_lm * s_mm_minus_1 - b_lm * s_mm_minus_2 + ylm[l,m] + a_lm = ( + np.sqrt( + ((2.0 * ll + 1.0) * (2.0 * ll + 3.0)) + / ((ll + 1.0 - mm) * (ll + 1.0 + mm)) + ) + * t + ) + b_lm = np.sqrt( + ((2.0 * ll + 5.0) * (ll + mm + 1.0) * (ll - mm + 1.0)) + / ((ll + 2.0 - mm) * (ll + 2.0 + mm) * (2.0 * ll + 1.0)) + ) + s_mm_l = a_lm * s_mm_minus_1 - b_lm * s_mm_minus_2 + ylm[l, m] s_mm_minus_2 = np.copy(s_mm_minus_1) s_mm_minus_1 = np.copy(s_mm_l) s_m[:] = np.copy(s_mm_l) - elif (m == 0): - s_mm_minus_2 = np.copy(ylm[lmax,0]) - a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/(lm*lm))*t - s_mm_minus_1 = a_lm * s_mm_minus_2 + ylm[lmax-1,0] - for l in range(lmax-2, m-1, -1): + elif m == 0: + s_mm_minus_2 = np.copy(ylm[lmax, 0]) + a_lm = np.sqrt(((2.0 * lm - 1.0) * (2.0 * lm + 1.0)) / (lm * lm)) * t + s_mm_minus_1 = a_lm * s_mm_minus_2 + ylm[lmax - 1, 0] + for l in range(lmax - 2, m - 1, -1): ll = np.float64(l) - a_lm=np.sqrt(((2.0*ll+1.0)*(2.0*ll+3.0))/((ll+1.0)*(ll+1.0)))*t - b_lm=np.sqrt(((2.0*ll+5.0)*(ll+1.0)*(ll+1.0))/((ll+2.0)*(ll+2.0)*(2.0*ll+1.0))) - s_mm_l = a_lm * s_mm_minus_1 - b_lm * s_mm_minus_2 + ylm[l,0] + a_lm = ( + np.sqrt( + ((2.0 * ll + 1.0) * (2.0 * ll + 3.0)) + / ((ll + 1.0) * (ll + 1.0)) + ) + * t + ) + b_lm = np.sqrt( + ((2.0 * ll + 5.0) * (ll + 1.0) * (ll + 1.0)) + / ((ll + 2.0) * (ll + 2.0) * (2.0 * ll + 1.0)) + ) + s_mm_l = a_lm * s_mm_minus_1 - b_lm * s_mm_minus_2 + ylm[l, 0] s_mm_minus_2 = np.copy(s_mm_minus_1) s_mm_minus_1 = np.copy(s_mm_l) s_m[:] = np.copy(s_mm_l) # return rescaled s_m - return s_m/SCALE + return s_m / SCALE diff --git a/gravity_toolkit/spatial.py b/gravity_toolkit/spatial.py index ded4d4bc..56539735 100644 --- a/gravity_toolkit/spatial.py +++ b/gravity_toolkit/spatial.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" spatial.py Written by Tyler Sutterley (10/2024) @@ -76,6 +76,7 @@ Updated 06/2020: added zeros_like() for creating an empty spatial object Written 06/2020 """ + import re import io import copy @@ -94,6 +95,7 @@ h5py = import_dependency('h5py') netCDF4 = import_dependency('netCDF4') + class spatial(object): """ Data class for reading, writing and processing spatial data @@ -120,20 +122,22 @@ class spatial(object): input or output filename """ + np.seterr(invalid='ignore') + def __init__(self, **kwargs): # set default keyword arguments - kwargs.setdefault('fill_value',None) + kwargs.setdefault('fill_value', None) # set default class attributes - self.data=None - self.mask=None - self.lon=None - self.lat=None - self.time=None - self.month=None - self.fill_value=kwargs['fill_value'] - self.attributes=dict() - self.filename=None + self.data = None + self.mask = None + self.lon = None + self.lat = None + self.time = None + self.month = None + self.fill_value = kwargs['fill_value'] + self.attributes = dict() + self.filename = None # iterator self.__index__ = 0 @@ -157,8 +161,11 @@ def case_insensitive_filename(self, filename): # check if file presently exists with input case if not self.filename.exists(): # search for filename without case dependence - f = [f.name for f in self.filename.parent.iterdir() if - re.match(self.filename.name, f.name, re.I)] + f = [ + f.name + for f in self.filename.parent.iterdir() + if re.match(self.filename.name, f.name, re.I) + ] if not f: msg = f'{filename} not found in file system' raise FileNotFoundError(msg) @@ -223,26 +230,26 @@ def from_ascii(self, filename, date=True, **kwargs): # set filename self.case_insensitive_filename(filename) # set default parameters - kwargs.setdefault('verbose',False) - kwargs.setdefault('compression',None) - kwargs.setdefault('spacing',[None,None]) - kwargs.setdefault('nlat',None) - kwargs.setdefault('nlon',None) - kwargs.setdefault('extent',[None]*4) - kwargs.setdefault('columns',['lon','lat','data','time']) - kwargs.setdefault('header',0) + kwargs.setdefault('verbose', False) + kwargs.setdefault('compression', None) + kwargs.setdefault('spacing', [None, None]) + kwargs.setdefault('nlat', None) + kwargs.setdefault('nlon', None) + kwargs.setdefault('extent', [None] * 4) + kwargs.setdefault('columns', ['lon', 'lat', 'data', 'time']) + kwargs.setdefault('header', 0) # open the ascii file and extract contents logging.info(str(self.filename)) - if (kwargs['compression'] == 'gzip'): + if kwargs['compression'] == 'gzip': # read input ascii data from gzip compressed file and split lines with gzip.open(self.filename, mode='r') as f: file_contents = f.read().decode('ISO-8859-1').splitlines() - elif (kwargs['compression'] == 'zip'): + elif kwargs['compression'] == 'zip': # read input ascii data from zipped file and split lines stem = self.filename.stem with zipfile.ZipFile(self.filename) as z: file_contents = z.read(stem).decode('ISO-8859-1').splitlines() - elif (kwargs['compression'] == 'bytes'): + elif kwargs['compression'] == 'bytes': # read input file object and split lines file_contents = self.filename.read().splitlines() else: @@ -265,7 +272,11 @@ def from_ascii(self, filename, date=True, **kwargs): dlon, dlat = kwargs.get('spacing') self.lat = np.arange(extent[3], extent[2] - dlat, dlat) self.lon = np.arange(extent[0], extent[1] + dlon, dlon) - elif kwargs['nlat'] and kwargs['nlon'] and (None not in kwargs['spacing']): + elif ( + kwargs['nlat'] + and kwargs['nlon'] + and (None not in kwargs['spacing']) + ): dlon, dlat = kwargs.get('spacing') self.lat = np.zeros((kwargs['nlat'])) self.lon = np.zeros((kwargs['nlon'])) @@ -285,17 +296,20 @@ def from_ascii(self, filename, date=True, **kwargs): for line in file_contents[header:]: # extract columns of interest and assign to dict # convert fortran exponentials if applicable - d = {c:r.replace('D','E') for c,r in zip(columns,rx.findall(line))} + d = { + c: r.replace('D', 'E') + for c, r in zip(columns, rx.findall(line)) + } # convert line coordinates to integers - ilon = np.int64(np.float64(d['lon'])/dlon) - ilat = np.int64((90.0 - np.float64(d['lat']))//dlat) + ilon = np.int64(np.float64(d['lon']) / dlon) + ilat = np.int64((90.0 - np.float64(d['lat'])) // dlat) self.data[ilat, ilon] = np.float64(d['data']) self.mask[ilat, ilon] = False self.lon[ilon] = np.float64(d['lon']) self.lat[ilat] = np.float64(d['lat']) # if the ascii file contains date variables if date: - self.time = np.array(d['time'],dtype='f') + self.time = np.array(d['time'], dtype='f') self.month = calendar_to_grace(self.time) # if the ascii file contains date variables if date: @@ -337,32 +351,34 @@ def from_netCDF4(self, filename, **kwargs): # set filename self.case_insensitive_filename(filename) # set default parameters - kwargs.setdefault('date',True) - kwargs.setdefault('compression',None) - kwargs.setdefault('varname','z') - kwargs.setdefault('lonname','lon') - kwargs.setdefault('latname','lat') - kwargs.setdefault('timename','time') - kwargs.setdefault('field_mapping',{}) - kwargs.setdefault('verbose',False) + kwargs.setdefault('date', True) + kwargs.setdefault('compression', None) + kwargs.setdefault('varname', 'z') + kwargs.setdefault('lonname', 'lon') + kwargs.setdefault('latname', 'lat') + kwargs.setdefault('timename', 'time') + kwargs.setdefault('field_mapping', {}) + kwargs.setdefault('verbose', False) # Open the NetCDF4 file for reading - if (kwargs['compression'] == 'gzip'): + if kwargs['compression'] == 'gzip': # read as in-memory (diskless) netCDF4 dataset with gzip.open(self.filename, mode='r') as f: fileID = netCDF4.Dataset(uuid.uuid4().hex, memory=f.read()) - elif (kwargs['compression'] == 'zip'): + elif kwargs['compression'] == 'zip': # read zipped file and extract file into in-memory file object stem = self.filename.stem with zipfile.ZipFile(self.filename) as z: # first try finding a netCDF4 file with same base filename # if none found simply try searching for a netCDF4 file try: - f,=[f for f in z.namelist() if re.match(stem,f,re.I)] + (f,) = [f for f in z.namelist() if re.match(stem, f, re.I)] except: - f,=[f for f in z.namelist() if re.search(r'\.nc(4)?$',f)] + (f,) = [ + f for f in z.namelist() if re.search(r'\.nc(4)?$', f) + ] # read bytes from zipfile as in-memory (diskless) netCDF4 dataset fileID = netCDF4.Dataset(uuid.uuid4().hex, memory=z.read(f)) - elif (kwargs['compression'] == 'bytes'): + elif kwargs['compression'] == 'bytes': # read as in-memory (diskless) netCDF4 dataset fileID = netCDF4.Dataset(uuid.uuid4().hex, memory=filename.read()) else: @@ -374,16 +390,23 @@ def from_netCDF4(self, filename, **kwargs): # set automasking fileID.set_auto_mask(False) # list of variable attributes - attributes_list = ['description','units','long_name','calendar', - 'standard_name','_FillValue','missing_value'] + attributes_list = [ + 'description', + 'units', + 'long_name', + 'calendar', + 'standard_name', + '_FillValue', + 'missing_value', + ] # mapping between output keys and netCDF4 variable names if not kwargs['field_mapping']: - fields = [kwargs['lonname'],kwargs['latname'],kwargs['varname']] + fields = [kwargs['lonname'], kwargs['latname'], kwargs['varname']] if kwargs['date']: fields.append(kwargs['timename']) kwargs['field_mapping'] = self.default_field_mapping(fields) # for each variable - for field,key in kwargs['field_mapping'].items(): + for field, key in kwargs['field_mapping'].items(): # Getting the data from each NetCDF variable # remove singleton dimensions setattr(self, field, np.squeeze(fileID.variables[key][:])) @@ -392,9 +415,10 @@ def from_netCDF4(self, filename, **kwargs): for attr in attributes_list: # try getting the attribute try: - self.attributes[field][attr] = \ - fileID.variables[key].getncattr(attr) - except (KeyError,ValueError,AttributeError): + self.attributes[field][attr] = fileID.variables[ + key + ].getncattr(attr) + except (KeyError, ValueError, AttributeError): pass # get global netCDF4 attributes self.attributes['ROOT'] = {} @@ -409,7 +433,7 @@ def from_netCDF4(self, filename, **kwargs): # set fill value and mask if '_FillValue' in self.attributes['data'].keys(): self.fill_value = self.attributes['data']['_FillValue'] - self.mask = (self.data == self.fill_value) + self.mask = self.data == self.fill_value else: self.mask = np.zeros(self.data.shape, dtype=bool) # set GRACE/GRACE-FO month if file has date variables @@ -453,16 +477,16 @@ def from_HDF5(self, filename, **kwargs): # set filename self.case_insensitive_filename(filename) # set default parameters - kwargs.setdefault('date',True) - kwargs.setdefault('compression',None) - kwargs.setdefault('varname','z') - kwargs.setdefault('lonname','lon') - kwargs.setdefault('latname','lat') - kwargs.setdefault('timename','time') - kwargs.setdefault('field_mapping',{}) - kwargs.setdefault('verbose',False) + kwargs.setdefault('date', True) + kwargs.setdefault('compression', None) + kwargs.setdefault('varname', 'z') + kwargs.setdefault('lonname', 'lon') + kwargs.setdefault('latname', 'lat') + kwargs.setdefault('timename', 'time') + kwargs.setdefault('field_mapping', {}) + kwargs.setdefault('verbose', False) # Open the HDF5 file for reading - if (kwargs['compression'] == 'gzip'): + if kwargs['compression'] == 'gzip': # read gzip compressed file and extract into in-memory file object with gzip.open(self.filename, mode='r') as f: fid = io.BytesIO(f.read()) @@ -472,16 +496,20 @@ def from_HDF5(self, filename, **kwargs): fid.seek(0) # read as in-memory (diskless) HDF5 dataset from BytesIO object fileID = h5py.File(fid, 'r') - elif (kwargs['compression'] == 'zip'): + elif kwargs['compression'] == 'zip': # read zipped file and extract file into in-memory file object stem = self.filename.stem with zipfile.ZipFile(self.filename) as z: # first try finding a HDF5 file with same base filename # if none found simply try searching for a HDF5 file try: - f,=[f for f in z.namelist() if re.match(stem,f,re.I)] + (f,) = [f for f in z.namelist() if re.match(stem, f, re.I)] except: - f,=[f for f in z.namelist() if re.search(r'\.H(DF)?5$',f,re.I)] + (f,) = [ + f + for f in z.namelist() + if re.search(r'\.H(DF)?5$', f, re.I) + ] # read bytes from zipfile into in-memory BytesIO object fid = io.BytesIO(z.read(f)) # set filename of BytesIO object @@ -490,7 +518,7 @@ def from_HDF5(self, filename, **kwargs): fid.seek(0) # read as in-memory (diskless) HDF5 dataset from BytesIO object fileID = h5py.File(fid, mode='r') - elif (kwargs['compression'] == 'bytes'): + elif kwargs['compression'] == 'bytes': # read as in-memory (diskless) HDF5 dataset fileID = h5py.File(filename, mode='r') else: @@ -500,16 +528,23 @@ def from_HDF5(self, filename, **kwargs): logging.info(fileID.filename) logging.info(list(fileID.keys())) # list of variable attributes - attributes_list = ['description','units','long_name','calendar', - 'standard_name','_FillValue','missing_value'] + attributes_list = [ + 'description', + 'units', + 'long_name', + 'calendar', + 'standard_name', + '_FillValue', + 'missing_value', + ] # mapping between output keys and HDF5 variable names if not kwargs['field_mapping']: - fields = [kwargs['lonname'],kwargs['latname'],kwargs['varname']] + fields = [kwargs['lonname'], kwargs['latname'], kwargs['varname']] if kwargs['date']: fields.append(kwargs['timename']) kwargs['field_mapping'] = self.default_field_mapping(fields) # for each variable - for field,key in kwargs['field_mapping'].items(): + for field, key in kwargs['field_mapping'].items(): # Getting the data from each HDF5 variable # remove singleton dimensions setattr(self, field, np.squeeze(fileID[key][:])) @@ -522,7 +557,7 @@ def from_HDF5(self, filename, **kwargs): pass # get global HDF5 attributes self.attributes['ROOT'] = {} - for att_name,att_val in fileID.attrs.items(): + for att_name, att_val in fileID.attrs.items(): self.attributes['ROOT'][att_name] = att_val # Closing the HDF5 file fileID.close() @@ -533,7 +568,7 @@ def from_HDF5(self, filename, **kwargs): # set fill value and mask if '_FillValue' in self.attributes['data'].keys(): self.fill_value = self.attributes['data']['_FillValue'] - self.mask = (self.data == self.fill_value) + self.mask = self.data == self.fill_value else: self.mask = np.zeros(self.data.shape, dtype=bool) # set GRACE/GRACE-FO month if file has date variables @@ -568,9 +603,9 @@ def from_index(self, filename, **kwargs): keyword arguments for input readers """ # set default keyword arguments - kwargs.setdefault('format',None) - kwargs.setdefault('date',True) - kwargs.setdefault('sort',True) + kwargs.setdefault('format', None) + kwargs.setdefault('date', True) + kwargs.setdefault('sort', True) # set filename self.case_insensitive_filename(filename) # file parser for reading index files @@ -583,18 +618,18 @@ def from_index(self, filename, **kwargs): # create a list of spatial objects s = [] # for each file in the index - for i,f in enumerate(file_list): - if (kwargs['format'] == 'ascii'): + for i, f in enumerate(file_list): + if kwargs['format'] == 'ascii': # netcdf (.nc) s.append(spatial().from_ascii(f, **kwargs)) - elif (kwargs['format'] == 'netCDF4'): + elif kwargs['format'] == 'netCDF4': # netcdf (.nc) s.append(spatial().from_netCDF4(f, **kwargs)) - elif (kwargs['format'] == 'HDF5'): + elif kwargs['format'] == 'HDF5': # HDF5 (.H5) s.append(spatial().from_HDF5(f, **kwargs)) # create a single spatial object from the list - return self.from_list(s,date=kwargs['date'],sort=kwargs['sort']) + return self.from_list(s, date=kwargs['date'], sort=kwargs['sort']) def from_list(self, object_list, **kwargs): """ @@ -613,21 +648,21 @@ def from_list(self, object_list, **kwargs): clear the list of ``spatial`` objects from memory """ # set default keyword arguments - kwargs.setdefault('date',True) - kwargs.setdefault('sort',True) - kwargs.setdefault('clear',False) + kwargs.setdefault('date', True) + kwargs.setdefault('sort', True) + kwargs.setdefault('clear', False) # number of spatial objects in list n = len(object_list) # indices to sort data objects if spatial list contain dates if kwargs['date'] and kwargs['sort']: - list_sort = np.argsort([d.time for d in object_list],axis=None) + list_sort = np.argsort([d.time for d in object_list], axis=None) else: list_sort = np.arange(n) # extract grid spacing shape = object_list[0].shape # create output spatial grid and mask self.data = np.zeros((shape[0], shape[1], n)) - self.mask = np.zeros((shape[0], shape[1], n),dtype=bool) + self.mask = np.zeros((shape[0], shape[1], n), dtype=bool) # add error if in original list attributes if hasattr(object_list[0], 'error'): self.error = np.zeros((shape[0], shape[1], n)) @@ -640,13 +675,13 @@ def from_list(self, object_list, **kwargs): # output dates if kwargs['date']: self.time = np.zeros((n)) - self.month = np.zeros((n),dtype=np.int64) + self.month = np.zeros((n), dtype=np.int64) # for each indice - for t,i in enumerate(list_sort): - self.data[:,:,t] = object_list[i].data[:,:].copy() - self.mask[:,:,t] |= object_list[i].mask[:,:] + for t, i in enumerate(list_sort): + self.data[:, :, t] = object_list[i].data[:, :].copy() + self.mask[:, :, t] |= object_list[i].mask[:, :] if hasattr(object_list[i], 'error'): - self.error[:,:,t] = object_list[i].error[:,:].copy() + self.error[:, :, t] = object_list[i].error[:, :].copy() if kwargs['date']: self.time[t] = np.atleast_1d(object_list[i].time) self.month[t] = np.atleast_1d(object_list[i].month) @@ -691,15 +726,15 @@ def from_file(self, filename, format=None, date=True, **kwargs): # set filename self.case_insensitive_filename(filename) # set default verbosity - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) # read from file - if (format == 'ascii'): + if format == 'ascii': # ascii (.txt) return spatial().from_ascii(filename, date=date, **kwargs) - elif (format == 'netCDF4'): + elif format == 'netCDF4': # netcdf (.nc) return spatial().from_netCDF4(filename, date=date, **kwargs) - elif (format == 'HDF5'): + elif format == 'HDF5': # HDF5 (.H5) return spatial().from_HDF5(filename, date=date, **kwargs) @@ -713,7 +748,15 @@ def from_dict(self, d, **kwargs): dictionary object to be converted """ # assign variables to self - for key in ['lon','lat','data','error','time','month','directory']: + for key in [ + 'lon', + 'lat', + 'data', + 'error', + 'time', + 'month', + 'directory', + ]: try: setattr(self, key, d[key].copy()) except (AttributeError, KeyError): @@ -743,29 +786,37 @@ def to_ascii(self, filename, **kwargs): """ self.filename = pathlib.Path(filename).expanduser().absolute() # set default verbosity and parameters - kwargs.setdefault('date',True) - kwargs.setdefault('verbose',False) + kwargs.setdefault('date', True) + kwargs.setdefault('verbose', False) logging.info(str(self.filename)) # open the output file fid = self.filename.open(mode='w', encoding='utf8') file_format = '{0:10.4f} {1:10.4f} ' float_format = kwargs.get('float_format', '12.4f') if hasattr(self, 'error') and kwargs['date']: - file_format += ' '.join([f'{{{i}:' + float_format + '}' for i in (2,3,4)]) + file_format += ' '.join( + [f'{{{i}:' + float_format + '}' for i in (2, 3, 4)] + ) elif hasattr(self, 'error'): - file_format += ' '.join([f'{{{i}:' + float_format + '}' for i in (2,3)]) + file_format += ' '.join( + [f'{{{i}:' + float_format + '}' for i in (2, 3)] + ) elif kwargs['date']: - file_format += ' '.join([f'{{{i}:' + float_format + '}' for i in (2,4)]) + file_format += ' '.join( + [f'{{{i}:' + float_format + '}' for i in (2, 4)] + ) else: - file_format += ' '.join([f'{{{i}:' + float_format + '}' for i in (2,)]) + file_format += ' '.join( + [f'{{{i}:' + float_format + '}' for i in (2,)] + ) # write to file for each valid latitude and longitude - ii,jj = np.nonzero((self.data != self.fill_value) & (~self.mask)) - for i,j in zip(ii,jj): + ii, jj = np.nonzero((self.data != self.fill_value) & (~self.mask)) + for i, j in zip(ii, jj): ln = self.lon[j] lt = self.lat[i] - data = self.data[i,j] - error = self.error[i,j] if hasattr(self, 'error') else 0.0 - print(file_format.format(ln,lt,data,error,self.time), file=fid) + data = self.data[i, j] + error = self.error[i, j] if hasattr(self, 'error') else 0.0 + print(file_format.format(ln, lt, data, error, self.time), file=fid) # close the output file fid.close() @@ -809,53 +860,72 @@ def to_netCDF4(self, filename, **kwargs): Output file and variable information """ # set default verbosity and parameters - kwargs.setdefault('verbose',False) - kwargs.setdefault('varname','z') - kwargs.setdefault('lonname','lon') - kwargs.setdefault('latname','lat') - kwargs.setdefault('timename','time') - kwargs.setdefault('field_mapping',{}) + kwargs.setdefault('verbose', False) + kwargs.setdefault('varname', 'z') + kwargs.setdefault('lonname', 'lon') + kwargs.setdefault('latname', 'lat') + kwargs.setdefault('timename', 'time') + kwargs.setdefault('field_mapping', {}) attributes = self.attributes.get('ROOT') or {} - kwargs.setdefault('attributes',dict(ROOT=attributes)) - kwargs.setdefault('units',None) - kwargs.setdefault('longname',None) - kwargs.setdefault('time_units','years') - kwargs.setdefault('time_longname','Date_in_Decimal_Years') - kwargs.setdefault('title',None) - kwargs.setdefault('source',None) - kwargs.setdefault('reference',None) - kwargs.setdefault('date',True) - kwargs.setdefault('clobber',True) - kwargs.setdefault('verbose',False) + kwargs.setdefault('attributes', dict(ROOT=attributes)) + kwargs.setdefault('units', None) + kwargs.setdefault('longname', None) + kwargs.setdefault('time_units', 'years') + kwargs.setdefault('time_longname', 'Date_in_Decimal_Years') + kwargs.setdefault('title', None) + kwargs.setdefault('source', None) + kwargs.setdefault('reference', None) + kwargs.setdefault('date', True) + kwargs.setdefault('clobber', True) + kwargs.setdefault('verbose', False) # setting NetCDF clobber attribute clobber = 'w' if kwargs['clobber'] else 'a' # opening NetCDF file for writing self.filename = pathlib.Path(filename).expanduser().absolute() - fileID = netCDF4.Dataset(self.filename, clobber, format="NETCDF4") + fileID = netCDF4.Dataset(self.filename, clobber, format='NETCDF4') # mapping between output keys and netCDF4 variable names if not kwargs['field_mapping']: - fields = [kwargs['lonname'],kwargs['latname'],kwargs['varname']] + fields = [kwargs['lonname'], kwargs['latname'], kwargs['varname']] if kwargs['date']: fields.append(kwargs['timename']) kwargs['field_mapping'] = self.default_field_mapping(fields) # create attributes dictionary for output variables - if not all(key in kwargs['attributes'] for key in kwargs['field_mapping'].values()): + if not all( + key in kwargs['attributes'] + for key in kwargs['field_mapping'].values() + ): # Defining attributes for longitude and latitude kwargs['attributes'][kwargs['field_mapping']['lon']] = {} - kwargs['attributes'][kwargs['field_mapping']['lon']]['long_name'] = 'longitude' - kwargs['attributes'][kwargs['field_mapping']['lon']]['units'] = 'degrees_east' + kwargs['attributes'][kwargs['field_mapping']['lon']][ + 'long_name' + ] = 'longitude' + kwargs['attributes'][kwargs['field_mapping']['lon']]['units'] = ( + 'degrees_east' + ) kwargs['attributes'][kwargs['field_mapping']['lat']] = {} - kwargs['attributes'][kwargs['field_mapping']['lat']]['long_name'] = 'latitude' - kwargs['attributes'][kwargs['field_mapping']['lat']]['units'] = 'degrees_north' + kwargs['attributes'][kwargs['field_mapping']['lat']][ + 'long_name' + ] = 'latitude' + kwargs['attributes'][kwargs['field_mapping']['lat']]['units'] = ( + 'degrees_north' + ) # Defining attributes for dataset kwargs['attributes'][kwargs['field_mapping']['data']] = {} - kwargs['attributes'][kwargs['field_mapping']['data']]['long_name'] = kwargs['longname'] - kwargs['attributes'][kwargs['field_mapping']['data']]['units'] = kwargs['units'] + kwargs['attributes'][kwargs['field_mapping']['data']][ + 'long_name' + ] = kwargs['longname'] + kwargs['attributes'][kwargs['field_mapping']['data']]['units'] = ( + kwargs['units'] + ) # Defining attributes for date if applicable if kwargs['date']: kwargs['attributes'][kwargs['field_mapping']['time']] = {} - kwargs['attributes'][kwargs['field_mapping']['time']]['long_name'] = kwargs['time_longname'] - kwargs['attributes'][kwargs['field_mapping']['time']]['units'] = kwargs['time_units'] + kwargs['attributes'][kwargs['field_mapping']['time']][ + 'long_name' + ] = kwargs['time_longname'] + kwargs['attributes'][kwargs['field_mapping']['time']][ + 'units' + ] = kwargs['time_units'] # add default global (file-level) attributes if kwargs['title']: kwargs['attributes']['ROOT']['title'] = kwargs['title'] @@ -875,38 +945,48 @@ def to_netCDF4(self, filename, **kwargs): # defining the NetCDF dimensions and variables nc = {} # NetCDF dimensions - for i,field in enumerate(dimensions): - temp = getattr(self,field) + for i, field in enumerate(dimensions): + temp = getattr(self, field) key = kwargs['field_mapping'][field] fileID.createDimension(key, len(temp)) nc[key] = fileID.createVariable(key, temp.dtype, (key,)) # NetCDF spatial data variables = set(kwargs['field_mapping'].keys()) - set(dimensions) for field in sorted(variables): - temp = getattr(self,field) + temp = getattr(self, field) ndim = temp.ndim key = kwargs['field_mapping'][field] - nc[key] = fileID.createVariable(key, temp.dtype, dims[:ndim], - fill_value=self.fill_value, zlib=True) + nc[key] = fileID.createVariable( + key, + temp.dtype, + dims[:ndim], + fill_value=self.fill_value, + zlib=True, + ) # filling NetCDF variables - for field,key in kwargs['field_mapping'].items(): - nc[key][:] = getattr(self,field) + for field, key in kwargs['field_mapping'].items(): + nc[key][:] = getattr(self, field) # filling netCDF dataset attributes - for att_name,att_val in kwargs['attributes'][key].items(): + for att_name, att_val in kwargs['attributes'][key].items(): # skip variable attribute if None if not att_val: continue # skip variable attributes if in list - if att_name not in ('DIMENSION_LIST','CLASS','NAME','_FillValue'): + if att_name not in ( + 'DIMENSION_LIST', + 'CLASS', + 'NAME', + '_FillValue', + ): nc[key].setncattr(att_name, att_val) # global attributes of NetCDF4 file - for att_name,att_val in kwargs['attributes']['ROOT'].items(): + for att_name, att_val in kwargs['attributes']['ROOT'].items(): fileID.setncattr(att_name, att_val) # add software information fileID.software_reference = gravity_toolkit.version.project_name fileID.software_version = gravity_toolkit.version.full_version # date created - fileID.date_created = time.strftime('%Y-%m-%d',time.localtime()) + fileID.date_created = time.strftime('%Y-%m-%d', time.localtime()) # Output NetCDF structure information logging.info(str(self.filename)) logging.info(list(fileID.variables.keys())) @@ -953,24 +1033,24 @@ def to_HDF5(self, filename, **kwargs): Output file and variable information """ # set default verbosity and parameters - kwargs.setdefault('verbose',False) - kwargs.setdefault('varname','z') - kwargs.setdefault('lonname','lon') - kwargs.setdefault('latname','lat') - kwargs.setdefault('timename','time') - kwargs.setdefault('field_mapping',{}) + kwargs.setdefault('verbose', False) + kwargs.setdefault('varname', 'z') + kwargs.setdefault('lonname', 'lon') + kwargs.setdefault('latname', 'lat') + kwargs.setdefault('timename', 'time') + kwargs.setdefault('field_mapping', {}) attributes = self.attributes.get('ROOT') or {} - kwargs.setdefault('attributes',dict(ROOT=attributes)) - kwargs.setdefault('units',None) - kwargs.setdefault('longname',None) - kwargs.setdefault('time_units','years') - kwargs.setdefault('time_longname','Date_in_Decimal_Years') - kwargs.setdefault('title',None) - kwargs.setdefault('source',None) - kwargs.setdefault('reference',None) - kwargs.setdefault('date',True) - kwargs.setdefault('clobber',True) - kwargs.setdefault('verbose',False) + kwargs.setdefault('attributes', dict(ROOT=attributes)) + kwargs.setdefault('units', None) + kwargs.setdefault('longname', None) + kwargs.setdefault('time_units', 'years') + kwargs.setdefault('time_longname', 'Date_in_Decimal_Years') + kwargs.setdefault('title', None) + kwargs.setdefault('source', None) + kwargs.setdefault('reference', None) + kwargs.setdefault('date', True) + kwargs.setdefault('clobber', True) + kwargs.setdefault('verbose', False) # setting NetCDF clobber attribute clobber = 'w' if kwargs['clobber'] else 'w-' # opening NetCDF file for writing @@ -978,28 +1058,47 @@ def to_HDF5(self, filename, **kwargs): fileID = h5py.File(self.filename, clobber) # mapping between output keys and HDF5 variable names if not kwargs['field_mapping']: - fields = [kwargs['lonname'],kwargs['latname'],kwargs['varname']] + fields = [kwargs['lonname'], kwargs['latname'], kwargs['varname']] if kwargs['date']: fields.append(kwargs['timename']) kwargs['field_mapping'] = self.default_field_mapping(fields) # create attributes dictionary for output variables - if not all(key in kwargs['attributes'] for key in kwargs['field_mapping'].values()): + if not all( + key in kwargs['attributes'] + for key in kwargs['field_mapping'].values() + ): # Defining attributes for longitude and latitude kwargs['attributes'][kwargs['field_mapping']['lon']] = {} - kwargs['attributes'][kwargs['field_mapping']['lon']]['long_name'] = 'longitude' - kwargs['attributes'][kwargs['field_mapping']['lon']]['units'] = 'degrees_east' + kwargs['attributes'][kwargs['field_mapping']['lon']][ + 'long_name' + ] = 'longitude' + kwargs['attributes'][kwargs['field_mapping']['lon']]['units'] = ( + 'degrees_east' + ) kwargs['attributes'][kwargs['field_mapping']['lat']] = {} - kwargs['attributes'][kwargs['field_mapping']['lat']]['long_name'] = 'latitude' - kwargs['attributes'][kwargs['field_mapping']['lat']]['units'] = 'degrees_north' + kwargs['attributes'][kwargs['field_mapping']['lat']][ + 'long_name' + ] = 'latitude' + kwargs['attributes'][kwargs['field_mapping']['lat']]['units'] = ( + 'degrees_north' + ) # Defining attributes for dataset kwargs['attributes'][kwargs['field_mapping']['data']] = {} - kwargs['attributes'][kwargs['field_mapping']['data']]['long_name'] = kwargs['longname'] - kwargs['attributes'][kwargs['field_mapping']['data']]['units'] = kwargs['units'] + kwargs['attributes'][kwargs['field_mapping']['data']][ + 'long_name' + ] = kwargs['longname'] + kwargs['attributes'][kwargs['field_mapping']['data']]['units'] = ( + kwargs['units'] + ) # Defining attributes for date if applicable if kwargs['date']: kwargs['attributes'][kwargs['field_mapping']['time']] = {} - kwargs['attributes'][kwargs['field_mapping']['time']]['long_name'] = kwargs['time_longname'] - kwargs['attributes'][kwargs['field_mapping']['time']]['units'] = kwargs['time_units'] + kwargs['attributes'][kwargs['field_mapping']['time']][ + 'long_name' + ] = kwargs['time_longname'] + kwargs['attributes'][kwargs['field_mapping']['time']][ + 'units' + ] = kwargs['time_units'] # add default global (file-level) attributes if kwargs['title']: kwargs['attributes']['ROOT']['title'] = kwargs['title'] @@ -1018,37 +1117,42 @@ def to_HDF5(self, filename, **kwargs): dims = tuple(kwargs['field_mapping'][key] for key in dimensions) # Defining the HDF5 dataset variables h5 = {} - for field,key in kwargs['field_mapping'].items(): - temp = getattr(self,field) + for field, key in kwargs['field_mapping'].items(): + temp = getattr(self, field) key = kwargs['field_mapping'][field] - h5[key] = fileID.create_dataset(key, temp.shape, - data=temp, dtype=temp.dtype, compression='gzip') + h5[key] = fileID.create_dataset( + key, temp.shape, data=temp, dtype=temp.dtype, compression='gzip' + ) # filling HDF5 dataset attributes - for att_name,att_val in kwargs['attributes'][key].items(): + for att_name, att_val in kwargs['attributes'][key].items(): # skip variable attribute if None if not att_val: continue # skip variable attributes if in list - if att_name not in ('DIMENSION_LIST','CLASS','NAME'): + if att_name not in ('DIMENSION_LIST', 'CLASS', 'NAME'): h5[key].attrs[att_name] = att_val # add dimensions variables = set(kwargs['field_mapping'].keys()) - set(dimensions) for field in sorted(variables): key = kwargs['field_mapping'][field] - for i,dim in enumerate(dims): + for i, dim in enumerate(dims): h5[key].dims[i].label = dim h5[key].dims[i].attach_scale(h5[dim]) # Dataset contains missing values - if (self.fill_value is not None): + if self.fill_value is not None: h5[key].attrs['_FillValue'] = self.fill_value # global attributes of HDF5 file - for att_name,att_val in kwargs['attributes']['ROOT'].items(): + for att_name, att_val in kwargs['attributes']['ROOT'].items(): fileID.attrs[att_name] = att_val # add software information - fileID.attrs['software_reference'] = gravity_toolkit.version.project_name + fileID.attrs['software_reference'] = ( + gravity_toolkit.version.project_name + ) fileID.attrs['software_version'] = gravity_toolkit.version.full_version # date created - fileID.attrs['date_created'] = time.strftime('%Y-%m-%d',time.localtime()) + fileID.attrs['date_created'] = time.strftime( + '%Y-%m-%d', time.localtime() + ) # Output HDF5 structure information logging.info(str(self.filename)) logging.info(list(fileID.keys())) @@ -1082,21 +1186,21 @@ def to_index(self, filename, file_list, format=None, date=True, **kwargs): self.filename = pathlib.Path(filename).expanduser().absolute() fid = self.filename.open(mode='w', encoding='utf8') # set default verbosity - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) # for each file to be in the index - for i,f in enumerate(file_list): + for i, f in enumerate(file_list): # print filename to index print(self.compressuser(f), file=fid) # index spatial object at i s = self.index(i, date=date) # write to file - if (format == 'ascii'): + if format == 'ascii': # ascii (.txt) s.to_ascii(f, date=date, **kwargs) - elif (format == 'netCDF4'): + elif format == 'netCDF4': # netcdf (.nc) s.to_netCDF4(f, date=date, **kwargs) - elif (format == 'HDF5'): + elif format == 'HDF5': # HDF5 (.H5) s.to_HDF5(f, date=date, **kwargs) # close the index file @@ -1124,15 +1228,15 @@ def to_file(self, filename, format=None, date=True, **kwargs): keyword arguments for output writers """ # set default verbosity - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) # write to file - if (format == 'ascii'): + if format == 'ascii': # ascii (.txt) self.to_ascii(filename, date=date, **kwargs) - elif (format == 'netCDF4'): + elif format == 'netCDF4': # netcdf (.nc) self.to_netCDF4(filename, date=date, **kwargs) - elif (format == 'HDF5'): + elif format == 'HDF5': # HDF5 (.H5) self.to_HDF5(filename, date=date, **kwargs) @@ -1170,15 +1274,16 @@ def to_masked_array(self): """ Convert a ``spatial`` object to a masked numpy array """ - return np.ma.array(self.data, mask=self.mask, - fill_value=self.fill_value) + return np.ma.array( + self.data, mask=self.mask, fill_value=self.fill_value + ) def update_mask(self): """ Update the mask of the ``spatial`` object """ if self.fill_value is not None: - self.mask |= (self.data == self.fill_value) + self.mask |= self.data == self.fill_value self.mask |= np.isnan(self.data) self.data[self.mask] = self.fill_value if hasattr(self, 'error'): @@ -1192,11 +1297,20 @@ def copy(self): temp = spatial(fill_value=self.fill_value) # copy attributes or update attributes dictionary if isinstance(self.attributes, list): - setattr(temp,'attributes',self.attributes) + setattr(temp, 'attributes', self.attributes) elif isinstance(self.attributes, dict): temp.attributes.update(self.attributes) # assign variables to self - var = ['lon','lat','data','mask','error','time','month','filename'] + var = [ + 'lon', + 'lat', + 'data', + 'mask', + 'error', + 'time', + 'month', + 'filename', + ] for key in var: try: val = getattr(self, key) @@ -1215,7 +1329,7 @@ def zeros_like(self): # assign variables to self temp.lon = self.lon.copy() temp.lat = self.lat.copy() - var = ['data','mask','error','time','month'] + var = ['data', 'mask', 'error', 'time', 'month'] for key in var: try: val = getattr(self, key) @@ -1234,16 +1348,16 @@ def expand_dims(self): self.time = np.atleast_1d(self.time) self.month = np.atleast_1d(self.month) # output spatial with a third dimension - if (np.ndim(self.data) == 2): - self.data = self.data[:,:,None] + if np.ndim(self.data) == 2: + self.data = self.data[:, :, None] # try expanding mask variable try: - self.mask = self.mask[:,:,None] + self.mask = self.mask[:, :, None] except Exception as exc: pass # try expanding spatial error try: - self.error = self.error[:,:,None] + self.error = self.error[:, :, None] except AttributeError as exc: pass # update mask @@ -1264,27 +1378,27 @@ def extend_matrix(self): # shape of the original data object ny, nx, *nt = self.shape # extended longitude array [x-1,x0,...,xN,xN+1] - temp.lon = np.zeros((nx+2), dtype=self.lon.dtype) + temp.lon = np.zeros((nx + 2), dtype=self.lon.dtype) temp.lon[0] = self.lon[0] - self.spacing[0] temp.lon[1:-1] = self.lon[:] temp.lon[-1] = self.lon[-1] + self.spacing[1] # attempt to extend possible data variables - for key in ['data','mask','error']: + for key in ['data', 'mask', 'error']: try: # get the original data variable var = getattr(self, key) # extended data matrices along longitude axis - if (self.ndim == 2): - tmp = np.zeros((ny, nx+2), dtype=var.dtype) - tmp[:,0] = var[:,-1] - tmp[:,1:-1] = var[:,:] - tmp[:,-1] = var[:,0] - elif (self.ndim == 3): + if self.ndim == 2: + tmp = np.zeros((ny, nx + 2), dtype=var.dtype) + tmp[:, 0] = var[:, -1] + tmp[:, 1:-1] = var[:, :] + tmp[:, -1] = var[:, 0] + elif self.ndim == 3: var = getattr(self, key) - tmp = np.zeros((ny, nx+2, nt[0]), dtype=var.dtype) - tmp[:,0,:] = var[:,-1,:] - tmp[:,1:-1,:] = var[:,:,:] - tmp[:,-1,:] = var[:,0,:] + tmp = np.zeros((ny, nx + 2, nt[0]), dtype=var.dtype) + tmp[:, 0, :] = var[:, -1, :] + tmp[:, 1:-1, :] = var[:, :, :] + tmp[:, -1, :] = var[:, 0, :] # set the output extended data variable setattr(temp, key, tmp) except Exception as exc: @@ -1302,7 +1416,7 @@ def squeeze(self): self.time = np.squeeze(self.time) self.month = np.squeeze(self.month) # attempt to squeeze possible data variables - for key in ['data','mask','error']: + for key in ['data', 'mask', 'error']: try: setattr(self, key, np.squeeze(getattr(self, key))) except Exception as exc: @@ -1325,10 +1439,10 @@ def index(self, indice, date=True): # output spatial object temp = spatial(fill_value=self.fill_value) # attempt to subset possible data variables - for key in ['data','mask','error']: + for key in ['data', 'mask', 'error']: try: tmp = getattr(self, key) - setattr(temp, key, tmp[:,:,indice].copy()) + setattr(temp, key, tmp[:, :, indice].copy()) except Exception as exc: pass # copy dimensions @@ -1366,28 +1480,28 @@ def subset(self, months): m = ','.join([f'{m:03d}' for m in months_check]) raise IOError(f'GRACE/GRACE-FO months {m} not Found') # indices to sort data objects - months_list = [i for i,m in enumerate(self.month) if m in months] + months_list = [i for i, m in enumerate(self.month) if m in months] # output spatial object temp = self.zeros_like() # create output spatial object - temp.data = np.zeros((self.shape[0],self.shape[1],n)) - temp.mask = np.zeros((self.shape[0],self.shape[1],n), dtype=bool) + temp.data = np.zeros((self.shape[0], self.shape[1], n)) + temp.mask = np.zeros((self.shape[0], self.shape[1], n), dtype=bool) # create output spatial error try: getattr(self, 'error') - temp.error = np.zeros((self.shape[0],self.shape[1],n)) + temp.error = np.zeros((self.shape[0], self.shape[1], n)) except AttributeError: pass # allocate for output dates temp.time = np.zeros((n)) - temp.month = np.zeros((n),dtype=np.int64) + temp.month = np.zeros((n), dtype=np.int64) temp.filename = [] # for each indice - for t,i in enumerate(months_list): - temp.data[:,:,t] = self.data[:,:,i].copy() - temp.mask[:,:,t] = self.mask[:,:,i].copy() + for t, i in enumerate(months_list): + temp.data[:, :, t] = self.data[:, :, i].copy() + temp.mask[:, :, t] = self.mask[:, :, i].copy() try: - temp.error[:,:,t] = self.error[:,:,i].copy() + temp.error[:, :, t] = self.error[:, :, i].copy() except AttributeError: pass # copy time dimensions @@ -1413,26 +1527,26 @@ def offset(self, var): """ temp = self.copy() # offset by a single constant or a time-variable scalar - if (np.ndim(var) == 0): + if np.ndim(var) == 0: temp.data = self.data + var elif (np.ndim(var) == 1) and (self.ndim == 2): n = len(var) - temp.data = np.zeros((temp.shape[0],temp.shape[1],n)) - temp.mask = np.zeros((temp.shape[0],temp.shape[1],n),dtype=bool) - for i,v in enumerate(var): - temp.data[:,:,i] = self.data[:,:] + v - temp.mask[:,:,i] = np.copy(self.mask[:,:]) + temp.data = np.zeros((temp.shape[0], temp.shape[1], n)) + temp.mask = np.zeros((temp.shape[0], temp.shape[1], n), dtype=bool) + for i, v in enumerate(var): + temp.data[:, :, i] = self.data[:, :] + v + temp.mask[:, :, i] = np.copy(self.mask[:, :]) elif (np.ndim(var) == 1) and (self.ndim == 3): - for i,v in enumerate(var): - temp.data[:,:,i] = self.data[:,:,i] + v + for i, v in enumerate(var): + temp.data[:, :, i] = self.data[:, :, i] + v elif (np.ndim(var) == 2) and (self.ndim == 2): temp.data = self.data + var elif (np.ndim(var) == 2) and (self.ndim == 3): - for i,t in enumerate(self.time): - temp.data[:,:,i] = self.data[:,:,i] + var + for i, t in enumerate(self.time): + temp.data[:, :, i] = self.data[:, :, i] + var elif (np.ndim(var) == 3) and (self.ndim == 3): - for i,t in enumerate(self.time): - temp.data[:,:,i] = self.data[:,:,i] + var[:,:,i] + for i, t in enumerate(self.time): + temp.data[:, :, i] = self.data[:, :, i] + var[:, :, i] # update mask temp.update_mask() return temp @@ -1448,26 +1562,26 @@ def scale(self, var): """ temp = self.copy() # multiply by a single constant or a time-variable scalar - if (np.ndim(var) == 0): - temp.data = var*self.data + if np.ndim(var) == 0: + temp.data = var * self.data elif (np.ndim(var) == 1) and (self.ndim == 2): n = len(var) - temp.data = np.zeros((temp.shape[0],temp.shape[1],n)) - temp.mask = np.zeros((temp.shape[0],temp.shape[1],n),dtype=bool) - for i,v in enumerate(var): - temp.data[:,:,i] = v*self.data[:,:] - temp.mask[:,:,i] = np.copy(self.mask[:,:]) + temp.data = np.zeros((temp.shape[0], temp.shape[1], n)) + temp.mask = np.zeros((temp.shape[0], temp.shape[1], n), dtype=bool) + for i, v in enumerate(var): + temp.data[:, :, i] = v * self.data[:, :] + temp.mask[:, :, i] = np.copy(self.mask[:, :]) elif (np.ndim(var) == 1) and (self.ndim == 3): - for i,v in enumerate(var): - temp.data[:,:,i] = v*self.data[:,:,i] + for i, v in enumerate(var): + temp.data[:, :, i] = v * self.data[:, :, i] elif (np.ndim(var) == 2) and (self.ndim == 2): - temp.data = var*self.data + temp.data = var * self.data elif (np.ndim(var) == 2) and (self.ndim == 3): - for i,t in enumerate(self.time): - temp.data[:,:,i] = var*self.data[:,:,i] + for i, t in enumerate(self.time): + temp.data[:, :, i] = var * self.data[:, :, i] elif (np.ndim(var) == 3) and (self.ndim == 3): - for i,t in enumerate(self.time): - temp.data[:,:,i] = var[:,:,i]*self.data[:,:,i] + for i, t in enumerate(self.time): + temp.data[:, :, i] = var[:, :, i] * self.data[:, :, i] # update mask temp.update_mask() return temp @@ -1484,24 +1598,25 @@ def mean(self, apply=False, indices=Ellipsis): indices of input ``spatial`` object to compute mean """ # output spatial object - temp = spatial(nlon=self.shape[0],nlat=self.shape[1], - fill_value=self.fill_value) + temp = spatial( + nlon=self.shape[0], nlat=self.shape[1], fill_value=self.fill_value + ) # copy dimensions temp.lon = self.lon.copy() temp.lat = self.lat.copy() # create output mean spatial object - temp.data = np.mean(self.data[:,:,indices],axis=2) - temp.mask = np.any(self.mask[:,:,indices],axis=2) + temp.data = np.mean(self.data[:, :, indices], axis=2) + temp.mask = np.any(self.mask[:, :, indices], axis=2) # calculate the mean time try: val = getattr(self, 'time') temp.time = np.mean(val[indices]) - except (AttributeError,TypeError): + except (AttributeError, TypeError): pass # calculate the spatial anomalies by removing the mean field if apply: - for i,t in enumerate(self.time): - self.data[:,:,i] -= temp.data[:,:] + for i, t in enumerate(self.time): + self.data[:, :, i] -= temp.data[:, :] # update mask temp.update_mask() return temp @@ -1518,14 +1633,14 @@ def flip(self, axis=0): # output spatial object temp = self.copy() # copy dimensions and reverse order - if (axis == 0): + if axis == 0: temp.lat = temp.lat[::-1].copy() - elif (axis == 1): + elif axis == 1: temp.lon = temp.lon[::-1].copy() - elif (axis == 2): + elif axis == 2: temp.time = temp.time[::-1].copy() # attempt to reverse possible data variables - for key in ['data','mask','error']: + for key in ['data', 'mask', 'error']: try: setattr(temp, key, np.flip(getattr(self, key), axis=axis)) except Exception as exc: @@ -1546,7 +1661,7 @@ def transpose(self, axes=None): # output spatial object temp = self.copy() # attempt to transpose possible data variables - for key in ['data','mask','error']: + for key in ['data', 'mask', 'error']: try: setattr(temp, key, np.transpose(getattr(self, key), axes=axes)) except Exception as exc: @@ -1565,14 +1680,15 @@ def sum(self, power=1): apply a power before calculating summation """ # output spatial object - temp = spatial(nlon=self.shape[0],nlat=self.shape[1], - fill_value=self.fill_value) + temp = spatial( + nlon=self.shape[0], nlat=self.shape[1], fill_value=self.fill_value + ) # copy dimensions temp.lon = self.lon.copy() temp.lat = self.lat.copy() # create output summation spatial object - temp.data = np.sum(np.power(self.data,power),axis=2) - temp.mask = np.any(self.mask,axis=2) + temp.data = np.sum(np.power(self.data, power), axis=2) + temp.mask = np.any(self.mask, axis=2) # update mask temp.update_mask() return temp @@ -1587,7 +1703,7 @@ def power(self, power): power to which the ``spatial`` object will be raised """ temp = self.copy() - temp.data = np.power(self.data,power) + temp.data = np.power(self.data, power) return temp def max(self): @@ -1595,14 +1711,15 @@ def max(self): Compute maximum value of a ``spatial`` object """ # output spatial object - temp = spatial(nlon=self.shape[0],nlat=self.shape[1], - fill_value=self.fill_value) + temp = spatial( + nlon=self.shape[0], nlat=self.shape[1], fill_value=self.fill_value + ) # copy dimensions temp.lon = self.lon.copy() temp.lat = self.lat.copy() # create output maximum spatial object - temp.data = np.max(self.data,axis=2) - temp.mask = np.any(self.mask,axis=2) + temp.data = np.max(self.data, axis=2) + temp.mask = np.any(self.mask, axis=2) # update mask temp.update_mask() return temp @@ -1612,14 +1729,15 @@ def min(self): Compute minimum value of a ``spatial`` object """ # output spatial object - temp = spatial(nlon=self.shape[0],nlat=self.shape[1], - fill_value=self.fill_value) + temp = spatial( + nlon=self.shape[0], nlat=self.shape[1], fill_value=self.fill_value + ) # copy dimensions temp.lon = self.lon.copy() temp.lat = self.lat.copy() # create output minimum spatial object - temp.data = np.min(self.data,axis=2) - temp.mask = np.any(self.mask,axis=2) + temp.data = np.min(self.data, axis=2) + temp.mask = np.any(self.mask, axis=2) # update mask temp.update_mask() return temp @@ -1639,11 +1757,11 @@ def replace_invalid(self, fill_value, mask=None): self.update_mask() # update the mask if specified if mask is not None: - if (np.shape(mask) == self.shape): + if np.shape(mask) == self.shape: self.mask |= mask elif (np.ndim(mask) == 2) & (self.ndim == 3): # broadcast mask over third dimension - temp = np.repeat(mask[:,:,np.newaxis],self.shape[2],axis=2) + temp = np.repeat(mask[:, :, np.newaxis], self.shape[2], axis=2) self.mask |= temp # update the fill value self.fill_value = fill_value @@ -1657,7 +1775,7 @@ def replace_masked(self): """ Replace the masked values with ``fill_value`` """ - if (self.fill_value is not None): + if self.fill_value is not None: self.data[self.mask] = self.fill_value if (self.fill_value is not None) and hasattr(self, 'error'): self.error[self.mask] = self.fill_value @@ -1670,11 +1788,10 @@ def dtype(self): @property def spacing(self): - """Step size of ``spatial`` object ``[longitude,latitude]`` - """ + """Step size of ``spatial`` object ``[longitude,latitude]``""" dlat = np.abs(self.lat[1] - self.lat[0]) dlon = np.abs(self.lon[1] - self.lon[0]) - return (dlon,dlat) + return (dlon, dlat) @property def extent(self): @@ -1690,27 +1807,24 @@ def extent(self): @property def shape(self): - """Dimensions of ``spatial`` object - """ + """Dimensions of ``spatial`` object""" return np.shape(self.data) @property def ndim(self): - """Number of dimensions in ``spatial`` object - """ + """Number of dimensions in ``spatial`` object""" return np.ndim(self.data) def __str__(self): - """String representation of the ``spatial`` object - """ + """String representation of the ``spatial`` object""" properties = ['gravity_toolkit.spatial'] extent = ', '.join(map(str, self.extent)) - properties.append(f" extent: {extent}") + properties.append(f' extent: {extent}') shape = ', '.join(map(str, self.shape)) - properties.append(f" shape: {shape}") + properties.append(f' shape: {shape}') if self.month: - properties.append(f" start_month: {min(self.month)}") - properties.append(f" end_month: {max(self.month)}") + properties.append(f' start_month: {min(self.month)}') + properties.append(f' end_month: {max(self.month)}') return '\n'.join(properties) def __add__(self, other): @@ -1721,7 +1835,7 @@ def __add__(self, other): def __div__(self, other): """Divide values from a ``spatial`` object""" return self.__truediv__(other) - + def __iadd__(self, other): """In-place add values to a ``spatial`` object""" return self.offset(other) @@ -1750,12 +1864,12 @@ def __mul__(self, other): """Multiply values from a ``spatial`` object""" temp = self.copy() return temp.scale(other) - + def __pow__(self, other): """Raise values from a ``spatial`` object to a power""" temp = self.copy() return temp.power(other) - + def __sub__(self, other): """Subtract values from a ``spatial`` object""" temp = self.copy() @@ -1767,25 +1881,22 @@ def __truediv__(self, other): return temp.scale(1.0 / other) def __len__(self): - """Number of months - """ + """Number of months""" return len(self.month) if np.any(self.month) else 0 def __iter__(self): - """Iterate over GRACE/GRACE-FO months - """ + """Iterate over GRACE/GRACE-FO months""" self.__index__ = 0 return self def __next__(self): - """Get the next month of data - """ + """Get the next month of data""" # output spatial object temp = spatial(fill_value=self.fill_value) # subset output spatial field and dates try: - temp.data = self.data[:,:,self.__index__].copy() - temp.mask = self.mask[:,:,self.__index__].copy() + temp.data = self.data[:, :, self.__index__].copy() + temp.mask = self.mask[:, :, self.__index__].copy() except IndexError as exc: raise StopIteration from exc # subset output spatial time and month @@ -1796,7 +1907,7 @@ def __next__(self): pass # subset output spatial error try: - temp.error = self.error[:,:,self.__index__].copy() + temp.error = self.error[:, :, self.__index__].copy() except AttributeError as exc: pass # subset filename @@ -1812,6 +1923,7 @@ def __next__(self): self.__index__ += 1 return temp + # PURPOSE: additional routines for the spatial module # for outputting scaling factor data class scaling_factors(spatial): @@ -1852,7 +1964,9 @@ class scaling_factors(spatial): filename: str input or output filename """ + np.seterr(invalid='ignore') + def __init__(self, **kwargs): super().__init__(**kwargs) self.error = None @@ -1892,27 +2006,27 @@ def from_ascii(self, filename, **kwargs): # set filename self.case_insensitive_filename(filename) # set default parameters - kwargs.setdefault('verbose',False) - kwargs.setdefault('compression',None) - kwargs.setdefault('spacing',[None,None]) - kwargs.setdefault('nlat',None) - kwargs.setdefault('nlon',None) - kwargs.setdefault('extent',[None]*4) - default_columns = ['lon','lat','kfactor','error','magnitude'] - kwargs.setdefault('columns',default_columns) - kwargs.setdefault('header',0) + kwargs.setdefault('verbose', False) + kwargs.setdefault('compression', None) + kwargs.setdefault('spacing', [None, None]) + kwargs.setdefault('nlat', None) + kwargs.setdefault('nlon', None) + kwargs.setdefault('extent', [None] * 4) + default_columns = ['lon', 'lat', 'kfactor', 'error', 'magnitude'] + kwargs.setdefault('columns', default_columns) + kwargs.setdefault('header', 0) # open the ascii file and extract contents logging.info(str(self.filename)) - if (kwargs['compression'] == 'gzip'): + if kwargs['compression'] == 'gzip': # read input ascii data from gzip compressed file and split lines with gzip.open(self.filename, mode='r') as f: file_contents = f.read().decode('ISO-8859-1').splitlines() - elif (kwargs['compression'] == 'zip'): + elif kwargs['compression'] == 'zip': # read input ascii data from zipped file and split lines stem = self.filename.stem with zipfile.ZipFile(self.filename) as z: file_contents = z.read(stem).decode('ISO-8859-1').splitlines() - elif (kwargs['compression'] == 'bytes'): + elif kwargs['compression'] == 'bytes': # read input file object and split lines file_contents = self.filename.read().splitlines() else: @@ -1935,7 +2049,11 @@ def from_ascii(self, filename, **kwargs): dlon, dlat = kwargs.get('spacing') self.lat = np.arange(extent[3], extent[2] - dlat, dlat) self.lon = np.arange(extent[0], extent[1] + dlon, dlon) - elif kwargs['nlat'] and kwargs['nlon'] and (None not in kwargs['spacing']): + elif ( + kwargs['nlat'] + and kwargs['nlon'] + and (None not in kwargs['spacing']) + ): dlon, dlat = kwargs.get('spacing') self.lat = np.zeros((kwargs['nlat'])) self.lon = np.zeros((kwargs['nlon'])) @@ -1955,16 +2073,19 @@ def from_ascii(self, filename, **kwargs): for line in file_contents[header:]: # extract columns of interest and assign to dict # convert fortran exponentials if applicable - d = {c:r.replace('D','E') for c,r in zip(columns,rx.findall(line))} + d = { + c: r.replace('D', 'E') + for c, r in zip(columns, rx.findall(line)) + } # convert line coordinates to integers - ilon = np.int64(np.float64(d['lon'])/dlon) - ilat = np.int64((90.0-np.float64(d['lat']))//dlat) + ilon = np.int64(np.float64(d['lon']) / dlon) + ilat = np.int64((90.0 - np.float64(d['lat'])) // dlat) # get scaling factor, error and magnitude - self.data[ilat,ilon] = np.float64(d['data']) - self.error[ilat,ilon] = np.float64(d['error']) - self.magnitude[ilat,ilon] = np.float64(d['magnitude']) + self.data[ilat, ilon] = np.float64(d['data']) + self.error[ilat, ilon] = np.float64(d['error']) + self.magnitude[ilat, ilon] = np.float64(d['magnitude']) # set mask - self.mask[ilat,ilon] = False + self.mask[ilat, ilon] = False # set latitude and longitude self.lon[ilon] = np.float64(d['lon']) self.lat[ilat] = np.float64(d['lat']) @@ -1985,16 +2106,21 @@ def to_ascii(self, filename, **kwargs): """ self.filename = pathlib.Path(filename).expanduser().absolute() # set default verbosity and parameters - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) logging.info(str(self.filename)) # open the output file fid = self.filename.open(mode='w', encoding='utf8') # write to file for each valid latitude and longitude - ii,jj = np.nonzero((self.data != self.fill_value) & (~self.mask)) - for i,j in zip(ii,jj): - print((f'{self.lon[j]:10.4f} {self.lat[i]:10.4f} ' - f'{self.data[i,j]:12.4f} {self.error[i,j]:12.4f} ' - f'{self.magnitude[i,j]:12.4f}'), file=fid) + ii, jj = np.nonzero((self.data != self.fill_value) & (~self.mask)) + for i, j in zip(ii, jj): + print( + ( + f'{self.lon[j]:10.4f} {self.lat[i]:10.4f} ' + f'{self.data[i, j]:12.4f} {self.error[i, j]:12.4f} ' + f'{self.magnitude[i, j]:12.4f}' + ), + file=fid, + ) # close the output file fid.close() @@ -2031,15 +2157,17 @@ def kfactor(self, var): temp.lat = np.copy(temp1.lat) # find valid data points and set mask temp.mask = np.any(temp1.mask | temp2.mask, axis=2) - indy,indx = np.nonzero(np.logical_not(temp.mask)) + indy, indx = np.nonzero(np.logical_not(temp.mask)) # calculate point-based scaling factors as centroids - val1 = np.sum(temp1.data[indy,indx,:]*temp2.data[indy,indx,:],axis=1) - val2 = np.sum(temp1.data[indy,indx,:]**2,axis=1) - temp.data[indy,indx] = val1/val2 + val1 = np.sum( + temp1.data[indy, indx, :] * temp2.data[indy, indx, :], axis=1 + ) + val2 = np.sum(temp1.data[indy, indx, :] ** 2, axis=1) + temp.data[indy, indx] = val1 / val2 # calculate difference between scaled and original variance = temp1.scale(temp.data).offset(-temp2.data) # calculate scaling factor errors as RMS of variance - temp.error = np.sqrt((variance.sum(power=2).data)/nt) + temp.error = np.sqrt((variance.sum(power=2).data) / nt) # calculate magnitude of original data temp.magnitude = temp2.sum(power=2.0).power(0.5).data[:] # update mask @@ -2052,7 +2180,7 @@ def update_mask(self): Update the mask of the ``scaling_factors`` object """ if self.fill_value is not None: - self.mask |= (self.data == self.fill_value) + self.mask |= self.data == self.fill_value self.mask |= np.isnan(self.data) self.data[self.mask] = self.fill_value # replace fill values within scaling factor errors diff --git a/gravity_toolkit/time.py b/gravity_toolkit/time.py index 8ad52ced..09e56ac2 100644 --- a/gravity_toolkit/time.py +++ b/gravity_toolkit/time.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" time.py Written by Tyler Sutterley (06/2024) Contributions by Hugo Lecomte @@ -37,6 +37,7 @@ Updated 08/2020: added NASA Earthdata routines for downloading from CDDIS Written 07/2020 """ + from __future__ import annotations import re @@ -51,18 +52,36 @@ import gravity_toolkit.utilities # conversion factors between time units and seconds -_to_sec = {'microseconds': 1e-6, 'microsecond': 1e-6, - 'microsec': 1e-6, 'microsecs': 1e-6, - 'milliseconds': 1e-3, 'millisecond': 1e-3, - 'millisec': 1e-3, 'millisecs': 1e-3, - 'msec': 1e-3, 'msecs': 1e-3, 'ms': 1e-3, - 'seconds': 1.0, 'second': 1.0, 'sec': 1.0, - 'secs': 1.0, 's': 1.0, - 'minutes': 60.0, 'minute': 60.0, - 'min': 60.0, 'mins': 60.0, - 'hours': 3600.0, 'hour': 3600.0, - 'hr': 3600.0, 'hrs': 3600.0, 'h': 3600.0, - 'day': 86400.0, 'days': 86400.0, 'd': 86400.0} +_to_sec = { + 'microseconds': 1e-6, + 'microsecond': 1e-6, + 'microsec': 1e-6, + 'microsecs': 1e-6, + 'milliseconds': 1e-3, + 'millisecond': 1e-3, + 'millisec': 1e-3, + 'millisecs': 1e-3, + 'msec': 1e-3, + 'msecs': 1e-3, + 'ms': 1e-3, + 'seconds': 1.0, + 'second': 1.0, + 'sec': 1.0, + 'secs': 1.0, + 's': 1.0, + 'minutes': 60.0, + 'minute': 60.0, + 'min': 60.0, + 'mins': 60.0, + 'hours': 3600.0, + 'hour': 3600.0, + 'hr': 3600.0, + 'hrs': 3600.0, + 'h': 3600.0, + 'day': 86400.0, + 'days': 86400.0, + 'd': 86400.0, +} # approximate conversions for longer periods _to_sec['mon'] = 30.0 * 86400.0 _to_sec['month'] = 30.0 * 86400.0 @@ -79,6 +98,7 @@ _gps_epoch = (1980, 1, 6, 0, 0, 0) _j2000_epoch = (2000, 1, 1, 12, 0, 0) + # PURPOSE: parse a date string and convert to a datetime object in UTC def parse(date_string): """ @@ -103,6 +123,7 @@ def parse(date_string): # return the datetime object return date + # PURPOSE: parse a date string into epoch and units scale def parse_date_string(date_string): """ @@ -138,6 +159,7 @@ def parse_date_string(date_string): # return the epoch (as list) and the time unit conversion factors return (datetime_to_list(epoch), _to_sec[units]) + # PURPOSE: split a date string into units and epoch def split_date_string(date_string): """ @@ -149,12 +171,13 @@ def split_date_string(date_string): time-units since yyyy-mm-dd hh:mm:ss """ try: - units,_,epoch = date_string.split(None, 2) + units, _, epoch = date_string.split(None, 2) except ValueError: raise ValueError(f'Invalid format: {date_string}') else: return (units.lower(), parse(epoch)) + # PURPOSE: convert a datetime object into a list def datetime_to_list(date): """ @@ -170,7 +193,15 @@ def datetime_to_list(date): date: list [year,month,day,hour,minute,second] """ - return [date.year,date.month,date.day,date.hour,date.minute,date.second] + return [ + date.year, + date.month, + date.day, + date.hour, + date.minute, + date.second, + ] + # PURPOSE: extract parameters from filename def parse_grace_file(granule): @@ -195,13 +226,18 @@ def parse_grace_file(granule): # GRGS: CNES Groupe de Recherche de Geodesie Spatiale centers = r'UTCSR|EIGEN|GFZOP|JPLEM|JPLMSC|GRGS|COSTG|GRGS' suffixes = r'\.gz|\.gfc|\.txt' - regex_pattern = (r'(.*?)-2_(\d{4})(\d{3})-(\d{4})(\d{3})_' - rf'(.*?)_({centers})_(.*?)_(\d+)(.*?)({suffixes})?$') + regex_pattern = ( + r'(.*?)-2_(\d{4})(\d{3})-(\d{4})(\d{3})_' + rf'(.*?)_({centers})_(.*?)_(\d+)(.*?)({suffixes})?$' + ) rx = re.compile(regex_pattern, re.VERBOSE) # extract parameters from input filename - PFX,SY,SD,EY,ED,AUX,PRC,F1,DRL,F2,SFX = rx.findall(file_basename).pop() + PFX, SY, SD, EY, ED, AUX, PRC, F1, DRL, F2, SFX = rx.findall( + file_basename + ).pop() # return the start and end date lists - return ((SY,SD),(EY,ED)) + return ((SY, SD), (EY, ED)) + # PURPOSE: extract dates from GRAZ or Swarm files with regular expressions def parse_gfc_file(granule, PROC, DSET): @@ -229,7 +265,7 @@ def parse_gfc_file(granule, PROC, DSET): # verify that filename is reduced to basename file_basename = pathlib.Path(granule).name # extract parameters from input filename - if (PROC == 'GRAZ'): + if PROC == 'GRAZ': # regular expression operators for ITSG data and models itsg_products = [] itsg_products.append(r'atmosphere') @@ -240,42 +276,51 @@ def parse_gfc_file(granule, PROC, DSET): itsg_products.append(r'Grace2016') itsg_products.append(r'Grace2018') itsg_products.append(r'Grace_operational') - regex_pattern=(r'(AOD1B_RL\d+|model|ITSG)[-_]({0})(_n\d+)?_' - r'(\d+)-(\d+)(\.gfc)').format(r'|'.join(itsg_products)) + regex_pattern = ( + r'(AOD1B_RL\d+|model|ITSG)[-_]({0})(_n\d+)?_' + r'(\d+)-(\d+)(\.gfc)' + ).format(r'|'.join(itsg_products)) # compile regular expression operator for parameters from files rx = re.compile(regex_pattern, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - PFX,PRD,trunc,year,month,SFX = rx.findall(file_basename).pop() + PFX, PRD, trunc, year, month, SFX = rx.findall(file_basename).pop() # number of days in each month for the calendar year dpm = calendar_days(int(year)) # create start and end date lists - start_date = [int(year),int(month),1,0,0,0] - end_date = [int(year),int(month),dpm[int(month)-1],23,59,59] + start_date = [int(year), int(month), 1, 0, 0, 0] + end_date = [int(year), int(month), dpm[int(month) - 1], 23, 59, 59] elif (PROC == 'Swarm') and (DSET == 'GSM'): # regular expression operators for Swarm data - regex_pattern=r'(SW)_(.*?)_(EGF_SHA_2)__(.*?)_(.*?)_(.*?)(\.gfc|\.ZIP)' + regex_pattern = ( + r'(SW)_(.*?)_(EGF_SHA_2)__(.*?)_(.*?)_(.*?)(\.gfc|\.ZIP)' + ) # compile regular expression operator for parameters from files rx = re.compile(regex_pattern, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - SAT,tmp,PROD,starttime,endtime,RL,SFX = rx.findall(file_basename).pop() - start_date,_ = parse_date_string(starttime) - end_date,_ = parse_date_string(endtime) + SAT, tmp, PROD, starttime, endtime, RL, SFX = rx.findall( + file_basename + ).pop() + start_date, _ = parse_date_string(starttime) + end_date, _ = parse_date_string(endtime) elif (PROC == 'Swarm') and (DSET != 'GSM'): # regular expression operators for Swarm models - regex_pattern=(r'(GAA|GAB|GAC|GAD)_Swarm_(\d+)_(\d{2})_(\d{4})' - r'(\.gfc|\.ZIP)') + regex_pattern = ( + r'(GAA|GAB|GAC|GAD)_Swarm_(\d+)_(\d{2})_(\d{4})' + r'(\.gfc|\.ZIP)' + ) # compile regular expression operator for parameters from files rx = re.compile(regex_pattern, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - PROD,trunc,month,year,SFX = rx.findall(file_basename).pop() + PROD, trunc, month, year, SFX = rx.findall(file_basename).pop() # number of days in each month for the calendar year dpm = calendar_days(int(year)) # create start and end date lists - start_date = [int(year),int(month),1,0,0,0] - end_date = [int(year),int(month),dpm[int(month)-1],23,59,59] + start_date = [int(year), int(month), 1, 0, 0, 0] + end_date = [int(year), int(month), dpm[int(month) - 1], 23, 59, 59] # return the start and end date lists return (start_date, end_date) + def reduce_by_date(granules): """ Reduce list of GRACE/GRACE-FO files by date to the newest version @@ -297,15 +342,17 @@ def reduce_by_date(granules): # GRGS: French Centre National D'Etudes Spatiales (CNES) # COSTG: International Combined Time-variable Gravity Fields args = r'UTCSR|EIGEN|GFZOP|JPLEM|JPLMSC|GRGS|COSTG' - regex_pattern = (r'(.*?)-2_(\d{{4}})(\d{{3}})-(\d{{4}})(\d{{3}})_(.*?)_' - r'({0})_(.*?)_(\d{{2}})(\d{{2}})(.*?)(\.gz|\.gfc)?$').format(args) + regex_pattern = ( + r'(.*?)-2_(\d{{4}})(\d{{3}})-(\d{{4}})(\d{{3}})_(.*?)_' + r'({0})_(.*?)_(\d{{2}})(\d{{2}})(.*?)(\.gz|\.gfc)?$' + ).format(args) rx = re.compile(regex_pattern, re.VERBOSE) # for each unique date for d in sorted(set(date_list)): - if (date_list.count(d) == 1): + if date_list.count(d) == 1: i = date_list.index(d) unique_list.append(granules[i]) - elif (date_list.count(d) >= 2): + elif date_list.count(d) >= 2: # if more than 1 file with date use newest version indices = [i for i, dt in enumerate(date_list) if (dt == d)] # find each version within the file @@ -314,8 +361,9 @@ def reduce_by_date(granules): # verify that filename is reduced to basename file_basename = pathlib.Path(granules[i]).name # parse filename to get file version - PFX,SY,SD,EY,ED,AUX,PRC,F1,DRL,VER,F2,SFX = \ + PFX, SY, SD, EY, ED, AUX, PRC, F1, DRL, VER, F2, SFX = ( rx.findall(file_basename).pop() + ) # append to list of file versions versions.append(int(VER)) # find file with newest version @@ -324,6 +372,7 @@ def reduce_by_date(granules): # return the sorted list of files with unique dates return unique_list + # PURPOSE: Adjust GRACE/GRACE-FO months to fix "Special Cases" def adjust_months(grace_month): """ @@ -355,29 +404,29 @@ def adjust_months(grace_month): # create temporary months object m = np.zeros_like(grace_month) # find unique months - _,i,c = np.unique(grace_month,return_inverse=True,return_counts=True) + _, i, c = np.unique(grace_month, return_inverse=True, return_counts=True) # simple unique months case - case1, = np.nonzero(c[i] == 1) + (case1,) = np.nonzero(c[i] == 1) m[case1] = grace_month[case1] # Special Months cases - case2, = np.nonzero(c[i] == 2) + (case2,) = np.nonzero(c[i] == 2) # for each special case month for j in case2: # prior month, current month, subsequent 2 months - mm1 = grace_month[j-1] + mm1 = grace_month[j - 1] mon = grace_month[j] - mp1 = grace_month[j+1] if (j < (nmon-1)) else (mon + 1) - mp2 = grace_month[j+2] if (j < (nmon-2)) else (mp1 + 1) + mp1 = grace_month[j + 1] if (j < (nmon - 1)) else (mon + 1) + mp2 = grace_month[j + 2] if (j < (nmon - 2)) else (mp1 + 1) # determine the months which meet the criteria need to be adjusted - if (mon == (mm1 + 1)): + if mon == (mm1 + 1): # case where month is correct # but subsequent month needs to be +1 m[j] = np.copy(grace_month[j]) - elif (mon == mm1) and (mon != m[j-1]): + elif (mon == mm1) and (mon != m[j - 1]): # case where prior month needed to be -1 # but current month is correct m[j] = np.copy(grace_month[j]) - elif (mon == mm1): + elif mon == mm1: # case where month should be +1 m[j] = grace_month[j] + 1 elif (mon == mp1) and ((mon == (mm1 + 2)) or (mp2 == (mp1 + 1))): @@ -386,8 +435,9 @@ def adjust_months(grace_month): # update months and remove singleton dimensions if necessary return np.squeeze(m) + # PURPOSE: convert calendar dates to GRACE/GRACE-FO months -def calendar_to_grace(year,month=1,around=np.floor): +def calendar_to_grace(year, month=1, around=np.floor): """ Converts calendar dates to GRACE/GRACE-FO months @@ -405,9 +455,10 @@ def calendar_to_grace(year,month=1,around=np.floor): grace_month: np.ndarray GRACE/GRACE-FO month """ - grace_month = around(12.0*(year - 2002.0)) + month + grace_month = around(12.0 * (year - 2002.0)) + month return np.array(grace_month, dtype=int) + # PURPOSE: convert GRACE/GRACE-FO months to calendar dates def grace_to_calendar(grace_month): """ @@ -425,10 +476,11 @@ def grace_to_calendar(grace_month): month: np.ndarray calendar month """ - year = np.array(2002 + (grace_month-1)//12).astype(int) - month = np.mod(grace_month-1,12) + 1 + year = np.array(2002 + (grace_month - 1) // 12).astype(int) + month = np.mod(grace_month - 1, 12) + 1 return (year, month) + # PURPOSE: convert calendar dates to Julian days def calendar_to_julian(year_decimal): """ @@ -448,18 +500,25 @@ def calendar_to_julian(year_decimal): year = np.floor(year_decimal) # calculation of day of the year dpy = calendar_days(year).sum() - DofY = dpy*(year_decimal % 1) + DofY = dpy * (year_decimal % 1) # Calculation of the Julian date from year and DofY - JD = np.array(367.0*year - np.floor(7.0*year/4.0) - - np.floor(3.0*(np.floor((7.0*year - 1.0)/700.0) + 1.0)/4.0) + - DofY + 1721058.5, dtype=np.float64) + JD = np.array( + 367.0 * year + - np.floor(7.0 * year / 4.0) + - np.floor(3.0 * (np.floor((7.0 * year - 1.0) / 700.0) + 1.0) / 4.0) + + DofY + + 1721058.5, + dtype=np.float64, + ) return JD + # days per month in a leap and a standard year # only difference is February (29 vs. 28) _dpm_leap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] _dpm_stnd = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + # PURPOSE: gets the number of days per month for a given year def calendar_days(year): """ @@ -482,16 +541,17 @@ def calendar_days(year): # Subtracting a leap year every 100 years ==> average 365.24 # Adding a leap year back every 400 years ==> average 365.2425 # Subtracting a leap year every 4000 years ==> average 365.24225 - m4 = (year % 4) - m100 = (year % 100) - m400 = (year % 400) - m4000 = (year % 4000) + m4 = year % 4 + m100 = year % 100 + m400 = year % 400 + m4000 = year % 4000 # find indices for standard years and leap years using criteria - if ((m4 == 0) & (m100 != 0) | (m400 == 0) & (m4000 != 0)): + if (m4 == 0) & (m100 != 0) | (m400 == 0) & (m4000 != 0): return np.array(_dpm_leap, dtype=np.float64) - elif ((m4 != 0) | (m100 == 0) & (m400 != 0) | (m4000 == 0)): + elif (m4 != 0) | (m100 == 0) & (m400 != 0) | (m4000 == 0): return np.array(_dpm_stnd, dtype=np.float64) + # PURPOSE: convert a numpy datetime array to delta times since an epoch def convert_datetime(date, epoch=_unix_epoch): """ @@ -517,6 +577,7 @@ def convert_datetime(date, epoch=_unix_epoch): # convert to delta time return (date - epoch) / np.timedelta64(1, 's') + # PURPOSE: convert times from seconds since epoch1 to time since epoch2 def convert_delta_time(delta_time, epoch1=None, epoch2=None, scale=1.0): """ @@ -545,12 +606,21 @@ def convert_delta_time(delta_time, epoch1=None, epoch2=None, scale=1.0): # calculate the total difference in time in seconds delta_time_epochs = (epoch2 - epoch1) / np.timedelta64(1, 's') # subtract difference in time and rescale to output units - return scale*(delta_time - delta_time_epochs) + return scale * (delta_time - delta_time_epochs) + # PURPOSE: calculate the delta time from calendar date # http://scienceworld.wolfram.com/astronomy/JulianDate.html -def convert_calendar_dates(year, month, day, hour=0.0, minute=0.0, second=0.0, - epoch=(1992,1,1,0,0,0), scale=1.0): +def convert_calendar_dates( + year, + month, + day, + hour=0.0, + minute=0.0, + second=0.0, + epoch=(1992, 1, 1, 0, 0, 0), + scale=1.0, +): """ Calculate the time in units since ``epoch`` from calendar dates @@ -580,10 +650,20 @@ def convert_calendar_dates(year, month, day, hour=0.0, minute=0.0, second=0.0, """ # calculate date in Modified Julian Days (MJD) from calendar date # MJD: days since November 17, 1858 (1858-11-17T00:00:00) - MJD = 367.0*year - np.floor(7.0*(year + np.floor((month+9.0)/12.0))/4.0) - \ - np.floor(3.0*(np.floor((year + (month - 9.0)/7.0)/100.0) + 1.0)/4.0) + \ - np.floor(275.0*month/9.0) + day + hour/24.0 + minute/1440.0 + \ - second/86400.0 + 1721028.5 - 2400000.5 + MJD = ( + 367.0 * year + - np.floor(7.0 * (year + np.floor((month + 9.0) / 12.0)) / 4.0) + - np.floor( + 3.0 * (np.floor((year + (month - 9.0) / 7.0) / 100.0) + 1.0) / 4.0 + ) + + np.floor(275.0 * month / 9.0) + + day + + hour / 24.0 + + minute / 1440.0 + + second / 86400.0 + + 1721028.5 + - 2400000.5 + ) # convert epochs to datetime variables epoch1 = np.datetime64(datetime.datetime(*_mjd_epoch)) if isinstance(epoch, (tuple, list)): @@ -593,11 +673,13 @@ def convert_calendar_dates(year, month, day, hour=0.0, minute=0.0, second=0.0, # calculate the total difference in time in days delta_time_epochs = (epoch - epoch1) / np.timedelta64(1, 'D') # return the date in units (default days) since epoch - return scale*np.array(MJD - delta_time_epochs, dtype=np.float64) + return scale * np.array(MJD - delta_time_epochs, dtype=np.float64) + # PURPOSE: Converts from calendar dates into decimal years -def convert_calendar_decimal(year, month, day=None, hour=None, minute=None, - second=None, DofY=None): +def convert_calendar_decimal( + year, month, day=None, hour=None, minute=None, second=None, DofY=None +): """ Converts from calendar date into decimal years taking into account leap years :cite:p:`Dershowitz:2007cc` @@ -658,34 +740,34 @@ def convert_calendar_decimal(year, month, day=None, hour=None, minute=None, # Subtracting a leap year every 100 years ==> average 365.24 # Adding a leap year back every 400 years ==> average 365.2425 # Subtracting a leap year every 4000 years ==> average 365.24225 - m4 = (cal_date['year'] % 4) - m100 = (cal_date['year'] % 100) - m400 = (cal_date['year'] % 400) - m4000 = (cal_date['year'] % 4000) + m4 = cal_date['year'] % 4 + m100 = cal_date['year'] % 100 + m400 = cal_date['year'] % 400 + m4000 = cal_date['year'] % 4000 # find indices for standard years and leap years using criteria - leap, = np.nonzero((m4 == 0) & (m100 != 0) | (m400 == 0) & (m4000 != 0)) - stnd, = np.nonzero((m4 != 0) | (m100 == 0) & (m400 != 0) | (m4000 == 0)) + (leap,) = np.nonzero((m4 == 0) & (m100 != 0) | (m400 == 0) & (m4000 != 0)) + (stnd,) = np.nonzero((m4 != 0) | (m100 == 0) & (m400 != 0) | (m4000 == 0)) # calculate the day of the year if DofY is not None: # if entered directly as an input # remove 1 so day 1 (Jan 1st) = 0.0 in decimal format - cal_date['DofY'][:] = np.squeeze(DofY)-1 + cal_date['DofY'][:] = np.squeeze(DofY) - 1 else: # use calendar month and day of the month to calculate day of the year # month minus 1: January = 0, February = 1, etc (indice of month) # in decimal form: January = 0.0 - month_m1 = np.array(cal_date['month'],dtype=np.int64) - 1 + month_m1 = np.array(cal_date['month'], dtype=np.int64) - 1 # day of month if day is not None: # remove 1 so 1st day of month = 0.0 in decimal format - cal_date['day'][:] = np.squeeze(day)-1.0 + cal_date['day'][:] = np.squeeze(day) - 1.0 else: # if not entering days as an input # will use the mid-month value - cal_date['day'][leap] = dpm_leap[month_m1[leap]]/2.0 - cal_date['day'][stnd] = dpm_stnd[month_m1[stnd]]/2.0 + cal_date['day'][leap] = dpm_leap[month_m1[leap]] / 2.0 + cal_date['day'][stnd] = dpm_stnd[month_m1[stnd]] / 2.0 # create matrix with the lower half = 1 # this matrix will be used in a matrix multiplication @@ -693,7 +775,7 @@ def convert_calendar_decimal(year, month, day=None, hour=None, minute=None, # the -1 will make the diagonal == 0 # i.e. first row == all zeros and the # last row == ones for all but the last element - mon_mat=np.tri(12,12,-1) + mon_mat = np.tri(12, 12, -1) # using a dot product to calculate total number of days # for the months before the input date # basically is sum(i*dpm) @@ -705,10 +787,12 @@ def convert_calendar_decimal(year, month, day=None, hour=None, minute=None, # calculate the day of the year for leap and standard # use total days of all months before date # and add number of days before date in month - cal_date['DofY'][stnd] = cal_date['day'][stnd] + \ - np.dot(mon_mat[month_m1[stnd],:],dpm_stnd) - cal_date['DofY'][leap] = cal_date['day'][leap] + \ - np.dot(mon_mat[month_m1[leap],:],dpm_leap) + cal_date['DofY'][stnd] = cal_date['day'][stnd] + np.dot( + mon_mat[month_m1[stnd], :], dpm_stnd + ) + cal_date['DofY'][leap] = cal_date['day'][leap] + np.dot( + mon_mat[month_m1[leap], :], dpm_leap + ) # hour of day (else is zero) if hour is not None: @@ -726,18 +810,23 @@ def convert_calendar_decimal(year, month, day=None, hour=None, minute=None, # convert hours, minutes and seconds into days # convert calculated fractional days into decimal fractions of the year # Leap years - t_date[leap] = cal_date['year'][leap] + \ - (cal_date['DofY'][leap] + cal_date['hour'][leap]/24. + \ - cal_date['minute'][leap]/1440. + \ - cal_date['second'][leap]/86400.)/np.sum(dpm_leap) + t_date[leap] = cal_date['year'][leap] + ( + cal_date['DofY'][leap] + + cal_date['hour'][leap] / 24.0 + + cal_date['minute'][leap] / 1440.0 + + cal_date['second'][leap] / 86400.0 + ) / np.sum(dpm_leap) # Standard years - t_date[stnd] = cal_date['year'][stnd] + \ - (cal_date['DofY'][stnd] + cal_date['hour'][stnd]/24. + \ - cal_date['minute'][stnd]/1440. + \ - cal_date['second'][stnd]/86400.)/np.sum(dpm_stnd) + t_date[stnd] = cal_date['year'][stnd] + ( + cal_date['DofY'][stnd] + + cal_date['hour'][stnd] / 24.0 + + cal_date['minute'][stnd] / 1440.0 + + cal_date['second'][stnd] / 86400.0 + ) / np.sum(dpm_stnd) return t_date + # PURPOSE: Converts from Julian day to calendar date and time def convert_julian(JD, **kwargs): """ @@ -777,15 +866,18 @@ def convert_julian(JD, **kwargs): kwargs.setdefault('format', 'dict') # raise warnings for deprecated keyword arguments deprecated_keywords = dict(ASTYPE='astype', FORMAT='format') - for old,new in deprecated_keywords.items(): + for old, new in deprecated_keywords.items(): if old in kwargs.keys(): - warnings.warn(f"""Deprecated keyword argument {old}. - Changed to '{new}'""", DeprecationWarning) + warnings.warn( + f"""Deprecated keyword argument {old}. + Changed to '{new}'""", + DeprecationWarning, + ) # set renamed argument to not break workflows kwargs[new] = copy.copy(kwargs[old]) # convert to array if only a single value was imported - if (np.ndim(JD) == 0): + if np.ndim(JD) == 0: JD = np.atleast_1d(JD) single_value = True else: @@ -796,24 +888,24 @@ def convert_julian(JD, **kwargs): C = np.zeros_like(JD) # calculate C for dates before and after the switch to Gregorian IGREG = 2299161.0 - ind1, = np.nonzero(JDO < IGREG) + (ind1,) = np.nonzero(JDO < IGREG) C[ind1] = JDO[ind1] + 1524.0 - ind2, = np.nonzero(JDO >= IGREG) - B = np.floor((JDO[ind2] - 1867216.25)/36524.25) - C[ind2] = JDO[ind2] + B - np.floor(B/4.0) + 1525.0 + (ind2,) = np.nonzero(JDO >= IGREG) + B = np.floor((JDO[ind2] - 1867216.25) / 36524.25) + C[ind2] = JDO[ind2] + B - np.floor(B / 4.0) + 1525.0 # calculate coefficients for date conversion - D = np.floor((C - 122.1)/365.25) - E = np.floor((365.0 * D) + np.floor(D/4.0)) - F = np.floor((C - E)/30.6001) + D = np.floor((C - 122.1) / 365.25) + E = np.floor((365.0 * D) + np.floor(D / 4.0)) + F = np.floor((C - E) / 30.6001) # calculate day, month, year and hour - day = np.floor(C - E + 0.5) - np.floor(30.6001*F) - month = F - 1.0 - 12.0*np.floor(F/14.0) - year = D - 4715.0 - np.floor((7.0 + month)/10.0) - hour = np.floor(24.0*(JD + 0.5 - JDO)) + day = np.floor(C - E + 0.5) - np.floor(30.6001 * F) + month = F - 1.0 - 12.0 * np.floor(F / 14.0) + year = D - 4715.0 - np.floor((7.0 + month) / 10.0) + hour = np.floor(24.0 * (JD + 0.5 - JDO)) # calculate minute and second - G = (JD + 0.5 - JDO) - hour/24.0 - minute = np.floor(G*1440.0) - second = (G - minute/1440.0) * 86400.0 + G = (JD + 0.5 - JDO) - hour / 24.0 + minute = np.floor(G * 1440.0) + second = (G - minute / 1440.0) * 86400.0 # convert all variables to output type (from float) if kwargs['astype'] is not None: @@ -834,10 +926,16 @@ def convert_julian(JD, **kwargs): second = second.item(0) # return date variables in output format - if (kwargs['format'] == 'dict'): - return dict(year=year, month=month, day=day, - hour=hour, minute=minute, second=second) - elif (kwargs['format'] == 'tuple'): + if kwargs['format'] == 'dict': + return dict( + year=year, + month=month, + day=day, + hour=hour, + minute=minute, + second=second, + ) + elif kwargs['format'] == 'tuple': return (year, month, day, hour, minute, second) - elif (kwargs['format'] == 'zip'): + elif kwargs['format'] == 'zip': return zip(year, month, day, hour, minute, second) diff --git a/gravity_toolkit/time_series/amplitude.py b/gravity_toolkit/time_series/amplitude.py index ad4df441..5a5b6661 100755 --- a/gravity_toolkit/time_series/amplitude.py +++ b/gravity_toolkit/time_series/amplitude.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" amplitude.py Written by Tyler Sutterley (07/2026) @@ -29,8 +29,10 @@ Updated 05/2013: converted to python Written 07/2012: """ + import numpy as np + def amplitude(bsin, bcos): """ Calculate the amplitude and phase of a harmonic function diff --git a/gravity_toolkit/time_series/fit.py b/gravity_toolkit/time_series/fit.py index 99af58d2..d01431af 100644 --- a/gravity_toolkit/time_series/fit.py +++ b/gravity_toolkit/time_series/fit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" fit.py Written by Tyler Sutterley (06/2023) Utilities for fitting time-series data with regression models @@ -11,9 +11,11 @@ Updated 06/2023: made the tidal aliasing period an option Written 05/2023 """ + from __future__ import annotations import numpy as np + # PURPOSE: build a list of tidal aliasing terms for regression fit def aliasing_terms(t_in: np.ndarray, period=161.0): """ @@ -37,19 +39,19 @@ def aliasing_terms(t_in: np.ndarray, period=161.0): # number of time points nmax = len(t_in) # create custom terms for tidal aliasing during GRACE period - ii, = np.nonzero(t_in[0:nmax] < 2018.0) + (ii,) = np.nonzero(t_in[0:nmax] < 2018.0) SIN = np.zeros((nmax)) COS = np.zeros((nmax)) - SIN[ii] = np.sin(np.pi*t_in[ii]*730.50/period) - COS[ii] = np.cos(np.pi*t_in[ii]*730.50/period) + SIN[ii] = np.sin(np.pi * t_in[ii] * 730.50 / period) + COS[ii] = np.cos(np.pi * t_in[ii] * 730.50 / period) TERMS.append(SIN) TERMS.append(COS) # create custom terms for tidal aliasing during GRACE-FO period - ii, = np.nonzero(t_in[0:nmax] >= 2018.0) + (ii,) = np.nonzero(t_in[0:nmax] >= 2018.0) SIN = np.zeros((nmax)) COS = np.zeros((nmax)) - SIN[ii] = np.sin(np.pi*t_in[ii]*730.50/period) - COS[ii] = np.cos(np.pi*t_in[ii]*730.50/period) + SIN[ii] = np.sin(np.pi * t_in[ii] * 730.50 / period) + COS[ii] = np.cos(np.pi * t_in[ii] * 730.50 / period) TERMS.append(SIN) TERMS.append(COS) # return the fit terms diff --git a/gravity_toolkit/time_series/lomb_scargle.py b/gravity_toolkit/time_series/lomb_scargle.py index 8b52a413..03ccc3a2 100755 --- a/gravity_toolkit/time_series/lomb_scargle.py +++ b/gravity_toolkit/time_series/lomb_scargle.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" lomb_scargle.py Written by Tyler Sutterley (06/2024) @@ -53,9 +53,11 @@ Updated 01/2015: added centroid output Written 08/2013 """ + import numpy as np import scipy.signal + def lomb_scargle(t_in, d_in, **kwargs): """ Computes periodograms for least-squares spectral analysis following @@ -110,15 +112,15 @@ def lomb_scargle(t_in, d_in, **kwargs): # number of independent measurements nmax = np.count_nonzero(np.isfinite(d_in)) - nyquist = 1.0/(2.0*np.mean(t_in[1:] - t_in[0:-1])) + nyquist = 1.0 / (2.0 * np.mean(t_in[1:] - t_in[0:-1])) # angular frequency range if kwargs['OMEGA']: OMEGA = np.atleast_1d(kwargs['OMEGA']) elif kwargs['FREQUENCY']: - OMEGA = np.atleast_1d(kwargs['FREQUENCY'])/(2.0*np.pi) + OMEGA = np.atleast_1d(kwargs['FREQUENCY']) / (2.0 * np.pi) elif kwargs['PERIOD']: - OMEGA = (2.0*np.pi)/np.atleast_1d(kwargs['PERIOD']) + OMEGA = (2.0 * np.pi) / np.atleast_1d(kwargs['PERIOD']) else: raise ValueError('Frequency range must be defined') @@ -132,34 +134,42 @@ def lomb_scargle(t_in, d_in, **kwargs): # analysis based on sample size. From Horne and Baliunas, # "A Prescription for Period Analysis of Unevenly Sampled Time Series", # The Astrophysical Journal, 392: 757-763, 1986. - independent_freq = np.round(-6.362 + 1.193*nmax + 0.00098*nmax**2) + independent_freq = np.round(-6.362 + 1.193 * nmax + 0.00098 * nmax**2) # if less than 1 independent frequency: set equal to 1 independent_freq = np.maximum(independent_freq, 1) # scaling the date (t[0] = 0) t = t_in - t_in[0] # periods and frequencies considered - frequency = angular_freq/(2.0*np.pi) - period = 2.0*np.pi/angular_freq + frequency = angular_freq / (2.0 * np.pi) + period = 2.0 * np.pi / angular_freq # scaling the data to be mean 0 with variance 1 - data_norm = (d_in - d_in.mean())/d_in.std() + data_norm = (d_in - d_in.mean()) / d_in.std() # computing the lomb-scargle periodogram # "normalized" spectral density refers to variance term in denominator # PowerDensity has exponential probability distribution with unit mean # can calculate normalized as described in Scipy reference - PowerDensity = scipy.signal.lombscargle(t, data_norm, angular_freq, - normalize=kwargs['NORMALIZE']) + PowerDensity = scipy.signal.lombscargle( + t, data_norm, angular_freq, normalize=kwargs['NORMALIZE'] + ) # probability of frequencies (NULL test, significance of peak) - probability = 1.0 - (1.0-np.exp(-PowerDensity))**independent_freq + probability = 1.0 - (1.0 - np.exp(-PowerDensity)) ** independent_freq # probability contours p = np.atleast_1d(kwargs['p']) - contour = -np.log(1.0 - (1 - p)**(1.0/independent_freq)) + contour = -np.log(1.0 - (1 - p) ** (1.0 / independent_freq)) # period at peak (maximum probability) ipeak = np.argmax(PowerDensity) peak = period[ipeak] # period at signal centroid - centroid = np.sum(period*PowerDensity)/np.sum(PowerDensity) - - return {'PowerDensity':PowerDensity, 'Probability':probability, - 'frequency':frequency, 'period':period, 'contour':contour, - 'Nyquist':nyquist, 'peak':peak, 'centroid':centroid} + centroid = np.sum(period * PowerDensity) / np.sum(PowerDensity) + + return { + 'PowerDensity': PowerDensity, + 'Probability': probability, + 'frequency': frequency, + 'period': period, + 'contour': contour, + 'Nyquist': nyquist, + 'peak': peak, + 'centroid': centroid, + } diff --git a/gravity_toolkit/time_series/piecewise.py b/gravity_toolkit/time_series/piecewise.py index 66042163..2fd9e08b 100755 --- a/gravity_toolkit/time_series/piecewise.py +++ b/gravity_toolkit/time_series/piecewise.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" piecewise.py Written by Tyler Sutterley (07/2026) @@ -93,13 +93,25 @@ Updated 01/2012: added std weighting for a error weighted least-squares Written 10/2011 """ + import numpy as np import scipy.stats import scipy.special -def piecewise(t_in, d_in, BREAK_TIME=None, BREAKPOINT=None, - CYCLES=[0.5,1.0], TERMS=[], DATA_ERR=0, WEIGHT=False, - STDEV=0, CONF=0, AICc=False): + +def piecewise( + t_in, + d_in, + BREAK_TIME=None, + BREAKPOINT=None, + CYCLES=[0.5, 1.0], + TERMS=[], + DATA_ERR=0, + WEIGHT=False, + STDEV=0, + CONF=0, + AICc=False, +): r""" Fits a synthetic signal to data over a time period by ordinary or weighted least-squares for breakpoint analysis :cite:p:`Toms:2003gv` @@ -197,8 +209,8 @@ def piecewise(t_in, d_in, BREAK_TIME=None, BREAKPOINT=None, DMAT.append(P_x1) # add cyclical terms (0.5=semi-annual, 1=annual) for c in CYCLES: - DMAT.append(np.sin(2.0*np.pi*t_in/np.float64(c))) - DMAT.append(np.cos(2.0*np.pi*t_in/np.float64(c))) + DMAT.append(np.sin(2.0 * np.pi * t_in / np.float64(c))) + DMAT.append(np.cos(2.0 * np.pi * t_in / np.float64(c))) # add additional terms to the design matrix for t in TERMS: DMAT.append(t) @@ -208,41 +220,41 @@ def piecewise(t_in, d_in, BREAK_TIME=None, BREAKPOINT=None, # Calculating Least-Squares Coefficients if WEIGHT: # Weighted Least-Squares fitting - if (np.ndim(DATA_ERR) == 0): + if np.ndim(DATA_ERR) == 0: raise ValueError('Input DATA_ERR for Weighted Least-Squares') # check if any error values are 0 (prevent infinite weights) if np.count_nonzero(DATA_ERR == 0.0): # change to minimum floating point value DATA_ERR[DATA_ERR == 0.0] = np.finfo(np.float64).eps # Weight Precision - wi = np.squeeze(DATA_ERR**(-2)) + wi = np.squeeze(DATA_ERR ** (-2)) # If uncorrelated weights are the diagonal W = np.diag(wi) # Least-Squares fitting # Temporary Matrix: Inv(X'.W.X) - TM1 = np.linalg.inv(np.dot(np.transpose(DMAT),np.dot(W,DMAT))) + TM1 = np.linalg.inv(np.dot(np.transpose(DMAT), np.dot(W, DMAT))) # Temporary Matrix: (X'.W.Y) - TM2 = np.dot(np.transpose(DMAT),np.dot(W,d_in)) + TM2 = np.dot(np.transpose(DMAT), np.dot(W, d_in)) # Least Squares Solutions: Inv(X'.W.X).(X'.W.Y) - beta_mat = np.dot(TM1,TM2) - else:# Standard Least-Squares fitting (the [0] denotes coefficients output) - beta_mat = np.linalg.lstsq(DMAT,d_in,rcond=-1)[0] + beta_mat = np.dot(TM1, TM2) + else: # Standard Least-Squares fitting (the [0] denotes coefficients output) + beta_mat = np.linalg.lstsq(DMAT, d_in, rcond=-1)[0] # Weights are equal wi = 1.0 # Calculating trend2 = beta1 + beta2 # beta2 = change in linear term from beta1 - beta_out = np.copy(beta_mat)# output beta + beta_out = np.copy(beta_mat) # output beta beta_out[2] = beta_mat[1] + beta_mat[2] # number of terms in least-squares solution n_terms = len(beta_mat) # modelled time-series - mod = np.dot(DMAT,beta_mat) + mod = np.dot(DMAT, beta_mat) # time-series residuals - res = d_in[0:nmax] - np.dot(DMAT,beta_mat) + res = d_in[0:nmax] - np.dot(DMAT, beta_mat) # Fitted Values without climate oscillations - simple = np.dot(DMAT[:,0:3],beta_mat[0:3]) + simple = np.dot(DMAT[:, 0:3], beta_mat[0:3]) # Error Analysis # nu = Degrees of Freedom = number of measurements-number of parameters @@ -250,101 +262,141 @@ def piecewise(t_in, d_in, BREAK_TIME=None, BREAKPOINT=None, # calculating R^2 values # SStotal = sum((Y-mean(Y))**2) - SStotal = np.dot(np.transpose(d_in[0:nmax] - np.mean(d_in[0:nmax])), - (d_in[0:nmax] - np.mean(d_in[0:nmax]))) + SStotal = np.dot( + np.transpose(d_in[0:nmax] - np.mean(d_in[0:nmax])), + (d_in[0:nmax] - np.mean(d_in[0:nmax])), + ) # SSerror = sum((Y-X*B)**2) - SSerror = np.dot(np.transpose(d_in[0:nmax] - np.dot(DMAT,beta_mat)), - (d_in[0:nmax] - np.dot(DMAT,beta_mat))) + SSerror = np.dot( + np.transpose(d_in[0:nmax] - np.dot(DMAT, beta_mat)), + (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) # R**2 term = 1- SSerror/SStotal - rsquare = 1.0 - (SSerror/SStotal) + rsquare = 1.0 - (SSerror / SStotal) # Adjusted R**2 term: weighted by degrees of freedom - rsq_adj = 1.0 - (SSerror/SStotal)*np.float64((nmax-1.0)/nu) + rsq_adj = 1.0 - (SSerror / SStotal) * np.float64((nmax - 1.0) / nu) # Fit Criterion # number of parameters including the intercept and the variance K = np.float64(n_terms + 1) # Log-Likelihood with weights (if unweighted, weight portions == 0) # log(L) = -0.5*n*log(sigma^2) - 0.5*n*log(2*pi) - 0.5*n - #log_lik = -0.5*nmax*(np.log(2.0 * np.pi) + 1.0 + np.log(np.sum((res**2)/nmax))) - log_lik = 0.5*(np.sum(np.log(wi)) - nmax*(np.log(2.0 * np.pi) + 1.0 - - np.log(nmax) + np.log(np.sum(wi * (res**2))))) + # log_lik = -0.5*nmax*(np.log(2.0 * np.pi) + 1.0 + np.log(np.sum((res**2)/nmax))) + log_lik = 0.5 * ( + np.sum(np.log(wi)) + - nmax + * ( + np.log(2.0 * np.pi) + + 1.0 + - np.log(nmax) + + np.log(np.sum(wi * (res**2))) + ) + ) # Aikaike's Information Criterion - AIC = -2.0*log_lik + 2.0*K + AIC = -2.0 * log_lik + 2.0 * K if AICc: # Second-Order AIC correcting for small sample sizes (restricted) # Burnham and Anderson (2002) advocate use of AICc where # ratio num/K is small # A small ratio is defined in the definition at approximately < 40 - AIC += (2.0*K*(K+1.0))/(nmax - K - 1.0) + AIC += (2.0 * K * (K + 1.0)) / (nmax - K - 1.0) # Bayesian Information Criterion (Schwarz Criterion) - BIC = -2.0*log_lik + np.log(nmax)*K + BIC = -2.0 * log_lik + np.log(nmax) * K # Error Analysis if WEIGHT: # WEIGHTED LEAST-SQUARES CASE (unequal error) # Covariance Matrix - Hinv = np.linalg.inv(np.dot(np.transpose(DMAT),np.dot(W,DMAT))) + Hinv = np.linalg.inv(np.dot(np.transpose(DMAT), np.dot(W, DMAT))) # Normal Equations - NORMEQ = np.dot(Hinv,np.transpose(np.dot(W,DMAT))) + NORMEQ = np.dot(Hinv, np.transpose(np.dot(W, DMAT))) temp_err = np.zeros((n_terms)) # Propagating RMS errors - for i in range(0,n_terms): - temp_err[i] = np.sqrt(np.sum((NORMEQ[i,:]*DATA_ERR)**2)) + for i in range(0, n_terms): + temp_err[i] = np.sqrt(np.sum((NORMEQ[i, :] * DATA_ERR) ** 2)) # Recalculating beta2 error beta_err = np.copy(temp_err) beta_err[2] = np.hypot(temp_err[1], temp_err[2]) # Weighted sum of squares Error - WSSE = np.dot(np.transpose(wi*(d_in[0:nmax] - np.dot(DMAT,beta_mat))), - wi*(d_in[0:nmax] - np.dot(DMAT,beta_mat)))/np.float64(nu) + WSSE = np.dot( + np.transpose(wi * (d_in[0:nmax] - np.dot(DMAT, beta_mat))), + wi * (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) / np.float64(nu) - return {'beta':beta_out, 'error':beta_err, 'R2':rsquare, - 'R2Adj':rsq_adj, 'WSSE':WSSE, 'AIC':AIC, 'BIC':BIC, - 'LOGLIK':log_lik, 'model':mod, 'residual':res, - 'N':n_terms, 'DOF':nu, 'cov_mat':Hinv} + return { + 'beta': beta_out, + 'error': beta_err, + 'R2': rsquare, + 'R2Adj': rsq_adj, + 'WSSE': WSSE, + 'AIC': AIC, + 'BIC': BIC, + 'LOGLIK': log_lik, + 'model': mod, + 'residual': res, + 'N': n_terms, + 'DOF': nu, + 'cov_mat': Hinv, + } - elif ((not WEIGHT) and (DATA_ERR != 0)): + elif (not WEIGHT) and (DATA_ERR != 0): # LEAST-SQUARES CASE WITH KNOWN AND EQUAL ERROR - P_err = DATA_ERR*np.ones((nmax)) - Hinv = np.linalg.inv(np.dot(np.transpose(DMAT),DMAT)) + P_err = DATA_ERR * np.ones((nmax)) + Hinv = np.linalg.inv(np.dot(np.transpose(DMAT), DMAT)) # Normal Equations - NORMEQ = np.dot(Hinv,np.transpose(DMAT)) + NORMEQ = np.dot(Hinv, np.transpose(DMAT)) temp_err = np.zeros((n_terms)) - for i in range(0,n_terms): - temp_err[i] = np.sum((NORMEQ[i,:]*P_err)**2) + for i in range(0, n_terms): + temp_err[i] = np.sum((NORMEQ[i, :] * P_err) ** 2) # Recalculating beta2 error beta_err = np.copy(temp_err) beta_err[2] = np.hypot(temp_err[1], temp_err[2]) # Mean square error - MSE = np.dot(np.transpose(d_in[0:nmax] - np.dot(DMAT,beta_mat)), - (d_in[0:nmax] - np.dot(DMAT,beta_mat)))/np.float64(nu) + MSE = np.dot( + np.transpose(d_in[0:nmax] - np.dot(DMAT, beta_mat)), + (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) / np.float64(nu) - return {'beta':beta_out, 'error':beta_err, 'R2':rsquare, - 'R2Adj':rsq_adj, 'MSE':MSE, 'AIC':AIC, 'BIC':BIC, - 'LOGLIK':log_lik, 'model':mod, 'residual':res, - 'N':n_terms, 'DOF':nu, 'cov_mat':Hinv} + return { + 'beta': beta_out, + 'error': beta_err, + 'R2': rsquare, + 'R2Adj': rsq_adj, + 'MSE': MSE, + 'AIC': AIC, + 'BIC': BIC, + 'LOGLIK': log_lik, + 'model': mod, + 'residual': res, + 'N': n_terms, + 'DOF': nu, + 'cov_mat': Hinv, + } else: # STANDARD LEAST-SQUARES CASE # Regression with Errors with Unknown Standard Deviations # MSE = (1/nu)*sum((Y-X*B)**2) # Mean square error - MSE = np.dot(np.transpose(d_in[0:nmax] - np.dot(DMAT,beta_mat)), - (d_in[0:nmax] - np.dot(DMAT,beta_mat)))/np.float64(nu) + MSE = np.dot( + np.transpose(d_in[0:nmax] - np.dot(DMAT, beta_mat)), + (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) / np.float64(nu) # Root mean square error RMSE = np.sqrt(MSE) # Normalized root mean square error - NRMSE = RMSE/(np.max(d_in[0:nmax])-np.min(d_in[0:nmax])) + NRMSE = RMSE / (np.max(d_in[0:nmax]) - np.min(d_in[0:nmax])) # Covariance Matrix # Multiplying the design matrix by itself - Hinv = np.linalg.inv(np.dot(np.transpose(DMAT),DMAT)) + Hinv = np.linalg.inv(np.dot(np.transpose(DMAT), DMAT)) # Taking the diagonal components of the cov matrix hdiag = np.diag(Hinv) # set either the standard deviation or the confidence interval - if (STDEV != 0): + if STDEV != 0: # Setting the standard deviation of the output error - alpha = 1.0 - scipy.special.erf(STDEV/np.sqrt(2.0)) - elif (CONF != 0): + alpha = 1.0 - scipy.special.erf(STDEV / np.sqrt(2.0)) + elif CONF != 0: # Setting the confidence interval of the output error alpha = 1.0 - CONF else: @@ -352,11 +404,11 @@ def piecewise(t_in, d_in, BREAK_TIME=None, BREAKPOINT=None, alpha = 1.0 - (0.95) # Student T-Distribution with D.O.F. nu # t.ppf parallels tinv in matlab - tstar = scipy.stats.t.ppf(1.0-(alpha/2.0),nu) + tstar = scipy.stats.t.ppf(1.0 - (alpha / 2.0), nu) # beta_err is the error for each coefficient # beta_err = t(nu,1-alpha/2)*standard error - temp_std = np.sqrt(MSE*hdiag) - temp_err = tstar*temp_std + temp_std = np.sqrt(MSE * hdiag) + temp_err = tstar * temp_std # Recalculating standard error for beta2 st_err = np.copy(temp_std) @@ -365,7 +417,21 @@ def piecewise(t_in, d_in, BREAK_TIME=None, BREAKPOINT=None, beta_err = np.copy(temp_err) beta_err[2] = np.hypot(temp_err[1], temp_err[2]) - return {'beta':beta_out, 'error':beta_err, 'std_err':st_err, 'R2':rsquare, - 'R2Adj':rsq_adj, 'MSE':MSE, 'NRMSE':NRMSE, 'AIC':AIC, 'BIC':BIC, - 'LOGLIK':log_lik, 'model':mod, 'simple': simple, 'residual':res, - 'N':n_terms, 'DOF': nu, 'cov_mat':Hinv} + return { + 'beta': beta_out, + 'error': beta_err, + 'std_err': st_err, + 'R2': rsquare, + 'R2Adj': rsq_adj, + 'MSE': MSE, + 'NRMSE': NRMSE, + 'AIC': AIC, + 'BIC': BIC, + 'LOGLIK': log_lik, + 'model': mod, + 'simple': simple, + 'residual': res, + 'N': n_terms, + 'DOF': nu, + 'cov_mat': Hinv, + } diff --git a/gravity_toolkit/time_series/regress.py b/gravity_toolkit/time_series/regress.py index bca9ecd4..ddb3d9c2 100755 --- a/gravity_toolkit/time_series/regress.py +++ b/gravity_toolkit/time_series/regress.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" regress.py Written by Tyler Sutterley (04/2023) @@ -101,13 +101,25 @@ Updated 01/2012: added std weighting for a error weighted least-squares Written 10/2011 """ + import numpy as np import scipy.stats import scipy.special -def regress(t_in, d_in, ORDER=1, CYCLES=[0.5,1.0], TERMS=[], - DATA_ERR=0, WEIGHT=False, RELATIVE=Ellipsis, STDEV=0, CONF=0, - AICc=True): + +def regress( + t_in, + d_in, + ORDER=1, + CYCLES=[0.5, 1.0], + TERMS=[], + DATA_ERR=0, + WEIGHT=False, + RELATIVE=Ellipsis, + STDEV=0, + CONF=0, + AICc=True, +): r""" Fits a synthetic signal to data over a time period by ordinary or weighted least-squares @@ -196,19 +208,19 @@ def regress(t_in, d_in, ORDER=1, CYCLES=[0.5,1.0], TERMS=[], t_rel = t_in[RELATIVE].mean() elif isinstance(RELATIVE, (float, int, np.float64, np.int_)): t_rel = np.copy(RELATIVE) - elif (RELATIVE == Ellipsis): + elif RELATIVE == Ellipsis: t_rel = t_in[RELATIVE].mean() # create design matrix based on polynomial order and harmonics # with any additional fit terms DMAT = [] # add polynomial orders (0=constant, 1=linear, 2=quadratic) - for o in range(ORDER+1): - DMAT.append((t_in-t_rel)**o) + for o in range(ORDER + 1): + DMAT.append((t_in - t_rel) ** o) # add cyclical terms (0.5=semi-annual, 1=annual) for c in CYCLES: - DMAT.append(np.sin(2.0*np.pi*t_in/np.float64(c))) - DMAT.append(np.cos(2.0*np.pi*t_in/np.float64(c))) + DMAT.append(np.sin(2.0 * np.pi * t_in / np.float64(c))) + DMAT.append(np.cos(2.0 * np.pi * t_in / np.float64(c))) # add additional terms to the design matrix for t in TERMS: DMAT.append(t) @@ -218,36 +230,36 @@ def regress(t_in, d_in, ORDER=1, CYCLES=[0.5,1.0], TERMS=[], # Calculating Least-Squares Coefficients if WEIGHT: # Weighted Least-Squares fitting - if (np.ndim(DATA_ERR) == 0): + if np.ndim(DATA_ERR) == 0: raise ValueError('Input DATA_ERR for Weighted Least-Squares') # check if any error values are 0 (prevent infinite weights) if np.count_nonzero(DATA_ERR == 0.0): # change to minimum floating point value DATA_ERR[DATA_ERR == 0.0] = np.finfo(np.float64).eps # Weight Precision - wi = np.squeeze(DATA_ERR**(-2)) + wi = np.squeeze(DATA_ERR ** (-2)) # If uncorrelated weights are the diagonal W = np.diag(wi) # Least-Squares fitting # Temporary Matrix: Inv(X'.W.X) - TM1 = np.linalg.inv(np.dot(np.transpose(DMAT),np.dot(W,DMAT))) + TM1 = np.linalg.inv(np.dot(np.transpose(DMAT), np.dot(W, DMAT))) # Temporary Matrix: (X'.W.Y) - TM2 = np.dot(np.transpose(DMAT),np.dot(W,d_in)) + TM2 = np.dot(np.transpose(DMAT), np.dot(W, d_in)) # Least Squares Solutions: Inv(X'.W.X).(X'.W.Y) - beta_mat = np.dot(TM1,TM2) - else:# Standard Least-Squares fitting (the [0] denotes coefficients output) - beta_mat = np.linalg.lstsq(DMAT,d_in,rcond=-1)[0] + beta_mat = np.dot(TM1, TM2) + else: # Standard Least-Squares fitting (the [0] denotes coefficients output) + beta_mat = np.linalg.lstsq(DMAT, d_in, rcond=-1)[0] # Weights are equal wi = 1.0 # number of terms in least-squares solution n_terms = len(beta_mat) # modelled time-series - mod = np.dot(DMAT,beta_mat) + mod = np.dot(DMAT, beta_mat) # residual - res = d_in[0:nmax] - np.dot(DMAT,beta_mat) + res = d_in[0:nmax] - np.dot(DMAT, beta_mat) # Fitted Values without (and with) climate oscillations - simple = np.dot(DMAT[:,0:(ORDER+1)],beta_mat[0:(ORDER+1)]) + simple = np.dot(DMAT[:, 0 : (ORDER + 1)], beta_mat[0 : (ORDER + 1)]) season = mod - simple # nu = Degrees of Freedom @@ -255,94 +267,138 @@ def regress(t_in, d_in, ORDER=1, CYCLES=[0.5,1.0], TERMS=[], # calculating R^2 values # SStotal = sum((Y-mean(Y))**2) - SStotal = np.dot(np.transpose(d_in[0:nmax] - np.mean(d_in[0:nmax])), - (d_in[0:nmax] - np.mean(d_in[0:nmax]))) + SStotal = np.dot( + np.transpose(d_in[0:nmax] - np.mean(d_in[0:nmax])), + (d_in[0:nmax] - np.mean(d_in[0:nmax])), + ) # SSerror = sum((Y-X*B)**2) - SSerror = np.dot(np.transpose(d_in[0:nmax] - np.dot(DMAT,beta_mat)), - (d_in[0:nmax] - np.dot(DMAT,beta_mat))) + SSerror = np.dot( + np.transpose(d_in[0:nmax] - np.dot(DMAT, beta_mat)), + (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) # R**2 term = 1- SSerror/SStotal - rsquare = 1.0 - (SSerror/SStotal) + rsquare = 1.0 - (SSerror / SStotal) # Adjusted R**2 term: weighted by degrees of freedom - rsq_adj = 1.0 - (SSerror/SStotal)*np.float64((nmax-1.0)/nu) + rsq_adj = 1.0 - (SSerror / SStotal) * np.float64((nmax - 1.0) / nu) # Fit Criterion # number of parameters including the intercept and the variance K = np.float64(n_terms + 1) # Log-Likelihood with weights (if unweighted, weight portions == 0) # log(L) = -0.5*n*log(sigma^2) - 0.5*n*log(2*pi) - 0.5*n - #log_lik = -0.5*nmax*(np.log(2.0 * np.pi) + 1.0 + np.log(np.sum((res**2)/nmax))) - log_lik = 0.5*(np.sum(np.log(wi)) - nmax*(np.log(2.0 * np.pi) + 1.0 - - np.log(nmax) + np.log(np.sum(wi * (res**2))))) + # log_lik = -0.5*nmax*(np.log(2.0 * np.pi) + 1.0 + np.log(np.sum((res**2)/nmax))) + log_lik = 0.5 * ( + np.sum(np.log(wi)) + - nmax + * ( + np.log(2.0 * np.pi) + + 1.0 + - np.log(nmax) + + np.log(np.sum(wi * (res**2))) + ) + ) # Aikaike's Information Criterion - AIC = -2.0*log_lik + 2.0*K + AIC = -2.0 * log_lik + 2.0 * K if AICc: # Second-Order AIC correcting for small sample sizes (restricted) # Burnham and Anderson (2002) advocate use of AICc where # ratio num/K is small # A small ratio is defined in the definition at approximately < 40 - AIC += (2.0*K*(K+1.0))/(nmax - K - 1.0) + AIC += (2.0 * K * (K + 1.0)) / (nmax - K - 1.0) # Bayesian Information Criterion (Schwarz Criterion) - BIC = -2.0*log_lik + np.log(nmax)*K + BIC = -2.0 * log_lik + np.log(nmax) * K # Error Analysis if WEIGHT: # WEIGHTED LEAST-SQUARES CASE (unequal error) # Covariance Matrix - Hinv = np.linalg.inv(np.dot(np.transpose(DMAT),np.dot(W,DMAT))) + Hinv = np.linalg.inv(np.dot(np.transpose(DMAT), np.dot(W, DMAT))) # Normal Equations - NORMEQ = np.dot(Hinv,np.transpose(np.dot(W,DMAT))) + NORMEQ = np.dot(Hinv, np.transpose(np.dot(W, DMAT))) beta_err = np.zeros((n_terms)) # Propagating RMS errors - for i in range(0,n_terms): - beta_err[i] = np.sqrt(np.sum((NORMEQ[i,:]*DATA_ERR)**2)) + for i in range(0, n_terms): + beta_err[i] = np.sqrt(np.sum((NORMEQ[i, :] * DATA_ERR) ** 2)) # Weighted sum of squares Error - WSSE = np.dot(np.transpose(wi*(d_in[0:nmax] - np.dot(DMAT,beta_mat))), - wi*(d_in[0:nmax] - np.dot(DMAT,beta_mat)))/np.float64(nu) + WSSE = np.dot( + np.transpose(wi * (d_in[0:nmax] - np.dot(DMAT, beta_mat))), + wi * (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) / np.float64(nu) - return {'beta':beta_mat, 'error':beta_err, 'R2':rsquare, - 'R2Adj':rsq_adj, 'WSSE':WSSE, 'AIC':AIC, 'BIC':BIC, - 'LOGLIK':log_lik, 'model':mod, 'residual':res, 'simple':simple, - 'season':season, 'N':n_terms, 'DOF':nu, 'cov_mat':Hinv} + return { + 'beta': beta_mat, + 'error': beta_err, + 'R2': rsquare, + 'R2Adj': rsq_adj, + 'WSSE': WSSE, + 'AIC': AIC, + 'BIC': BIC, + 'LOGLIK': log_lik, + 'model': mod, + 'residual': res, + 'simple': simple, + 'season': season, + 'N': n_terms, + 'DOF': nu, + 'cov_mat': Hinv, + } - elif ((not WEIGHT) and (DATA_ERR != 0)): + elif (not WEIGHT) and (DATA_ERR != 0): # LEAST-SQUARES CASE WITH KNOWN AND EQUAL ERROR - P_err = DATA_ERR*np.ones((nmax)) - Hinv = np.linalg.inv(np.dot(np.transpose(DMAT),DMAT)) + P_err = DATA_ERR * np.ones((nmax)) + Hinv = np.linalg.inv(np.dot(np.transpose(DMAT), DMAT)) # Normal Equations - NORMEQ = np.dot(Hinv,np.transpose(DMAT)) + NORMEQ = np.dot(Hinv, np.transpose(DMAT)) beta_err = np.zeros((n_terms)) - for i in range(0,n_terms): - beta_err[i] = np.sqrt(np.sum((NORMEQ[i,:]*P_err)**2)) + for i in range(0, n_terms): + beta_err[i] = np.sqrt(np.sum((NORMEQ[i, :] * P_err) ** 2)) # Mean square error - MSE = np.dot(np.transpose(d_in[0:nmax] - np.dot(DMAT,beta_mat)), - (d_in[0:nmax] - np.dot(DMAT,beta_mat)))/np.float64(nu) + MSE = np.dot( + np.transpose(d_in[0:nmax] - np.dot(DMAT, beta_mat)), + (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) / np.float64(nu) - return {'beta':beta_mat, 'error':beta_err, 'R2':rsquare, - 'R2Adj':rsq_adj, 'MSE':MSE, 'AIC':AIC, 'BIC':BIC, - 'LOGLIK':log_lik, 'model':mod, 'residual':res, 'simple':simple, - 'season':season,'N':n_terms, 'DOF':nu, 'cov_mat':Hinv} + return { + 'beta': beta_mat, + 'error': beta_err, + 'R2': rsquare, + 'R2Adj': rsq_adj, + 'MSE': MSE, + 'AIC': AIC, + 'BIC': BIC, + 'LOGLIK': log_lik, + 'model': mod, + 'residual': res, + 'simple': simple, + 'season': season, + 'N': n_terms, + 'DOF': nu, + 'cov_mat': Hinv, + } else: # STANDARD LEAST-SQUARES CASE # Regression with Errors with Unknown Standard Deviations # MSE = (1/nu)*sum((Y-X*B)**2) # Mean square error - MSE = np.dot(np.transpose(d_in[0:nmax] - np.dot(DMAT,beta_mat)), - (d_in[0:nmax] - np.dot(DMAT,beta_mat)))/np.float64(nu) + MSE = np.dot( + np.transpose(d_in[0:nmax] - np.dot(DMAT, beta_mat)), + (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) / np.float64(nu) # Root mean square error RMSE = np.sqrt(MSE) # Normalized root mean square error - NRMSE = RMSE/(np.max(d_in[0:nmax])-np.min(d_in[0:nmax])) + NRMSE = RMSE / (np.max(d_in[0:nmax]) - np.min(d_in[0:nmax])) # Covariance Matrix # Multiplying the design matrix by itself - Hinv = np.linalg.inv(np.dot(np.transpose(DMAT),DMAT)) + Hinv = np.linalg.inv(np.dot(np.transpose(DMAT), DMAT)) # Taking the diagonal components of the cov matrix hdiag = np.diag(Hinv) # set either the standard deviation or the confidence interval - if (STDEV != 0): + if STDEV != 0: # Setting the standard deviation of the output error - alpha = 1.0 - scipy.special.erf(STDEV/np.sqrt(2.0)) - elif (CONF != 0): + alpha = 1.0 - scipy.special.erf(STDEV / np.sqrt(2.0)) + elif CONF != 0: # Setting the confidence interval of the output error alpha = 1.0 - CONF else: @@ -350,13 +406,28 @@ def regress(t_in, d_in, ORDER=1, CYCLES=[0.5,1.0], TERMS=[], alpha = 1.0 - (0.95) # Student T-Distribution with D.O.F. nu # t.ppf parallels tinv in matlab - tstar = scipy.stats.t.ppf(1.0-(alpha/2.0),nu) + tstar = scipy.stats.t.ppf(1.0 - (alpha / 2.0), nu) # beta_err is the error for each coefficient # beta_err = t(nu,1-alpha/2)*standard error - st_err = np.sqrt(MSE*hdiag) - beta_err = tstar*st_err + st_err = np.sqrt(MSE * hdiag) + beta_err = tstar * st_err - return {'beta':beta_mat, 'error':beta_err, 'std_err':st_err, 'R2':rsquare, - 'R2Adj':rsq_adj, 'MSE':MSE, 'NRMSE':NRMSE, 'AIC':AIC, 'BIC':BIC, - 'LOGLIK':log_lik, 'model':mod, 'residual':res, 'simple':simple, - 'season':season, 'N':n_terms, 'DOF':nu, 'cov_mat':Hinv} + return { + 'beta': beta_mat, + 'error': beta_err, + 'std_err': st_err, + 'R2': rsquare, + 'R2Adj': rsq_adj, + 'MSE': MSE, + 'NRMSE': NRMSE, + 'AIC': AIC, + 'BIC': BIC, + 'LOGLIK': log_lik, + 'model': mod, + 'residual': res, + 'simple': simple, + 'season': season, + 'N': n_terms, + 'DOF': nu, + 'cov_mat': Hinv, + } diff --git a/gravity_toolkit/time_series/savitzky_golay.py b/gravity_toolkit/time_series/savitzky_golay.py index 42ea690f..6ed22a78 100644 --- a/gravity_toolkit/time_series/savitzky_golay.py +++ b/gravity_toolkit/time_series/savitzky_golay.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" savitzky_golay.py Written by Tyler Sutterley (01/2023) Adapted from Numerical Recipes, Third Edition @@ -57,13 +57,16 @@ Updated 08/2015: changed sys.exit to raise ValueError Written 06/2014 """ + from __future__ import division import numpy as np import scipy.special -def savitzky_golay(t_in, y_in, WINDOW=None, ORDER=2, DERIV=0, - RATE=1, DATA_ERR=0): + +def savitzky_golay( + t_in, y_in, WINDOW=None, ORDER=2, DERIV=0, RATE=1, DATA_ERR=0 +): """ Smooth and optionally differentiate data with a Savitzky-Golay filter :cite:p:`Savitzky:1964bn,Press:1988we` @@ -101,43 +104,54 @@ def savitzky_golay(t_in, y_in, WINDOW=None, ORDER=2, DERIV=0, # verify that WINDOW is positive, odd and greater than ORDER+1 if WINDOW is None: - WINDOW = ORDER + -1*(ORDER % 2) + 3 + WINDOW = ORDER + -1 * (ORDER % 2) + 3 if WINDOW % 2 != 1 or WINDOW < 1: - raise ValueError("WINDOW size must be a positive odd number") + raise ValueError('WINDOW size must be a positive odd number') if WINDOW < ORDER + 2: - raise ValueError("WINDOW is too small for the polynomials order") + raise ValueError('WINDOW is too small for the polynomials order') # remove any singleton dimensions t_in = np.squeeze(t_in) y_in = np.squeeze(y_in) nmax = len(t_in) # order range - order_range = np.arange(ORDER+1) + order_range = np.arange(ORDER + 1) # filter half-window half_window = (WINDOW - 1) // 2 # output time-series (removing half-windows on ends) - t_out = t_in[half_window:nmax-half_window] + t_out = t_in[half_window : nmax - half_window] # output smoothed timeseries (or derivative) - y_out = np.zeros((nmax-2*half_window)) - y_err = np.zeros((nmax-2*half_window)) - for n in range(0, (nmax-(2*half_window))): - yran = y_in[n + np.arange(0, 2*half_window+1)] + y_out = np.zeros((nmax - 2 * half_window)) + y_err = np.zeros((nmax - 2 * half_window)) + for n in range(0, (nmax - (2 * half_window))): + yran = y_in[n + np.arange(0, 2 * half_window + 1)] # Vandermonde matrix for the time-series - b = np.mat([[(t_in[k]-t_in[n+half_window])**i for i in order_range] - for k in range(n, n+2*half_window+1)]) + b = np.mat( + [ + [(t_in[k] - t_in[n + half_window]) ** i for i in order_range] + for k in range(n, n + 2 * half_window + 1) + ] + ) # compute the pseudoinverse of the design matrix - m=np.linalg.pinv(b).A[DERIV]*RATE**DERIV*scipy.special.factorial(DERIV) + m = ( + np.linalg.pinv(b).A[DERIV] + * RATE**DERIV + * scipy.special.factorial(DERIV) + ) # pad the signal at the extremes with values taken from the signal - firstvals = yran[0] - np.abs(yran[1:half_window+1][::-1] - yran[0]) - lastvals = yran[-1] + np.abs(yran[-half_window-1:-1][::-1] - yran[-1]) + firstvals = yran[0] - np.abs(yran[1 : half_window + 1][::-1] - yran[0]) + lastvals = yran[-1] + np.abs( + yran[-half_window - 1 : -1][::-1] - yran[-1] + ) yn = np.concatenate((firstvals, yran, lastvals)) # compute the convolution and use middle value y_out[n] = np.convolve(m[::-1], yn, mode='valid')[half_window] - if (DATA_ERR != 0): + if DATA_ERR != 0: # if data error is known and of equal value - P_err = DATA_ERR*np.ones((4*half_window+1)) + P_err = DATA_ERR * np.ones((4 * half_window + 1)) # compute the convolution and use middle value - y_err[n] = np.sqrt(np.convolve(m[::-1]**2, P_err**2, - mode='valid')[half_window]) + y_err[n] = np.sqrt( + np.convolve(m[::-1] ** 2, P_err**2, mode='valid')[half_window] + ) - return {'data':y_out, 'error':y_err, 'time':t_out} + return {'data': y_out, 'error': y_err, 'time': t_out} diff --git a/gravity_toolkit/time_series/smooth.py b/gravity_toolkit/time_series/smooth.py index d80050e6..6ba1d0dd 100755 --- a/gravity_toolkit/time_series/smooth.py +++ b/gravity_toolkit/time_series/smooth.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" smooth.py Written by Tyler Sutterley (07/2026) @@ -78,12 +78,15 @@ Updated 03/2012: added Loess smoothing following Velicogna (2009) Written 12/2011 """ + import numpy as np import scipy.stats import scipy.special -def smooth(t_in, d_in, HFWTH=6, MOVING=False, DATA_ERR=0, WEIGHT=0, - STDEV=0, CONF=0): + +def smooth( + t_in, d_in, HFWTH=6, MOVING=False, DATA_ERR=0, WEIGHT=0, STDEV=0, CONF=0 +): """ Computes the moving average of a time-series @@ -145,10 +148,10 @@ def smooth(t_in, d_in, HFWTH=6, MOVING=False, DATA_ERR=0, WEIGHT=0, SEAS = 2 # set either the standard deviation or the confidence interval - if (STDEV != 0): + if STDEV != 0: # Setting the standard deviation of the output error - alpha = 1.0 - scipy.special.erf(STDEV/np.sqrt(2.0)) - elif (CONF != 0): + alpha = 1.0 - scipy.special.erf(STDEV / np.sqrt(2.0)) + elif CONF != 0: # Setting the confidence interval of the output error alpha = 1.0 - CONF else: @@ -161,14 +164,16 @@ def smooth(t_in, d_in, HFWTH=6, MOVING=False, DATA_ERR=0, WEIGHT=0, # equal to mean of Jan:Dec and Feb:Jan+1 for HFWTH 6 # problematic with GRACE due to missing months within time-series # output time - tout = t_in[HFWTH:nmax-HFWTH] - smth = np.zeros((nmax-2*HFWTH)) - for k in range(0, (nmax-(2*HFWTH))): + tout = t_in[HFWTH : nmax - HFWTH] + smth = np.zeros((nmax - 2 * HFWTH)) + for k in range(0, (nmax - (2 * HFWTH))): # centered moving average sum[2:i-1] + 0.5[1] + 0.5[i] - smth[k] = np.sum(d_in[k+1:k+2*HFWTH]) + 0.5*(d_in[k]+d_in[k+2*HFWTH]) - dsmth = smth/(2*HFWTH) - return {'data':dsmth, 'time':tout} - elif WEIGHT in (1,2): + smth[k] = np.sum(d_in[k + 1 : k + 2 * HFWTH]) + 0.5 * ( + d_in[k] + d_in[k + 2 * HFWTH] + ) + dsmth = smth / (2 * HFWTH) + return {'data': dsmth, 'time': tout} + elif WEIGHT in (1, 2): # weighted moving average calculated from the least-squares of window # and removing An/SAn signal. models entire range of dates # for a HFWTH of 6 (remove annual) @@ -179,18 +184,25 @@ def smooth(t_in, d_in, HFWTH=6, MOVING=False, DATA_ERR=0, WEIGHT=0, # smoothed time-series = sum(smth*weights)/sum(weights)the weight array # output time = input time tout = np.copy(t_in) - if (WEIGHT == 1): + if WEIGHT == 1: # linear weights (range from 1:HFWTH+1:-1) - wi = np.concatenate((np.arange(1,HFWTH+2,dtype=np.float64), - np.arange(HFWTH,0,-1,dtype=np.float64)),axis=0) - elif (WEIGHT == 2): + wi = np.concatenate( + ( + np.arange(1, HFWTH + 2, dtype=np.float64), + np.arange(HFWTH, 0, -1, dtype=np.float64), + ), + axis=0, + ) + elif WEIGHT == 2: # gaussian weights # default standard deviation of 2 stdev = 2.0 # gaussian function over range 2*HFWTH # centered on HFWTH - xi=np.arange(0, 2*HFWTH+1) - wi=np.exp(-(xi-HFWTH)**2/(2.0*stdev**2))/(stdev*np.sqrt(2.0*np.pi)) + xi = np.arange(0, 2 * HFWTH + 1) + wi = np.exp(-((xi - HFWTH) ** 2) / (2.0 * stdev**2)) / ( + stdev * np.sqrt(2.0 * np.pi) + ) dsmth = np.zeros((nmax)) dseason = np.zeros((nmax)) @@ -201,37 +213,37 @@ def smooth(t_in, d_in, HFWTH=6, MOVING=False, DATA_ERR=0, WEIGHT=0, semiamp = np.zeros((nmax)) semiphase = np.zeros((nmax)) weight = np.zeros((nmax)) - for i in range(0, (nmax-(2*HFWTH))): - ran = i + np.arange(0, 2*HFWTH+1) - P_x0 = np.ones((2*HFWTH+1))# Constant Term - P_x1 = t_in[ran]# Linear Term + for i in range(0, (nmax - (2 * HFWTH))): + ran = i + np.arange(0, 2 * HFWTH + 1) + P_x0 = np.ones((2 * HFWTH + 1)) # Constant Term + P_x1 = t_in[ran] # Linear Term # Annual term = 2*pi*t*harmonic - P_asin = np.sin(2*np.pi*t_in[ran]) - P_acos = np.cos(2*np.pi*t_in[ran]) - #Semi-Annual = 4*pi*t*harmonic - P_ssin = np.sin(4*np.pi*t_in[ran]) - P_scos = np.cos(4*np.pi*t_in[ran]) + P_asin = np.sin(2 * np.pi * t_in[ran]) + P_acos = np.cos(2 * np.pi * t_in[ran]) + # Semi-Annual = 4*pi*t*harmonic + P_ssin = np.sin(4 * np.pi * t_in[ran]) + P_scos = np.cos(4 * np.pi * t_in[ran]) # x0,x1,AS,AC,SS,SC TMAT = np.array([P_x0, P_x1, P_asin, P_acos, P_ssin, P_scos]) TMAT = np.transpose(TMAT) # Least-Squares fitting # (the [0] denotes coefficients output)standard - beta_mat = np.linalg.lstsq(TMAT,d_in[ran],rcond=-1)[0] + beta_mat = np.linalg.lstsq(TMAT, d_in[ran], rcond=-1)[0] # Calculating the output components # add weighted smoothed time series - dsmth[ran] += wi*np.dot(TMAT[:,0:SEAS],beta_mat[0:SEAS]) + dsmth[ran] += wi * np.dot(TMAT[:, 0:SEAS], beta_mat[0:SEAS]) # seasonal component - dseason[ran] += wi*np.dot(TMAT[:,SEAS:],beta_mat[SEAS:]) + dseason[ran] += wi * np.dot(TMAT[:, SEAS:], beta_mat[SEAS:]) # annual component - AS,AC = beta_mat[SEAS:SEAS+2] - dannual[ran] += wi*np.dot(TMAT[:,SEAS:SEAS+2],[AS,AC]) - annamp[ran] += wi*np.hypot(AS, AC) - annphase[ran] += wi*np.degrees(np.arctan2(AC, AS)) + AS, AC = beta_mat[SEAS : SEAS + 2] + dannual[ran] += wi * np.dot(TMAT[:, SEAS : SEAS + 2], [AS, AC]) + annamp[ran] += wi * np.hypot(AS, AC) + annphase[ran] += wi * np.degrees(np.arctan2(AC, AS)) # semi-annual component - SS,SC = beta_mat[SEAS+2:SEAS+4] - dsemian[ran] += wi*np.dot(TMAT[:,SEAS+2:SEAS+4],[SS,SC]) - semiamp[ran] += wi*np.hypot(SS, SC) - semiphase[ran] += wi*np.degrees(np.arctan2(SC, SS)) + SS, SC = beta_mat[SEAS + 2 : SEAS + 4] + dsemian[ran] += wi * np.dot(TMAT[:, SEAS + 2 : SEAS + 4], [SS, SC]) + semiamp[ran] += wi * np.hypot(SS, SC) + semiphase[ran] += wi * np.degrees(np.arctan2(SC, SS)) # add weights weight[ran] += wi # divide weighted smoothed time-series by weights @@ -246,64 +258,78 @@ def smooth(t_in, d_in, HFWTH=6, MOVING=False, DATA_ERR=0, WEIGHT=0, semiphase /= weight # noise = data - smoothed - seasonal dnoise = d_in - dsmth - dseason - return {'data':dsmth, 'seasonal':dseason, 'annual':dannual, - 'annamp':annamp, 'annphase':annphase, 'semiann':dsemian, - 'semiamp':semiamp, 'semiphase':semiphase, 'noise':dnoise, - 'time':tout, 'weight':weight} + return { + 'data': dsmth, + 'seasonal': dseason, + 'annual': dannual, + 'annamp': annamp, + 'annphase': annphase, + 'semiann': dsemian, + 'semiamp': semiamp, + 'semiphase': semiphase, + 'noise': dnoise, + 'time': tout, + 'weight': weight, + } else: # Moving average calculated from least-squares of window # and removing An/SAn signal # output time - tout = t_in[HFWTH:nmax-HFWTH] - dsmth = np.zeros((nmax-2*HFWTH)) - dtrend = np.zeros((nmax-2*HFWTH)) - derror = np.zeros((nmax-2*HFWTH)) - dseason = np.zeros((nmax-2*HFWTH)) - dannual = np.zeros((nmax-2*HFWTH)) - annamp = np.zeros((nmax-2*HFWTH)) - annphase = np.zeros((nmax-2*HFWTH)) - dsemian = np.zeros((nmax-2*HFWTH)) - semiamp = np.zeros((nmax-2*HFWTH)) - semiphase = np.zeros((nmax-2*HFWTH)) - dnoise = np.zeros((nmax-2*HFWTH)) - dreduce = np.zeros((nmax-2*HFWTH)) - for i in range(0, (nmax-(2*HFWTH))): - ran = i + np.arange(0, 2*HFWTH+1) - P_x0 = np.ones((2*HFWTH+1))# Constant Term - P_x1 = t_in[ran]# Linear Term + tout = t_in[HFWTH : nmax - HFWTH] + dsmth = np.zeros((nmax - 2 * HFWTH)) + dtrend = np.zeros((nmax - 2 * HFWTH)) + derror = np.zeros((nmax - 2 * HFWTH)) + dseason = np.zeros((nmax - 2 * HFWTH)) + dannual = np.zeros((nmax - 2 * HFWTH)) + annamp = np.zeros((nmax - 2 * HFWTH)) + annphase = np.zeros((nmax - 2 * HFWTH)) + dsemian = np.zeros((nmax - 2 * HFWTH)) + semiamp = np.zeros((nmax - 2 * HFWTH)) + semiphase = np.zeros((nmax - 2 * HFWTH)) + dnoise = np.zeros((nmax - 2 * HFWTH)) + dreduce = np.zeros((nmax - 2 * HFWTH)) + for i in range(0, (nmax - (2 * HFWTH))): + ran = i + np.arange(0, 2 * HFWTH + 1) + P_x0 = np.ones((2 * HFWTH + 1)) # Constant Term + P_x1 = t_in[ran] # Linear Term # Annual term = 2*pi*t*harmonic - P_asin = np.sin(2*np.pi*t_in[ran]) - P_acos = np.cos(2*np.pi*t_in[ran]) - #Semi-Annual = 4*pi*t*harmonic - P_ssin = np.sin(4*np.pi*t_in[ran]) - P_scos = np.cos(4*np.pi*t_in[ran]) + P_asin = np.sin(2 * np.pi * t_in[ran]) + P_acos = np.cos(2 * np.pi * t_in[ran]) + # Semi-Annual = 4*pi*t*harmonic + P_ssin = np.sin(4 * np.pi * t_in[ran]) + P_scos = np.cos(4 * np.pi * t_in[ran]) # x0,x1,AS,AC,SS,SC TMAT = np.array([P_x0, P_x1, P_asin, P_acos, P_ssin, P_scos]) TMAT = np.transpose(TMAT) # Least-Squares fitting # (the [0] denotes coefficients output) - beta_mat = np.linalg.lstsq(TMAT,d_in[ran],rcond=-1)[0] + beta_mat = np.linalg.lstsq(TMAT, d_in[ran], rcond=-1)[0] n_terms = len(beta_mat) - if (DATA_ERR != 0): + if DATA_ERR != 0: # LEAST-SQUARES CASE WITH KNOWN AND EQUAL ERROR - P_err = DATA_ERR*np.ones((2*HFWTH+1)) - Hinv = np.linalg.inv(np.dot(np.transpose(TMAT),TMAT)) + P_err = DATA_ERR * np.ones((2 * HFWTH + 1)) + Hinv = np.linalg.inv(np.dot(np.transpose(TMAT), TMAT)) # Normal Equations - NORMEQ = np.dot(Hinv,np.transpose(TMAT)) + NORMEQ = np.dot(Hinv, np.transpose(TMAT)) beta_err = np.zeros((n_terms)) - for n in range(0,n_terms): - beta_err[n] = np.sqrt(np.sum((NORMEQ[n,:]*P_err)**2)) + for n in range(0, n_terms): + beta_err[n] = np.sqrt(np.sum((NORMEQ[n, :] * P_err) ** 2)) else: # Error Analysis # Degrees of Freedom - nu = (2*HFWTH+1) - n_terms + nu = (2 * HFWTH + 1) - n_terms # Mean square error - MSE = np.dot(np.transpose(d_in[ran] - np.dot(TMAT,beta_mat)), - (d_in[ran] - np.dot(TMAT,beta_mat)))/nu + MSE = ( + np.dot( + np.transpose(d_in[ran] - np.dot(TMAT, beta_mat)), + (d_in[ran] - np.dot(TMAT, beta_mat)), + ) + / nu + ) # Covariance Matrix # Multiplying the design matrix by itself - Hinv = np.linalg.inv(np.dot(np.transpose(TMAT),TMAT)) + Hinv = np.linalg.inv(np.dot(np.transpose(TMAT), TMAT)) # Taking the diagonal components of the cov matrix hdiag = np.diag(Hinv) @@ -311,35 +337,46 @@ def smooth(t_in, d_in, HFWTH=6, MOVING=False, DATA_ERR=0, WEIGHT=0, # Regression with Errors with Unknown Standard Deviations # Student T-Distribution with D.O.F. nu # t.ppf parallels tinv in matlab - tstar = scipy.stats.t.ppf(1.0-(alpha/2.0),nu) + tstar = scipy.stats.t.ppf(1.0 - (alpha / 2.0), nu) # beta_err is the error for each coefficient # beta_err = t(nu,1-alpha/2)*standard error - st_err = np.sqrt(MSE*hdiag) - beta_err = tstar*st_err + st_err = np.sqrt(MSE * hdiag) + beta_err = tstar * st_err # Calculating the output components # smoothed time series - dsmth[i] = np.dot(TMAT[HFWTH,0:SEAS],beta_mat[0:SEAS]) - dtrend[i] = np.copy(beta_mat[1])# Instantaneous data trend - derror[i] = np.copy(beta_err[1])# Error in trend + dsmth[i] = np.dot(TMAT[HFWTH, 0:SEAS], beta_mat[0:SEAS]) + dtrend[i] = np.copy(beta_mat[1]) # Instantaneous data trend + derror[i] = np.copy(beta_err[1]) # Error in trend # seasonal component - dseason[i] = np.dot(TMAT[HFWTH,SEAS:],beta_mat[SEAS:]) + dseason[i] = np.dot(TMAT[HFWTH, SEAS:], beta_mat[SEAS:]) # annual component - AS,AC = beta_mat[SEAS:SEAS+2] - dannual[i] = np.dot(TMAT[HFWTH,SEAS:SEAS+2],[AS,AC]) + AS, AC = beta_mat[SEAS : SEAS + 2] + dannual[i] = np.dot(TMAT[HFWTH, SEAS : SEAS + 2], [AS, AC]) annphase[i] = np.degrees(np.arctan2(AC, AS)) annamp[i] = np.hypot(AS, AC) # semi-annual component - SS,SC = beta_mat[SEAS+2:SEAS+4] - dsemian[i] = np.dot(TMAT[HFWTH,SEAS+2:SEAS+4],[SS,SC]) + SS, SC = beta_mat[SEAS + 2 : SEAS + 4] + dsemian[i] = np.dot(TMAT[HFWTH, SEAS + 2 : SEAS + 4], [SS, SC]) semiamp[i] = np.hypot(SS, SC) semiphase[i] = np.degrees(np.arctan2(SC, SS)) # noise component - dnoise[i] = d_in[i+HFWTH] - dsmth[i] - dseason[i] + dnoise[i] = d_in[i + HFWTH] - dsmth[i] - dseason[i] # reduced time-series - dreduce[i] = d_in[i+HFWTH] + dreduce[i] = d_in[i + HFWTH] - return {'data':dsmth, 'trend':dtrend, 'error':derror, - 'seasonal':dseason, 'annual':dannual, 'annphase':annphase, - 'annamp':annamp, 'semiann':dsemian, 'semiamp':semiamp, - 'semiphase':semiphase, 'noise':dnoise, 'time':tout, 'reduce':dreduce} + return { + 'data': dsmth, + 'trend': dtrend, + 'error': derror, + 'seasonal': dseason, + 'annual': dannual, + 'annphase': annphase, + 'annamp': annamp, + 'semiann': dsemian, + 'semiamp': semiamp, + 'semiphase': semiphase, + 'noise': dnoise, + 'time': tout, + 'reduce': dreduce, + } diff --git a/gravity_toolkit/tools.py b/gravity_toolkit/tools.py index 16cc786a..c31d511a 100644 --- a/gravity_toolkit/tools.py +++ b/gravity_toolkit/tools.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" tools.py Written by Tyler Sutterley (07/2026) Jupyter notebook, user interface and plotting tools @@ -30,7 +30,7 @@ Updated 07/2026: use np.radians and np.degrees for angle conversions Updated 11/2024: fix deprecated widget object copies Updated 04/2024: add widget for setting endpoint for accessing PODAAC data - place colormap registration within try/except to check for existing + place colormap registration within try/except to check for existing Updated 05/2023: use pathlib to define and operate on paths Updated 03/2023: add wrap longitudes function to change convention improve typing for variables in docstrings @@ -42,6 +42,7 @@ Updated 12/2021: added custom colormap function for some common scales Written 09/2021 """ + import os import re import copy @@ -58,32 +59,32 @@ try: import ipywidgets except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("ipywidgets not available", ImportWarning) + warnings.warn('ipywidgets not available', ImportWarning) try: import matplotlib.cm as cm import matplotlib.colors as colors except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: from tkinter import Tk, filedialog except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("tkinter not available", ImportWarning) + warnings.warn('tkinter not available', ImportWarning) filedialog = None try: import IPython.display except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("IPython.display not available", ImportWarning) + warnings.warn('IPython.display not available', ImportWarning) # ignore warnings warnings.filterwarnings('ignore') + # widgets for Jupyter notebooks class widgets: def __init__(self, **kwargs): - """Widgets and functions for running GRACE/GRACE-FO analyses - """ + """Widgets and functions for running GRACE/GRACE-FO analyses""" # set default keyword arguments kwargs.setdefault('directory', pathlib.Path.cwd()) - kwargs.setdefault('defaults', ['CSR','RL06','GSM',60]) + kwargs.setdefault('defaults', ['CSR', 'RL06', 'GSM', 60]) kwargs.setdefault('style', {}) # set style self.style = copy.copy(kwargs['style']) @@ -114,16 +115,15 @@ def select_directory(self, **kwargs): ) # button and label for directory selection self.directory_button = ipywidgets.Button( - description="Directory select", - mustexist="False", - width="30%", + description='Directory select', + mustexist='False', + width='30%', ) # create hbox of directory selection - if os.environ.get("DISPLAY") and (filedialog is not None): - self.directory = ipywidgets.HBox([ - self.directory_label, - self.directory_button - ]) + if os.environ.get('DISPLAY') and (filedialog is not None): + self.directory = ipywidgets.HBox( + [self.directory_label, self.directory_button] + ) else: self.directory = self.directory_label # connect directory select button with action @@ -149,8 +149,7 @@ def select_directory(self, **kwargs): ) def set_directory(self, b): - """function for directory selection - """ + """function for directory selection""" IPython.display.clear_output() root = Tk() root.withdraw() @@ -208,12 +207,15 @@ def select_product(self): ) # find available months for data product - total_months = grace_find_months(self.base_directory, - self.center.value, self.release.value, - DSET=self.product.value) + total_months = grace_find_months( + self.base_directory, + self.center.value, + self.release.value, + DSET=self.product.value, + ) # select months to run # https://tsutterley.github.io/data/GRACE-Months.html - options=[str(m).zfill(3) for m in total_months['months']] + options = [str(m).zfill(3) for m in total_months['months']] self.months = ipywidgets.SelectMultiple( options=options, value=options, @@ -229,23 +231,20 @@ def select_product(self): self.center.observe(self.update_months) self.release.observe(self.update_months) - # function for setting the data release def set_release(self, sender): - """function for updating available releases - """ - if (self.center.value == 'CNES'): - releases = ['RL01','RL02','RL03', 'RL04', 'RL05'] + """function for updating available releases""" + if self.center.value == 'CNES': + releases = ['RL01', 'RL02', 'RL03', 'RL04', 'RL05'] else: releases = ['RL04', 'RL05', 'RL06'] - self.release.options=releases - self.release.value=releases[-1] + self.release.options = releases + self.release.value = releases[-1] # function for setting the data product def set_product(self, sender): - """function for updating available products - """ - if (self.center.value == 'CNES'): + """function for updating available products""" + if self.center.value == 'CNES': products = {} products['RL01'] = ['GAC', 'GSM'] products['RL02'] = ['GAA', 'GAB', 'GSM'] @@ -253,24 +252,26 @@ def set_product(self, sender): products['RL04'] = ['GSM'] products['RL05'] = ['GAA', 'GAB', 'GSM'] valid_products = products[self.release.value] - elif (self.center.value == 'CSR'): + elif self.center.value == 'CSR': valid_products = ['GAC', 'GAD', 'GSM'] - elif (self.center.value in ('GFZ','JPL')): + elif self.center.value in ('GFZ', 'JPL'): valid_products = ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'] - self.product.options=valid_products - self.product.value=self.defaults[2] + self.product.options = valid_products + self.product.value = self.defaults[2] # function for updating the available months def update_months(self, sender): - """function for updating available months - """ + """function for updating available months""" # https://tsutterley.github.io/data/GRACE-Months.html - total_months = grace_find_months(self.base_directory, - self.center.value, self.release.value, - DSET=self.product.value) - options=[str(m).zfill(3) for m in total_months['months']] - self.months.options=options - self.months.value=options + total_months = grace_find_months( + self.base_directory, + self.center.value, + self.release.value, + DSET=self.product.value, + ) + options = [str(m).zfill(3) for m in total_months['months']] + self.months.options = options + self.months.value = options def select_options(self, **kwargs): r""" @@ -341,7 +342,7 @@ def select_options(self, **kwargs): # SLR C20 C20_default = 'GSFC' if (self.product.value == 'GSM') else '[none]' self.C20 = ipywidgets.Dropdown( - options=['[none]','CSR','GSFC'], + options=['[none]', 'CSR', 'GSFC'], value=C20_default, description='SLR C20:', disabled=False, @@ -350,7 +351,7 @@ def select_options(self, **kwargs): # SLR C21 and S21 self.CS21 = ipywidgets.Dropdown( - options=['[none]','CSR'], + options=['[none]', 'CSR'], value='[none]', description='SLR CS21:', disabled=False, @@ -359,7 +360,7 @@ def select_options(self, **kwargs): # SLR C22 and S22 self.CS22 = ipywidgets.Dropdown( - options=['[none]','CSR'], + options=['[none]', 'CSR'], value='[none]', description='SLR CS22:', disabled=False, @@ -369,7 +370,7 @@ def select_options(self, **kwargs): # SLR C30 C30_default = 'GSFC' if (self.product.value == 'GSM') else '[none]' self.C30 = ipywidgets.Dropdown( - options=['[none]','CSR','GSFC'], + options=['[none]', 'CSR', 'GSFC'], value=C30_default, description='SLR C30:', disabled=False, @@ -378,7 +379,7 @@ def select_options(self, **kwargs): # SLR C40 self.C40 = ipywidgets.Dropdown( - options=['[none]','CSR','GSFC'], + options=['[none]', 'CSR', 'GSFC'], value='[none]', description='SLR C40:', disabled=False, @@ -387,7 +388,7 @@ def select_options(self, **kwargs): # SLR C50 self.C50 = ipywidgets.Dropdown( - options=['[none]','CSR','GSFC'], + options=['[none]', 'CSR', 'GSFC'], value='[none]', description='SLR C50:', disabled=False, @@ -395,8 +396,13 @@ def select_options(self, **kwargs): ) # Pole Tide Drift (Wahr et al., 2015) for Release-5 - poletide_default = True if ((self.release.value == 'RL05') - and (self.product.value == 'GSM')) else False + poletide_default = ( + True + if ( + (self.release.value == 'RL05') and (self.product.value == 'GSM') + ) + else False + ) self.pole_tide = ipywidgets.Checkbox( value=poletide_default, description='Pole Tide Corrections', @@ -426,39 +432,40 @@ def select_options(self, **kwargs): # function for setting the spherical harmonic degree def set_max_degree(self, sender): - """function for setting max degree of a product - """ - if (self.center == 'CNES'): - LMAX = dict(RL01=50,RL02=50,RL03=80,RL04=90,RL05=90) - elif (self.center in ('CSR','JPL')): + """function for setting max degree of a product""" + if self.center == 'CNES': + LMAX = dict(RL01=50, RL02=50, RL03=80, RL04=90, RL05=90) + elif self.center in ('CSR', 'JPL'): # CSR RL04/5/6 at LMAX 60 # JPL RL04/5/6 at LMAX 60 - LMAX = dict(RL04=60,RL05=60,RL06=60) - elif (self.center == 'GFZ'): + LMAX = dict(RL04=60, RL05=60, RL06=60) + elif self.center == 'GFZ': # GFZ RL04/5 at LMAX 90 # GFZ RL06 at LMAX 60 - LMAX = dict(RL04=90,RL05=90,RL06=60) - self.lmax.max=LMAX[self.release.value] - self.lmax.value=LMAX[self.release.value] + LMAX = dict(RL04=90, RL05=90, RL06=60) + self.lmax.max = LMAX[self.release.value] + self.lmax.value = LMAX[self.release.value] # function for setting the spherical harmonic order def set_max_order(self, sender): - """function for setting default max order - """ - self.mmax.max=self.lmax.value - self.mmax.value=self.lmax.value + """function for setting default max order""" + self.mmax.max = self.lmax.value + self.mmax.value = self.lmax.value # function for setting pole tide drift corrections for Release-5 def set_pole_tide(self, sender): - """function for setting default pole tide correction for a release - """ - self.pole_tide.value = True if ((self.release.value == 'RL05') - and (self.product.value == 'GSM')) else False + """function for setting default pole tide correction for a release""" + self.pole_tide.value = ( + True + if ( + (self.release.value == 'RL05') and (self.product.value == 'GSM') + ) + else False + ) # function for setting atmospheric jump corrections for Release-5 def set_atm_corr(self, sender): - """function for setting default ATM correction for a release - """ + """function for setting default ATM correction for a release""" self.atm.value = True if (self.release.value == 'RL05') else False def select_corrections(self, **kwargs): @@ -500,7 +507,9 @@ def select_corrections(self, **kwargs): Dropdown menu widget for setting output units """ # set default keyword arguments - kwargs.setdefault('units', ['cmwe','mmGH','mmCU',u'\u03BCGal','mbar']) + kwargs.setdefault( + 'units', ['cmwe', 'mmGH', 'mmCU', '\u03bcGal', 'mbar'] + ) # set the GIA file # files come in different formats depending on the group @@ -512,15 +521,12 @@ def select_corrections(self, **kwargs): ) # button and label for input file selection self.GIA_button = ipywidgets.Button( - description="File select", - width="30%", + description='File select', + width='30%', ) # create hbox of GIA file selection - if os.environ.get("DISPLAY") and (filedialog is not None): - self.GIA_file = ipywidgets.HBox([ - self.GIA_label, - self.GIA_button - ]) + if os.environ.get('DISPLAY') and (filedialog is not None): + self.GIA_file = ipywidgets.HBox([self.GIA_label, self.GIA_button]) else: self.GIA_file = self.GIA_label # connect fileselect button with action @@ -539,9 +545,21 @@ def select_corrections(self, **kwargs): # ascii: GIA reformatted to ascii # netCDF4: GIA reformatted to netCDF4 # HDF5: GIA reformatted to HDF5 - gia_list = ['[None]','IJ05-R2','W12a','SM09','ICE6G', - 'Wu10','AW13-ICE6G','AW13-IJ05','Caron','ICE6G-D', - 'ascii','netCDF4','HDF5'] + gia_list = [ + '[None]', + 'IJ05-R2', + 'W12a', + 'SM09', + 'ICE6G', + 'Wu10', + 'AW13-ICE6G', + 'AW13-IJ05', + 'Caron', + 'ICE6G-D', + 'ascii', + 'netCDF4', + 'HDF5', + ] self.GIA = ipywidgets.Dropdown( options=gia_list, value='[None]', @@ -560,14 +578,13 @@ def select_corrections(self, **kwargs): ) # button and label for input file selection self.remove_button = ipywidgets.Button( - description="File select", + description='File select', ) # create hbox of remove file selection - if os.environ.get("DISPLAY") and (filedialog is not None): - self.remove_file = ipywidgets.HBox([ - self.remove_label, - self.remove_button - ]) + if os.environ.get('DISPLAY') and (filedialog is not None): + self.remove_file = ipywidgets.HBox( + [self.remove_label, self.remove_button] + ) else: self.remove_file = self.remove_label # connect fileselect button with action @@ -580,8 +597,14 @@ def select_corrections(self, **kwargs): # index (ascii): index of monthly ascii files # index (netCDF4): index of monthly netCDF4 files # index (HDF5): index of monthly HDF5 files - remove_list = ['[None]','netCDF4','HDF5', - 'index (ascii)','index (netCDF4)','index (HDF5)'] + remove_list = [ + '[None]', + 'netCDF4', + 'HDF5', + 'index (ascii)', + 'index (netCDF4)', + 'index (HDF5)', + ] self.remove_format = ipywidgets.Dropdown( options=remove_list, value='[None]', @@ -607,15 +630,12 @@ def select_corrections(self, **kwargs): ) # button and label for input file selection self.mask_button = ipywidgets.Button( - description="File select", - width="30%", + description='File select', + width='30%', ) # create hbox of remove file selection - if os.environ.get("DISPLAY") and (filedialog is not None): - self.mask = ipywidgets.HBox([ - self.mask_label, - self.mask_button - ]) + if os.environ.get('DISPLAY') and (filedialog is not None): + self.mask = ipywidgets.HBox([self.mask_label, self.mask_button]) else: self.mask = self.mask_label # connect fileselect button with action @@ -676,62 +696,56 @@ def select_corrections(self, **kwargs): ) def select_GIA_file(self, b): - """function for GIA file selection - """ + """function for GIA file selection""" IPython.display.clear_output() root = Tk() root.withdraw() root.call('wm', 'attributes', '.', '-topmost', True) - filetypes = (("All Files", "*.*")) + filetypes = ('All Files', '*.*') b.files = filedialog.askopenfilename( - filetypes=filetypes, - multiple=False) + filetypes=filetypes, multiple=False + ) self.GIA_label.value = b.files def select_remove_file(self, b): - """function for removed file selection - """ + """function for removed file selection""" IPython.display.clear_output() root = Tk() root.withdraw() root.call('wm', 'attributes', '.', '-topmost', True) - filetypes = (("ascii file", "*.txt"), - ("HDF5 file", "*.h5"), - ("netCDF file", "*.nc"), - ("All Files", "*.*")) + filetypes = ( + ('ascii file', '*.txt'), + ('HDF5 file', '*.h5'), + ('netCDF file', '*.nc'), + ('All Files', '*.*'), + ) b.files = filedialog.askopenfilename( - defaultextension='nc', - filetypes=filetypes, - multiple=True) + defaultextension='nc', filetypes=filetypes, multiple=True + ) self.remove_files.extend(b.files) self.set_removelabel() def set_removefile(self, sender): - """function for updating removed file list - """ + """function for updating removed file list""" if self.remove_label.value: self.remove_files = self.remove_label.value.split(',') else: self.remove_files = [] def set_removelabel(self): - """function for updating removed file label - """ + """function for updating removed file label""" self.remove_label.value = ','.join(self.remove_files) def select_mask_file(self, b): - """function for mask file selection - """ + """function for mask file selection""" IPython.display.clear_output() root = Tk() root.withdraw() root.call('wm', 'attributes', '.', '-topmost', True) - filetypes = (("netCDF file", "*.nc"), - ("All Files", "*.*")) + filetypes = (('netCDF file', '*.nc'), ('All Files', '*.*')) b.files = filedialog.askopenfilename( - defaultextension='nc', - filetypes=filetypes, - multiple=False) + defaultextension='nc', filetypes=filetypes, multiple=False + ) self.mask_label.value = b.files def select_output(self, **kwargs): @@ -746,7 +760,7 @@ def select_output(self, **kwargs): # set default keyword arguments # dropdown menu for setting output data format self.output_format = ipywidgets.Dropdown( - options=['[None]','netCDF4', 'HDF5'], + options=['[None]', 'netCDF4', 'HDF5'], value='[None]', description='Output:', disabled=False, @@ -755,34 +769,30 @@ def select_output(self, **kwargs): @property def base_directory(self): - """Returns the data directory - """ + """Returns the data directory""" return pathlib.Path(self.directory_label.value).expanduser().absolute() @property def GIA_model(self): - """Returns the GIA model file - """ + """Returns the GIA model file""" return pathlib.Path(self.GIA_label.value).expanduser().absolute() @property def landmask(self): - """Returns the land-sea mask file - """ + """Returns the land-sea mask file""" return pathlib.Path(self.mask_label.value).expanduser().absolute() @property def unit_index(self): - """Returns the index for output spatial units - """ + """Returns the index for output spatial units""" return self.units.index + 1 @property def format(self): - """Returns the output format string - """ + """Returns the output format string""" return self.output_format.value + class colormap: """ Widgets for setting matplotlib colormaps for visualization @@ -799,6 +809,7 @@ class colormap: reverse Checkbox widget for reversing the output colormap """ + def __init__(self, **kwargs): # set default keyword arguments kwargs.setdefault('vmin', None) @@ -813,7 +824,7 @@ def __init__(self, **kwargs): self.vmax = copy.copy(kwargs['vmax']) # slider for range of color bar self.range = ipywidgets.IntRangeSlider( - value=[self.vmin,self.vmax], + value=[self.vmin, self.vmax], min=self.vmin, max=self.vmax, step=1, @@ -826,11 +837,11 @@ def __init__(self, **kwargs): ) # slider for steps in color bar - step = (self.vmax-self.vmin)//kwargs['steps'] + step = (self.vmax - self.vmin) // kwargs['steps'] self.step = ipywidgets.IntSlider( value=step, min=0, - max=self.vmax-self.vmin, + max=self.vmax - self.vmin, step=1, description='Plot Step:', disabled=False, @@ -846,20 +857,65 @@ def __init__(self, **kwargs): # (no reversed, qualitative or miscellaneous) self.cmaps_listed = copy.copy(kwargs['cmaps_listed']) self.cmaps_listed['Perceptually Uniform Sequential'] = [ - 'viridis','plasma','inferno','magma','cividis'] - self.cmaps_listed['Sequential'] = ['Greys','Purples', - 'Blues','Greens','Oranges','Reds','YlOrBr','YlOrRd', - 'OrRd','PuRd','RdPu','BuPu','GnBu','PuBu','YlGnBu', - 'PuBuGn','BuGn','YlGn'] - self.cmaps_listed['Sequential (2)'] = ['binary','gist_yarg', - 'gist_gray','gray','bone','pink','spring','summer', - 'autumn','winter','cool','Wistia','hot','afmhot', - 'gist_heat','copper'] - self.cmaps_listed['Diverging'] = ['PiYG','PRGn','BrBG', - 'PuOr','RdGy','RdBu','RdYlBu','RdYlGn','Spectral', - 'coolwarm', 'bwr','seismic'] - self.cmaps_listed['Cyclic'] = ['twilight', - 'twilight_shifted','hsv'] + 'viridis', + 'plasma', + 'inferno', + 'magma', + 'cividis', + ] + self.cmaps_listed['Sequential'] = [ + 'Greys', + 'Purples', + 'Blues', + 'Greens', + 'Oranges', + 'Reds', + 'YlOrBr', + 'YlOrRd', + 'OrRd', + 'PuRd', + 'RdPu', + 'BuPu', + 'GnBu', + 'PuBu', + 'YlGnBu', + 'PuBuGn', + 'BuGn', + 'YlGn', + ] + self.cmaps_listed['Sequential (2)'] = [ + 'binary', + 'gist_yarg', + 'gist_gray', + 'gray', + 'bone', + 'pink', + 'spring', + 'summer', + 'autumn', + 'winter', + 'cool', + 'Wistia', + 'hot', + 'afmhot', + 'gist_heat', + 'copper', + ] + self.cmaps_listed['Diverging'] = [ + 'PiYG', + 'PRGn', + 'BrBG', + 'PuOr', + 'RdGy', + 'RdBu', + 'RdYlBu', + 'RdYlGn', + 'Spectral', + 'coolwarm', + 'bwr', + 'seismic', + ] + self.cmaps_listed['Cyclic'] = ['twilight', 'twilight_shifted', 'hsv'] # create list of available colormaps in program cmap_list = [] for val in self.cmaps_listed.values(): @@ -885,37 +941,33 @@ def __init__(self, **kwargs): @property def _r(self): - """return string for reversed Matplotlib colormaps - """ + """return string for reversed Matplotlib colormaps""" cmap_reverse_flag = '_r' if self.reverse.value else '' return cmap_reverse_flag @property def value(self): - """return string for Matplotlib colormaps - """ + """return string for Matplotlib colormaps""" return copy.copy(cm.get_cmap(self.name.value + self._r)) @property def norm(self): - """return normalization for Matplotlib - """ - cmin,cmax = self.range.value - return colors.Normalize(vmin=cmin,vmax=cmax) + """return normalization for Matplotlib""" + cmin, cmax = self.range.value + return colors.Normalize(vmin=cmin, vmax=cmax) @property def levels(self): - """return tick steps for Matplotlib colorbars - """ - cmin,cmax = self.range.value - return [l for l in range(cmin,cmax+self.step.value,self.step.value)] + """return tick steps for Matplotlib colorbars""" + cmin, cmax = self.range.value + return [l for l in range(cmin, cmax + self.step.value, self.step.value)] @property def label(self): - """return tick labels for Matplotlib colorbars - """ + """return tick labels for Matplotlib colorbars""" return [f'{ct:0.0f}' for ct in self.levels] + def from_cpt(filename, use_extremes=True, **kwargs): """ Reads GMT color palette table files and registers the @@ -944,15 +996,15 @@ def from_cpt(filename, use_extremes=True, **kwargs): rx = re.compile(r'[-+]?(?:(?:\d*\.\d+)|(?:\d+\.?))(?:[Ee][+-]?\d+)?') # create list objects for x, r, g, b - x,r,g,b = ([],[],[],[]) + x, r, g, b = ([], [], [], []) # assume RGB color model - colorModel = "RGB" + colorModel = 'RGB' # back, forward and no data flags - flags = dict(B=None,F=None,N=None) + flags = dict(B=None, F=None, N=None) for line in file_contents: # find back, forward and no-data flags - model = re.search(r'COLOR_MODEL.*(HSV|RGB)',line,re.I) - BFN = re.match(r'[BFN]',line,re.I) + model = re.search(r'COLOR_MODEL.*(HSV|RGB)', line, re.I) + BFN = re.match(r'[BFN]', line, re.I) # parse non-color data lines if model: # find color model @@ -961,11 +1013,11 @@ def from_cpt(filename, use_extremes=True, **kwargs): elif BFN: flags[BFN.group(0)] = [float(i) for i in rx.findall(line)] continue - elif re.search(r"#",line): + elif re.search(r'#', line): # skip over commented header text continue # find numerical instances within line - x1,r1,g1,b1,x2,r2,g2,b2 = rx.findall(line) + x1, r1, g1, b1, x2, r2, g2, b2 = rx.findall(line) # append colors and locations to lists x.append(float(x1)) r.append(float(r1)) @@ -978,47 +1030,49 @@ def from_cpt(filename, use_extremes=True, **kwargs): b.append(float(b2)) # convert input colormap to output - xNorm = [None]*len(x) - if (colorModel == "HSV"): + xNorm = [None] * len(x) + if colorModel == 'HSV': # convert HSV (hue-saturation-value) to RGB # calculate normalized locations (0:1) - for i,xi in enumerate(x): - rr,gg,bb = colorsys.hsv_to_rgb(r[i]/360.,g[i],b[i]) + for i, xi in enumerate(x): + rr, gg, bb = colorsys.hsv_to_rgb(r[i] / 360.0, g[i], b[i]) r[i] = rr g[i] = gg b[i] = bb - xNorm[i] = (xi - x[0])/(x[-1] - x[0]) - elif (colorModel == "RGB"): + xNorm[i] = (xi - x[0]) / (x[-1] - x[0]) + elif colorModel == 'RGB': # normalize hexadecimal RGB triple from (0:255) to (0:1) # calculate normalized locations (0:1) - for i,xi in enumerate(x): + for i, xi in enumerate(x): r[i] /= 255.0 g[i] /= 255.0 b[i] /= 255.0 - xNorm[i] = (xi - x[0])/(x[-1] - x[0]) + xNorm[i] = (xi - x[0]) / (x[-1] - x[0]) # output RGB lists containing normalized location and colors - cdict = dict(red=[None]*len(x),green=[None]*len(x),blue=[None]*len(x)) - for i,xi in enumerate(x): - cdict['red'][i] = [xNorm[i],r[i],r[i]] - cdict['green'][i] = [xNorm[i],g[i],g[i]] - cdict['blue'][i] = [xNorm[i],b[i],b[i]] + cdict = dict( + red=[None] * len(x), green=[None] * len(x), blue=[None] * len(x) + ) + for i, xi in enumerate(x): + cdict['red'][i] = [xNorm[i], r[i], r[i]] + cdict['green'][i] = [xNorm[i], g[i], g[i]] + cdict['blue'][i] = [xNorm[i], b[i], b[i]] # create colormap for use in matplotlib cmap = colors.LinearSegmentedColormap(name, cdict, **kwargs) # set flags for under, over and bad values - extremes = dict(under=None,over=None,bad=None) - for key,attr in zip(['B','F','N'],['under','over','bad']): + extremes = dict(under=None, over=None, bad=None) + for key, attr in zip(['B', 'F', 'N'], ['under', 'over', 'bad']): if flags[key] is not None: - r,g,b = flags[key] - if (colorModel == "HSV"): + r, g, b = flags[key] + if colorModel == 'HSV': # convert HSV (hue-saturation-value) to RGB - r,g,b = colorsys.hsv_to_rgb(r/360.,g,b) - elif (colorModel == 'RGB'): + r, g, b = colorsys.hsv_to_rgb(r / 360.0, g, b) + elif colorModel == 'RGB': # normalize hexadecimal RGB triple from (0:255) to (0:1) - r,g,b = (r/255.0,g/255.0,b/255.0) + r, g, b = (r / 255.0, g / 255.0, b / 255.0) # set attribute for under, over and bad values - extremes[attr] = (r,g,b) + extremes[attr] = (r, g, b) # create copy of colormap with extremes if use_extremes: cmap = cmap.with_extremes(**extremes) @@ -1030,6 +1084,7 @@ def from_cpt(filename, use_extremes=True, **kwargs): # return the colormap return cmap + def custom_colormap(N, map_name, **kwargs): """ Calculates a custom colormap and registers it @@ -1051,58 +1106,60 @@ def custom_colormap(N, map_name, **kwargs): # make sure map_name is properly formatted map_name = map_name.capitalize() - if (map_name == 'Joughin'): + if map_name == 'Joughin': # calculate initial HSV for Ian Joughin's color map - h = np.linspace(0.1,1,N) + h = np.linspace(0.1, 1, N) s = np.ones((N)) v = np.ones((N)) # calculate RGB color map from HSV - color_map = np.zeros((N,3)) + color_map = np.zeros((N, 3)) for i in range(N): - color_map[i,:] = colorsys.hsv_to_rgb(h[i],s[i],v[i]) - elif (map_name == 'Seroussi'): + color_map[i, :] = colorsys.hsv_to_rgb(h[i], s[i], v[i]) + elif map_name == 'Seroussi': # calculate initial HSV for Helene Seroussi's color map - h = np.linspace(0,1,N) + h = np.linspace(0, 1, N) s = np.ones((N)) v = np.ones((N)) # calculate RGB color map from HSV - RGB = np.zeros((N,3)) + RGB = np.zeros((N, 3)) for i in range(N): - RGB[i,:] = colorsys.hsv_to_rgb(h[i],s[i],v[i]) + RGB[i, :] = colorsys.hsv_to_rgb(h[i], s[i], v[i]) # reverse color order and trim to range - RGB = RGB[::-1,:] - RGB = RGB[1:np.floor(0.7*N).astype('i'),:] + RGB = RGB[::-1, :] + RGB = RGB[1 : np.floor(0.7 * N).astype('i'), :] # calculate HSV color map from RGB HSV = np.zeros_like(RGB) - for i,val in enumerate(RGB): - HSV[i,:] = colorsys.rgb_to_hsv(val[0],val[1],val[2]) + for i, val in enumerate(RGB): + HSV[i, :] = colorsys.rgb_to_hsv(val[0], val[1], val[2]) # calculate saturation as a function of hue - HSV[:,1] = np.clip(0.1 + HSV[:,0], 0, 1) + HSV[:, 1] = np.clip(0.1 + HSV[:, 0], 0, 1) # calculate RGB color map from HSV color_map = np.zeros_like(HSV) - for i,val in enumerate(HSV): - color_map[i,:] = colorsys.hsv_to_rgb(val[0],val[1],val[2]) - elif (map_name == 'Rignot'): + for i, val in enumerate(HSV): + color_map[i, :] = colorsys.hsv_to_rgb(val[0], val[1], val[2]) + elif map_name == 'Rignot': # calculate initial HSV for Eric Rignot's color map - h = np.linspace(0,1,N) + h = np.linspace(0, 1, N) s = np.clip(0.1 + h, 0, 1) v = np.ones((N)) # calculate RGB color map from HSV - color_map = np.zeros((N,3)) + color_map = np.zeros((N, 3)) for i in range(N): - color_map[i,:] = colorsys.hsv_to_rgb(h[i],s[i],v[i]) + color_map[i, :] = colorsys.hsv_to_rgb(h[i], s[i], v[i]) else: raise ValueError(f'Incorrect color map specified ({map_name})') # output RGB lists containing normalized location and colors Xnorm = len(color_map) - 1.0 - cdict = dict(red=[None]*len(color_map), - green=[None]*len(color_map), - blue=[None]*len(color_map)) - for i,rgb in enumerate(color_map): - cdict['red'][i] = [float(i)/Xnorm,rgb[0],rgb[0]] - cdict['green'][i] = [float(i)/Xnorm,rgb[1],rgb[1]] - cdict['blue'][i] = [float(i)/Xnorm,rgb[2],rgb[2]] + cdict = dict( + red=[None] * len(color_map), + green=[None] * len(color_map), + blue=[None] * len(color_map), + ) + for i, rgb in enumerate(color_map): + cdict['red'][i] = [float(i) / Xnorm, rgb[0], rgb[0]] + cdict['green'][i] = [float(i) / Xnorm, rgb[1], rgb[1]] + cdict['blue'][i] = [float(i) / Xnorm, rgb[2], rgb[2]] # create colormap for use in matplotlib cmap = colors.LinearSegmentedColormap(map_name, cdict, **kwargs) @@ -1114,6 +1171,7 @@ def custom_colormap(N, map_name, **kwargs): # return the colormap return cmap + # PURPOSE: adjusts longitudes to be -180:180 def wrap_longitudes(lon): """ @@ -1128,6 +1186,7 @@ def wrap_longitudes(lon): # convert phi from radians to degrees return np.degrees(phi) + # PURPOSE: parallels the matplotlib basemap shiftgrid function def shift_grid(lon0, data, lon, CYCLIC=360.0): """ @@ -1154,25 +1213,26 @@ def shift_grid(lon0, data, lon, CYCLIC=360.0): shift_lon: np.ndarray shifted longitude array """ - start_idx = 0 if (np.fabs(lon[-1]-lon[0]-CYCLIC) > 1.e-4) else 1 - i0 = np.argmin(np.fabs(lon-lon0)) + start_idx = 0 if (np.fabs(lon[-1] - lon[0] - CYCLIC) > 1.0e-4) else 1 + i0 = np.argmin(np.fabs(lon - lon0)) # shift longitudinal values if np.ma.isMA(lon): - shift_lon = np.ma.zeros(lon.shape,lon.dtype) + shift_lon = np.ma.zeros(lon.shape, lon.dtype) else: - shift_lon = np.zeros(lon.shape,lon.dtype) + shift_lon = np.zeros(lon.shape, lon.dtype) shift_lon[0:-i0] = lon[i0:] - CYCLIC - shift_lon[-i0:] = lon[start_idx:i0+start_idx] + shift_lon[-i0:] = lon[start_idx : i0 + start_idx] # shift data values if np.ma.isMA(data): - shift_data = np.ma.zeros(data.shape,data.dtype) + shift_data = np.ma.zeros(data.shape, data.dtype) else: - shift_data = np.zeros(data.shape,data.dtype) - shift_data[:,:-i0] = data[:,i0:] - shift_data[:,-i0:] = data[:,start_idx:i0+start_idx] + shift_data = np.zeros(data.shape, data.dtype) + shift_data[:, :-i0] = data[:, i0:] + shift_data[:, -i0:] = data[:, start_idx : i0 + start_idx] # return the shifted values return (shift_data, shift_lon) + # PURPOSE: parallels the matplotlib basemap interp function with scipy splines def interp_grid(data, xin, yin, xout, yout, order=0): """ @@ -1203,27 +1263,30 @@ def interp_grid(data, xin, yin, xout, yout, order=0): interp_data: np.ndarray interpolated data grid """ - if (order == 0): + if order == 0: # interpolate with nearest-neighbors - xcoords = (len(xin)-1)*(xout-xin[0])/(xin[-1]-xin[0]) - ycoords = (len(yin)-1)*(yout-yin[0])/(yin[-1]-yin[0]) - xcoords = np.clip(xcoords,0,len(xin)-1) - ycoords = np.clip(ycoords,0,len(yin)-1) + xcoords = (len(xin) - 1) * (xout - xin[0]) / (xin[-1] - xin[0]) + ycoords = (len(yin) - 1) * (yout - yin[0]) / (yin[-1] - yin[0]) + xcoords = np.clip(xcoords, 0, len(xin) - 1) + ycoords = np.clip(ycoords, 0, len(yin) - 1) xcoordsi = np.around(xcoords).astype(np.int32) ycoordsi = np.around(ycoords).astype(np.int32) - interp_data = data[ycoordsi,xcoordsi] + interp_data = data[ycoordsi, xcoordsi] else: # interpolate with bivariate spline approximations - spl = scipy.interpolate.RectBivariateSpline(xin, yin, - data.T, kx=order, ky=order) - interp_data = spl.ev(xout,yout) + spl = scipy.interpolate.RectBivariateSpline( + xin, yin, data.T, kx=order, ky=order + ) + interp_data = spl.ev(xout, yout) # return the interpolated data on the output grid return interp_data + # PURPOSE: parallels the matplotlib basemap maskoceans function but with # updated Greenland coastlines (G250) and Rignot (2017) Antarctic grounded ice -def mask_oceans(xin, yin, data=None, order=0, lakes=False, - iceshelves=True, resolution='qd'): +def mask_oceans( + xin, yin, data=None, order=0, lakes=False, iceshelves=True, resolution='qd' +): """ Mask a data grid over global ocean and water points @@ -1261,28 +1324,34 @@ def mask_oceans(xin, yin, data=None, order=0, lakes=False, masked data grid """ # read in land/sea mask - lsmask = get_data_path(['data',f'landsea_{resolution}.nc']) + lsmask = get_data_path(['data', f'landsea_{resolution}.nc']) # Land-Sea Mask with Antarctica from Rignot (2017) and Greenland from GEUS # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading landsea = spatial().from_netCDF4(lsmask, date=False, varname='LSMASK') # create land function - nth,nphi = landsea.shape - land_function = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + land_function = np.zeros((nth, nphi), dtype=bool) # extract land function from file # find land values (1) - land_function |= (landsea.data == 1) + land_function |= landsea.data == 1 # find lake values (2) if lakes: - land_function |= (landsea.data == 2) + land_function |= landsea.data == 2 # find small island values (3) - land_function |= (landsea.data == 3) + land_function |= landsea.data == 3 # find Greenland and Antarctic ice shelf values (4) if iceshelves: - land_function |= (landsea.data == 4) + land_function |= landsea.data == 4 # interpolate to output grid - mask = interp_grid(land_function.astype(np.int32), - landsea.lon, landsea.lat, xin, yin, order) + mask = interp_grid( + land_function.astype(np.int32), + landsea.lon, + landsea.lat, + xin, + yin, + order, + ) # mask input data or return the interpolated mask if data is not None: # update data mask with interpolated mask diff --git a/gravity_toolkit/units.py b/gravity_toolkit/units.py index 6fb20109..87324750 100644 --- a/gravity_toolkit/units.py +++ b/gravity_toolkit/units.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" units.py Written by Tyler Sutterley (05/2024) Contributions by Hugo Lecomte @@ -26,10 +26,12 @@ Updated 04/2020: include earth parameters as attributes Written 03/2020 """ + from __future__ import annotations import numpy as np + class units(object): r""" Class for converting spherical harmonic and spatial data to specific units @@ -54,12 +56,15 @@ class units(object): l: int spherical harmonic degree up to ``lmax`` """ + np.seterr(invalid='ignore') - def __init__(self, - lmax: int | None = None, - a_axis: float = 6.378137e8, - flat: float = 1.0/298.257223563 - ): + + def __init__( + self, + lmax: int | None = None, + a_axis: float = 6.378137e8, + flat: float = 1.0 / 298.257223563, + ): # Earth Parameters # universal gravitational constant [dyn*cm^2/g^2] self.G = 6.67430e-8 @@ -91,31 +96,27 @@ def __init__(self, self.Pa = None self.lmax = lmax # calculate spherical harmonic degree (0 is falsy) - self.l = np.arange(self.lmax+1) if (self.lmax is not None) else None + self.l = np.arange(self.lmax + 1) if (self.lmax is not None) else None @property def b_axis(self) -> float: - """semi-minor axis of the Earth's ellipsoid in cm - """ - return (1.0 - self.flat)*self.a_axis + """semi-minor axis of the Earth's ellipsoid in cm""" + return (1.0 - self.flat) * self.a_axis @property def rad_e(self) -> float: - """average radius of the Earth in cm with the same volume as the ellipsoid - """ - return self.a_axis*(1.0 - self.flat)**(1.0/3.0) + """average radius of the Earth in cm with the same volume as the ellipsoid""" + return self.a_axis * (1.0 - self.flat) ** (1.0 / 3.0) @property def rho_e(self) -> float: - r"""average density of the Earth in g/cm\ :sup:`3` - """ - return 0.75*self.GM/(self.G*np.pi*self.rad_e**3) + r"""average density of the Earth in g/cm\ :sup:`3`""" + return 0.75 * self.GM / (self.G * np.pi * self.rad_e**3) @property def mass(self) -> float: - """approximate mass of the Earth in g - """ - return (4.0/3.0)*np.pi*self.rho_e*self.rad_e**3 + """approximate mass of the Earth in g""" + return (4.0 / 3.0) * np.pi * self.rho_e * self.rad_e**3 def harmonic(self, hl, kl, ll, **kwargs): """ @@ -163,36 +164,59 @@ def harmonic(self, hl, kl, ll, **kwargs): # set default keyword arguments kwargs.setdefault('include_elastic', True) kwargs.setdefault('include_ellipsoidal', False) - fraction = np.ones((self.lmax+1)) + fraction = np.ones((self.lmax + 1)) # compensate for elastic deformation within the solid earth if kwargs['include_elastic']: fraction += kl[self.l] # include effects for Earth's oblateness if kwargs['include_ellipsoidal']: - fraction /= (1.0 - self.flat) + fraction /= 1.0 - self.flat # degree dependent coefficients # norm, fully normalized spherical harmonics - self.norm = np.ones((self.lmax+1)) + self.norm = np.ones((self.lmax + 1)) # cmwe, centimeters water equivalent [g/cm^2] - self.cmwe = self.rho_e*self.rad_e*(2.0*self.l+1.0)/fraction/3.0 + self.cmwe = ( + self.rho_e * self.rad_e * (2.0 * self.l + 1.0) / fraction / 3.0 + ) # mmwe, millimeters water equivalent [kg/m^2] - self.mmwe = 10.0*self.rho_e*self.rad_e*(2.0*self.l+1.0)/fraction/3.0 + self.mmwe = ( + 10.0 + * self.rho_e + * self.rad_e + * (2.0 * self.l + 1.0) + / fraction + / 3.0 + ) # mmGH, millimeters geoid height - self.mmGH = np.ones((self.lmax+1))*(10.0*self.rad_e) + self.mmGH = np.ones((self.lmax + 1)) * (10.0 * self.rad_e) # mmCU, millimeters elastic crustal deformation (uplift) - self.mmCU = 10.0*self.rad_e*hl[self.l]/fraction + self.mmCU = 10.0 * self.rad_e * hl[self.l] / fraction # mmCH, millimeters elastic crustal deformation (horizontal) - self.mmCH = 10.0*self.rad_e*ll[self.l]/fraction + self.mmCH = 10.0 * self.rad_e * ll[self.l] / fraction # cmVCU, centimeters viscoelastic crustal uplift - self.cmVCU = self.rad_e*(2.0*self.l+1.0)/2.0 + self.cmVCU = self.rad_e * (2.0 * self.l + 1.0) / 2.0 # mVCU, meters viscoelastic crustal uplift - self.mVCU = self.rad_e*(2.0*self.l+1.0)/200.0 + self.mVCU = self.rad_e * (2.0 * self.l + 1.0) / 200.0 # microGal, microGal gravity perturbations - self.microGal = 1.e6*self.GM*(self.l+1.0)/(self.rad_e**2.0) + self.microGal = 1.0e6 * self.GM * (self.l + 1.0) / (self.rad_e**2.0) # mbar, millibar equivalent surface pressure - self.mbar = self.g_wmo*self.rho_e*self.rad_e*(2.0*self.l+1.0)/fraction/3e3 + self.mbar = ( + self.g_wmo + * self.rho_e + * self.rad_e + * (2.0 * self.l + 1.0) + / fraction + / 3e3 + ) # Pa, pascals equivalent surface pressure - self.Pa = self.g_wmo*self.rho_e*self.rad_e*(2.0*self.l+1.0)/fraction/30.0 + self.Pa = ( + self.g_wmo + * self.rho_e + * self.rad_e + * (2.0 * self.l + 1.0) + / fraction + / 30.0 + ) # return the degree dependent unit conversions return self @@ -228,21 +252,33 @@ def spatial(self, hl, kl, ll, **kwargs): """ # set default keyword arguments kwargs.setdefault('include_elastic', True) - fraction = np.ones((self.lmax+1)) + fraction = np.ones((self.lmax + 1)) # compensate for elastic deformation within the solid earth if kwargs['include_elastic']: fraction += kl[self.l] # degree dependent coefficients # norm, fully normalized spherical harmonics - self.norm = np.ones((self.lmax+1)) + self.norm = np.ones((self.lmax + 1)) # cmwe, centimeters water equivalent [g/cm^2] - self.cmwe = 3.0*fraction/(1.0+2.0*self.l)/(4.0*np.pi*self.rad_e*self.rho_e) + self.cmwe = ( + 3.0 + * fraction + / (1.0 + 2.0 * self.l) + / (4.0 * np.pi * self.rad_e * self.rho_e) + ) # mmwe, millimeters water equivalent [kg/m^2] - self.mmwe = 3.0*fraction/(1.0+2.0*self.l)/(40.0*np.pi*self.rad_e*self.rho_e) + self.mmwe = ( + 3.0 + * fraction + / (1.0 + 2.0 * self.l) + / (40.0 * np.pi * self.rad_e * self.rho_e) + ) # mmGH, millimeters geoid height - self.mmGH = np.ones((self.lmax+1))/(4.0*np.pi*self.rad_e) + self.mmGH = np.ones((self.lmax + 1)) / (4.0 * np.pi * self.rad_e) # microGal, microGal gravity perturbations - self.microGal = (self.rad_e**2.0)/(4.0*np.pi*1.e6*self.GM)/(self.l+1.0) + self.microGal = ( + (self.rad_e**2.0) / (4.0 * np.pi * 1.0e6 * self.GM) / (self.l + 1.0) + ) # return the degree dependent unit conversions return self @@ -271,13 +307,13 @@ def bycode(var: int) -> str: Named unit code for spherical harmonics or spatial fields """ named_units = [ - 'norm', # 0: keep original scale - 'cmwe', # 1: cmwe, centimeters water equivalent - 'mmGH', # 2: mmGH, mm geoid height - 'mmCU', # 3: mmCU, mm elastic crustal deformation - 'microGal', # 4: microGal, microGal gravity perturbations - 'mbar', # 5: mbar, equivalent surface pressure - 'cmVCU' # 6: cmVCU, cm viscoelastic crustal uplift (GIA) + 'norm', # 0: keep original scale + 'cmwe', # 1: cmwe, centimeters water equivalent + 'mmGH', # 2: mmGH, mm geoid height + 'mmCU', # 3: mmCU, mm elastic crustal deformation + 'microGal', # 4: microGal, microGal gravity perturbations + 'mbar', # 5: mbar, equivalent surface pressure + 'cmVCU', # 6: cmVCU, cm viscoelastic crustal uplift (GIA) ] try: return named_units[var] @@ -299,12 +335,12 @@ def get_attributes(var: str) -> str: mmwe=('mm', 'Equivalent_Water_Thickness'), cmwe=('cm', 'Equivalent_Water_Thickness'), mmGH=('mm', 'Geoid_Height'), - mmCU=('mm','Elastic_Crustal_Uplift'), - mmCH=('mm','Horizontal_Elastic_Crustal_Deformation'), - microGal=(u'\u03BCGal', 'Gravitational_Undulation'), + mmCU=('mm', 'Elastic_Crustal_Uplift'), + mmCH=('mm', 'Horizontal_Elastic_Crustal_Deformation'), + microGal=('\u03bcGal', 'Gravitational_Undulation'), mbar=('mbar', 'Equivalent_Surface_Pressure'), cmVCU=('cm', 'Viscoelastic_Crustal_Uplift'), - mVCU=('meters', 'Viscoelastic_Crustal_Uplift') + mVCU=('meters', 'Viscoelastic_Crustal_Uplift'), ) try: return named_attributes[var] diff --git a/gravity_toolkit/utilities.py b/gravity_toolkit/utilities.py index 5a7a27ff..da2abf24 100644 --- a/gravity_toolkit/utilities.py +++ b/gravity_toolkit/utilities.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" utilities.py Written by Tyler Sutterley (07/2026) Download and management utilities for syncing files @@ -62,6 +62,7 @@ Updated 08/2020: add PO.DAAC Drive opener, login and download functions Written 08/2020 """ + from __future__ import print_function, division, annotations import sys @@ -88,7 +89,7 @@ import lxml.etree import subprocess import platformdirs -import calendar,time +import calendar, time if sys.version_info[0] == 2: from cookielib import CookieJar @@ -100,6 +101,7 @@ from urllib.parse import urlencode, urlparse import urllib.request as urllib2 + # PURPOSE: get absolute path within a package from a relative path def get_data_path(relpath: list | str | pathlib.Path): """ @@ -119,10 +121,11 @@ def get_data_path(relpath: list | str | pathlib.Path): elif isinstance(relpath, str): return filepath.joinpath(relpath) + # PURPOSE: get the path to the user cache directory def get_cache_path( relpath: list | str | pathlib.Path | None = None, - appname="gravtk", + appname='gravtk', ensure_exists=True, ): """ @@ -138,7 +141,7 @@ def get_cache_path( Verify that the cache directory exists """ # check for custom environment variable for cache directory - cache_dir = os.environ.get("GRAVTK_CACHE_DIR") + cache_dir = os.environ.get('GRAVTK_CACHE_DIR') if cache_dir: # custom environment variable for cache directory filepath = pathlib.Path(cache_dir).expanduser().absolute() @@ -157,9 +160,10 @@ def get_cache_path( filepath = filepath.joinpath(relpath) return pathlib.Path(filepath) + def import_dependency( name: str, - extra: str = "", + extra: str = '', raise_exception: bool = False, ): """ @@ -186,7 +190,7 @@ def import_dependency( raise TypeError(f"Invalid module name: '{name}'; must be a string") # default error if module cannot be imported err = f"Missing optional dependency '{name}'. {extra}" - module = type("module", (), {}) + module = type('module', (), {}) # try to import the module try: module = importlib.import_module(name) @@ -198,6 +202,7 @@ def import_dependency( # return the module return module + def dependency_available( name: str, minversion: str | None = None, @@ -229,6 +234,7 @@ def dependency_available( # return if both checks are passed return True + def is_valid_url(url: str) -> bool: """ Checks if a string is a valid URL @@ -248,6 +254,7 @@ def is_valid_url(url: str) -> bool: class reify(object): """Class decorator that puts the result of the method it decorates into the instance""" + def __init__(self, wrapped): self.wrapped = wrapped self.__name__ = wrapped.__name__ @@ -260,11 +267,9 @@ def __get__(self, inst, objtype=None): setattr(inst, self.wrapped.__name__, val) return val + # PURPOSE: get the hash value of a file -def get_hash( - local: str | io.IOBase | pathlib.Path, - algorithm: str = 'md5' - ): +def get_hash(local: str | io.IOBase | pathlib.Path, algorithm: str = 'md5'): """ Get the hash value from a local file or ``BytesIO`` object @@ -298,11 +303,9 @@ def get_hash( else: return '' + # PURPOSE: get the git hash value -def get_git_revision_hash( - refname: str = 'HEAD', - short: bool = False - ): +def get_git_revision_hash(refname: str = 'HEAD', short: bool = False): """ Get the ``git`` hash value for a particular reference @@ -325,10 +328,10 @@ def get_git_revision_hash( with warnings.catch_warnings(): return str(subprocess.check_output(cmd), encoding='utf8').strip() + # PURPOSE: get the current git status def get_git_status(): - """Get the status of a ``git`` repository as a boolean value - """ + """Get the status of a ``git`` repository as a boolean value""" # get path to .git directory from current file path filename = inspect.getframeinfo(inspect.currentframe()).filename basepath = pathlib.Path(filename).absolute().parent.parent @@ -338,6 +341,7 @@ def get_git_status(): with warnings.catch_warnings(): return bool(subprocess.check_output(cmd)) + # PURPOSE: recursively split a url path def url_split(s: str): """ @@ -349,12 +353,13 @@ def url_split(s: str): url string """ head, tail = posixpath.split(s) - if head in ('http:','https:','ftp:','s3:'): - return s, + if head in ('http:', 'https:', 'ftp:', 's3:'): + return (s,) elif head in ('', posixpath.sep): - return tail, + return (tail,) return url_split(head) + (tail,) + # PURPOSE: convert file lines to arguments def convert_arg_line_to_args(arg_line): """ @@ -366,16 +371,14 @@ def convert_arg_line_to_args(arg_line): line string containing a single argument and/or comments """ # remove commented lines and after argument comments - for arg in re.sub(r'\#(.*?)$',r'',arg_line).split(): + for arg in re.sub(r'\#(.*?)$', r'', arg_line).split(): if not arg.strip(): continue yield arg + # PURPOSE: returns the Unix timestamp value for a formatted date string -def get_unix_time( - time_string: str, - format: str = '%Y-%m-%d %H:%M:%S' - ): +def get_unix_time(time_string: str, format: str = '%Y-%m-%d %H:%M:%S'): """ Get the Unix timestamp value for a formatted date string @@ -400,6 +403,7 @@ def get_unix_time( else: return parsed_time.timestamp() + # PURPOSE: output a time string in isoformat def isoformat(time_string: str): """ @@ -418,6 +422,7 @@ def isoformat(time_string: str): else: return parsed_time.isoformat() + # PURPOSE: rounds a number to an even number less than or equal to original def even(value: float): """ @@ -428,7 +433,8 @@ def even(value: float): value: float number to be rounded """ - return 2*int(value//2) + return 2 * int(value // 2) + # PURPOSE: rounds a number upward to its nearest integer def ceil(value: float): @@ -440,15 +446,16 @@ def ceil(value: float): value: float number to be rounded upward """ - return -int(-value//1) + return -int(-value // 1) + # PURPOSE: make a copy of a file with all system information def copy( - source: str | pathlib.Path, - destination: str | pathlib.Path, - move: bool = False, - **kwargs - ): + source: str | pathlib.Path, + destination: str | pathlib.Path, + move: bool = False, + **kwargs, +): """ Copy or move a file with all system information @@ -471,6 +478,7 @@ def copy( if move: source.unlink() + # PURPOSE: open a unique file adding a numerical instance if existing def create_unique_file(filename: str | pathlib.Path): """ @@ -499,12 +507,11 @@ def create_unique_file(filename: str | pathlib.Path): filename = filename.with_name(f'{stem}_{counter:d}{suffix}') counter += 1 + # PURPOSE: check ftp connection def check_ftp_connection( - HOST: str, - username: str | None = None, - password: str | None = None - ): + HOST: str, username: str | None = None, password: str | None = None +): """ Check internet connection with ftp host @@ -521,7 +528,7 @@ def check_ftp_connection( try: f = ftplib.FTP(HOST) f.login(username, password) - f.voidcmd("NOOP") + f.voidcmd('NOOP') except IOError: raise RuntimeError('Check internet connection') except ftplib.error_perm: @@ -529,16 +536,17 @@ def check_ftp_connection( else: return True + # PURPOSE: list a directory on a ftp host def ftp_list( - HOST: str | list, - username: str | None = None, - password: str | None = None, - timeout: int | None = None, - basename: bool = False, - pattern: str | None = None, - sort: bool = False - ): + HOST: str | list, + username: str | None = None, + password: str | None = None, + timeout: int | None = None, + basename: bool = False, + pattern: str | None = None, + sort: bool = False, +): """ List a directory on a ftp host @@ -571,17 +579,17 @@ def ftp_list( HOST = url_split(HOST) # try to connect to ftp host try: - ftp = ftplib.FTP(HOST[0],timeout=timeout) - except (socket.gaierror,IOError): + ftp = ftplib.FTP(HOST[0], timeout=timeout) + except (socket.gaierror, IOError): raise RuntimeError(f'Unable to connect to {HOST[0]}') else: - ftp.login(username,password) + ftp.login(username, password) # list remote path output = ftp.nlst(posixpath.join(*HOST[1:])) # get last modified date of ftp files and convert into unix time - mtimes = [None]*len(output) + mtimes = [None] * len(output) # iterate over each file in the list and get the modification time - for i,f in enumerate(output): + for i, f in enumerate(output): try: # try sending modification time command mdtm = ftp.sendcmd(f'MDTM {f}') @@ -590,19 +598,19 @@ def ftp_list( pass else: # convert the modification time into unix time - mtimes[i] = get_unix_time(mdtm[4:], format="%Y%m%d%H%M%S") + mtimes[i] = get_unix_time(mdtm[4:], format='%Y%m%d%H%M%S') # reduce to basenames if basename: output = [posixpath.basename(i) for i in output] # reduce using regular expression pattern if pattern: - i = [i for i,f in enumerate(output) if re.search(pattern,f)] + i = [i for i, f in enumerate(output) if re.search(pattern, f)] # reduce list of listed items and last modified times output = [output[indice] for indice in i] mtimes = [mtimes[indice] for indice in i] # sort the list if sort: - i = [i for i,j in sorted(enumerate(output), key=lambda i: i[1])] + i = [i for i, j in sorted(enumerate(output), key=lambda i: i[1])] # sort list of listed items and last modified times output = [output[indice] for indice in i] mtimes = [mtimes[indice] for indice in i] @@ -611,19 +619,20 @@ def ftp_list( # return the list of items and last modified times return (output, mtimes) + # PURPOSE: download a file from a ftp host def from_ftp( - HOST: str | list, - username: str | None = None, - password: str | None = None, - timeout: int | None = None, - local: str | pathlib.Path | None = None, - hash: str = '', - chunk: int = 8192, - verbose: bool = False, - fid=sys.stdout, - mode: oct = 0o775 - ): + HOST: str | list, + username: str | None = None, + password: str | None = None, + timeout: int | None = None, + local: str | pathlib.Path | None = None, + hash: str = '', + chunk: int = 8192, + verbose: bool = False, + fid=sys.stdout, + mode: oct = 0o775, +): """ Download a file from a ftp host @@ -665,16 +674,17 @@ def from_ftp( try: # try to connect to ftp host ftp = ftplib.FTP(HOST[0], timeout=timeout) - except (socket.gaierror,IOError): + except (socket.gaierror, IOError): raise RuntimeError(f'Unable to connect to {HOST[0]}') else: - ftp.login(username,password) + ftp.login(username, password) # remote path ftp_remote_path = posixpath.join(*HOST[1:]) # copy remote file contents to bytesIO object remote_buffer = io.BytesIO() - ftp.retrbinary(f'RETR {ftp_remote_path}', - remote_buffer.write, blocksize=chunk) + ftp.retrbinary( + f'RETR {ftp_remote_path}', remote_buffer.write, blocksize=chunk + ) remote_buffer.seek(0) # save file basename with bytesIO object remote_buffer.filename = HOST[-1] @@ -682,7 +692,7 @@ def from_ftp( remote_hash = hashlib.md5(remote_buffer.getvalue()).hexdigest() # get last modified date of remote file and convert into unix time mdtm = ftp.sendcmd(f'MDTM {ftp_remote_path}') - remote_mtime = get_unix_time(mdtm[4:], format="%Y%m%d%H%M%S") + remote_mtime = get_unix_time(mdtm[4:], format='%Y%m%d%H%M%S') # compare checksums if local and (hash != remote_hash): # convert to absolute path @@ -706,25 +716,25 @@ def from_ftp( remote_buffer.seek(0) return remote_buffer + def _create_default_ssl_context() -> ssl.SSLContext: - """Creates the default SSL context - """ + """Creates the default SSL context""" context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) _set_ssl_context_options(context) context.options |= ssl.OP_NO_COMPRESSION return context + def _create_ssl_context_no_verify() -> ssl.SSLContext: - """Creates an SSL context for unverified connections - """ + """Creates an SSL context for unverified connections""" context = _create_default_ssl_context() context.check_hostname = False context.verify_mode = ssl.CERT_NONE return context + def _set_ssl_context_options(context: ssl.SSLContext) -> None: - """Sets the default options for the SSL context - """ + """Sets the default options for the SSL context""" if sys.version_info >= (3, 10) or ssl.OPENSSL_VERSION_INFO >= (1, 1, 0, 7): context.minimum_version = ssl.TLSVersion.TLSv1_2 else: @@ -733,14 +743,16 @@ def _set_ssl_context_options(context: ssl.SSLContext) -> None: context.options |= ssl.OP_NO_TLSv1 context.options |= ssl.OP_NO_TLSv1_1 + # default ssl context _default_ssl_context = _create_ssl_context_no_verify() + # PURPOSE: check internet connection def check_connection( - HOST: str, - context: ssl.SSLContext = _default_ssl_context, - ): + HOST: str, + context: ssl.SSLContext = _default_ssl_context, +): """ Check internet connection with http host @@ -759,16 +771,17 @@ def check_connection( else: return True + # PURPOSE: list a directory on an Apache http Server def http_list( - HOST: str | list, - timeout: int | None = None, - context: ssl.SSLContext = _default_ssl_context, - parser = lxml.etree.HTMLParser(), - format: str = '%Y-%m-%d %H:%M', - pattern: str = '', - sort: bool = False - ): + HOST: str | list, + timeout: int | None = None, + context: ssl.SSLContext = _default_ssl_context, + parser=lxml.etree.HTMLParser(), + format: str = '%Y-%m-%d %H:%M', + pattern: str = '', + sort: bool = False, +): """ List a directory on an Apache http Server @@ -811,35 +824,38 @@ def http_list( tree = lxml.etree.parse(response, parser) colnames = tree.xpath('//tr/td[not(@*)]//a/@href') # get the Unix timestamp value for a modification time - collastmod = [get_unix_time(i,format=format) - for i in tree.xpath('//tr/td[@align="right"][1]/text()')] + collastmod = [ + get_unix_time(i, format=format) + for i in tree.xpath('//tr/td[@align="right"][1]/text()') + ] # reduce using regular expression pattern if pattern: - i = [i for i,f in enumerate(colnames) if re.search(pattern, f)] + i = [i for i, f in enumerate(colnames) if re.search(pattern, f)] # reduce list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # sort the list if sort: - i = [i for i,j in sorted(enumerate(colnames), key=lambda i: i[1])] + i = [i for i, j in sorted(enumerate(colnames), key=lambda i: i[1])] # sort list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # return the list of column names and last modified times return (colnames, collastmod) + # PURPOSE: download a file from a http host def from_http( - HOST: str | list, - timeout: int | None = None, - context: ssl.SSLContext = _default_ssl_context, - local: str | pathlib.Path | None = None, - hash: str = '', - chunk: int = 16384, - verbose: bool = False, - fid = sys.stdout, - mode: oct = 0o775 - ): + HOST: str | list, + timeout: int | None = None, + context: ssl.SSLContext = _default_ssl_context, + local: str | pathlib.Path | None = None, + hash: str = '', + chunk: int = 16384, + verbose: bool = False, + fid=sys.stdout, + mode: oct = 0o775, +): """ Download a file from a http host @@ -910,12 +926,13 @@ def from_http( remote_buffer.seek(0) return remote_buffer + # PURPOSE: load a JSON response from a http host def from_json( - HOST: str | list, - timeout: int | None = None, - context: ssl.SSLContext = _default_ssl_context - ) -> dict: + HOST: str | list, + timeout: int | None = None, + context: ssl.SSLContext = _default_ssl_context, +) -> dict: """ Load a JSON response from a http host @@ -948,16 +965,17 @@ def from_json( # load JSON response return json.loads(response.read()) + # PURPOSE: attempt to build an opener with netrc def attempt_login( - urs: str, - context: ssl.SSLContext = _default_ssl_context, - password_manager: bool = True, - get_ca_certs: bool = False, - redirect: bool = False, - authorization_header: bool = True, - **kwargs - ): + urs: str, + context: ssl.SSLContext = _default_ssl_context, + password_manager: bool = True, + get_ca_certs: bool = False, + redirect: bool = False, + authorization_header: bool = True, + **kwargs, +): """ Attempt to build a ``urllib`` opener for NASA Earthdata @@ -1014,13 +1032,16 @@ def attempt_login( # for each retry for retry in range(kwargs['retries']): # build an opener for urs with credentials - opener = build_opener(username, password, + opener = build_opener( + username, + password, context=context, password_manager=password_manager, get_ca_certs=get_ca_certs, redirect=redirect, authorization_header=authorization_header, - urs=urs) + urs=urs, + ) # try logging in by check credentials HOST = 'https://archive.podaac.earthdata.nasa.gov/s3credentials' try: @@ -1035,17 +1056,18 @@ def attempt_login( # reached end of available retries raise RuntimeError('End of Retries: Check NASA Earthdata credentials') + # PURPOSE: "login" to NASA Earthdata with supplied credentials def build_opener( - username: str, - password: str, - context: ssl.SSLContext = _default_ssl_context, - password_manager: bool = False, - get_ca_certs: bool = False, - redirect: bool = False, - authorization_header: bool = True, - urs: str = 'https://urs.earthdata.nasa.gov' - ): + username: str, + password: str, + context: ssl.SSLContext = _default_ssl_context, + password_manager: bool = False, + get_ca_certs: bool = False, + redirect: bool = False, + authorization_header: bool = True, + urs: str = 'https://urs.earthdata.nasa.gov', +): """ Build ``urllib`` opener for NASA Earthdata with supplied credentials @@ -1099,7 +1121,7 @@ def build_opener( # add Authorization header to opener if authorization_header: b64 = base64.b64encode(f'{username}:{password}'.encode()) - opener.addheaders = [("Authorization", f"Basic {b64.decode()}")] + opener.addheaders = [('Authorization', f'Basic {b64.decode()}')] # Now all calls to urllib2.urlopen use our opener. urllib2.install_opener(opener) # All calls to urllib2.urlopen will now use handler @@ -1107,15 +1129,16 @@ def build_opener( # HTTPPasswordMgrWithDefaultRealm will be confused. return opener + # PURPOSE: generate a NASA Earthdata user token def get_token( - HOST: str = 'https://urs.earthdata.nasa.gov/api/users/token', - username: str | None = None, - password: str | None = None, - build: bool = True, - context: ssl.SSLContext = _default_ssl_context, - urs: str = 'urs.earthdata.nasa.gov', - ): + HOST: str = 'https://urs.earthdata.nasa.gov/api/users/token', + username: str | None = None, + password: str | None = None, + build: bool = True, + context: ssl.SSLContext = _default_ssl_context, + urs: str = 'urs.earthdata.nasa.gov', +): """ Generate a NASA Earthdata User Token @@ -1143,14 +1166,16 @@ def get_token( """ # attempt to build urllib2 opener and check credentials if build: - attempt_login(urs, + attempt_login( + urs, username=username, password=password, context=context, password_manager=False, get_ca_certs=False, redirect=False, - authorization_header=True) + authorization_header=True, + ) # create post response with Earthdata token API try: request = urllib2.Request(HOST, method='POST') @@ -1164,15 +1189,16 @@ def get_token( # read and return JSON response return json.loads(response.read()) + # PURPOSE: generate a NASA Earthdata user token def list_tokens( - HOST: str = 'https://urs.earthdata.nasa.gov/api/users/tokens', - username: str | None = None, - password: str | None = None, - build: bool = True, - context: ssl.SSLContext = _default_ssl_context, - urs: str = 'urs.earthdata.nasa.gov', - ): + HOST: str = 'https://urs.earthdata.nasa.gov/api/users/tokens', + username: str | None = None, + password: str | None = None, + build: bool = True, + context: ssl.SSLContext = _default_ssl_context, + urs: str = 'urs.earthdata.nasa.gov', +): """ List the current associated NASA Earthdata User Tokens @@ -1200,14 +1226,16 @@ def list_tokens( """ # attempt to build urllib2 opener and check credentials if build: - attempt_login(urs, + attempt_login( + urs, username=username, password=password, context=context, password_manager=False, get_ca_certs=False, redirect=False, - authorization_header=True) + authorization_header=True, + ) # create get response with Earthdata list tokens API try: request = urllib2.Request(HOST) @@ -1221,16 +1249,17 @@ def list_tokens( # read and return JSON response return json.loads(response.read()) + # PURPOSE: revoke a NASA Earthdata user token def revoke_token( - token: str, - HOST: str = f'https://urs.earthdata.nasa.gov/api/users/revoke_token', - username: str | None = None, - password: str | None = None, - build: bool = True, - context: ssl.SSLContext = _default_ssl_context, - urs: str = 'urs.earthdata.nasa.gov', - ): + token: str, + HOST: str = f'https://urs.earthdata.nasa.gov/api/users/revoke_token', + username: str | None = None, + password: str | None = None, + build: bool = True, + context: ssl.SSLContext = _default_ssl_context, + urs: str = 'urs.earthdata.nasa.gov', +): """ Generate a NASA Earthdata User Token @@ -1255,14 +1284,16 @@ def revoke_token( """ # attempt to build urllib2 opener and check credentials if build: - attempt_login(urs, + attempt_login( + urs, username=username, password=password, context=context, password_manager=False, get_ca_certs=False, redirect=False, - authorization_header=True) + authorization_header=True, + ) # full path for NASA Earthdata revoke token API url = f'{HOST}?token={token}' # create post response with Earthdata revoke tokens API @@ -1278,6 +1309,7 @@ def revoke_token( # verbose response logging.debug(f'Token Revoked: {token}') + # NASA on-prem DAAC providers _daac_providers = { 'gesdisc': 'GES_DISC', @@ -1305,7 +1337,7 @@ def revoke_token( 'lpdaac': 'https://data.lpdaac.earthdatacloud.nasa.gov/s3credentials', 'nsidc': 'https://data.nsidc.earthdatacloud.nasa.gov/s3credentials', 'ornldaac': 'https://data.ornldaac.earthdata.nasa.gov/s3credentials', - 'podaac': 'https://archive.podaac.earthdata.nasa.gov/s3credentials' + 'podaac': 'https://archive.podaac.earthdata.nasa.gov/s3credentials', } # NASA Cumulus AWS S3 buckets @@ -1316,9 +1348,10 @@ def revoke_token( 'nsidc': 'nsidc-cumulus-prod-protected', 'ornldaac': 'ornl-cumulus-prod-protected', 'podaac': 'podaac-ops-cumulus-protected', - 'podaac-doc': 'podaac-ops-cumulus-docs' + 'podaac-doc': 'podaac-ops-cumulus-docs', } + def s3_region(): """ Get AWS s3 region for EC2 instance @@ -1332,12 +1365,13 @@ def s3_region(): region_name = boto3.session.Session().region_name return region_name + # PURPOSE: get AWS s3 client for PO.DAAC Cumulus def s3_client( - HOST: str = _s3_endpoints['podaac'], - timeout: int | None = None, - region_name: str = 'us-west-2' - ): + HOST: str = _s3_endpoints['podaac'], + timeout: int | None = None, + region_name: str = 'us-west-2', +): """ Get AWS s3 client for PO.DAAC Cumulus @@ -1360,14 +1394,17 @@ def s3_client( cumulus = json.loads(response.read()) # get AWS client object boto3 = import_dependency('boto3') - client = boto3.client('s3', + client = boto3.client( + 's3', aws_access_key_id=cumulus['accessKeyId'], aws_secret_access_key=cumulus['secretAccessKey'], aws_session_token=cumulus['sessionToken'], - region_name=region_name) + region_name=region_name, + ) # return the AWS client for region return client + # PURPOSE: get a s3 bucket name from a presigned url def s3_bucket(presigned_url: str) -> str: """ @@ -1387,6 +1424,7 @@ def s3_bucket(presigned_url: str) -> str: bucket = re.sub(r's3:\/\/', r'', host[0], re.IGNORECASE) return bucket + # PURPOSE: get a s3 bucket key from a presigned url def s3_key(presigned_url: str) -> str: """ @@ -1406,6 +1444,7 @@ def s3_key(presigned_url: str) -> str: key = posixpath.join(*host[1:]) return key + # PURPOSE: check that entered NASA Earthdata credentials are valid def check_credentials(HOST: str = _s3_endpoints['podaac']): """ @@ -1424,18 +1463,19 @@ def check_credentials(HOST: str = _s3_endpoints['podaac']): else: return True + # PURPOSE: list a directory on JPL PO.DAAC/ECCO Drive https server def drive_list( - HOST: str | list, - username: str | None = None, - password: str | None = None, - build: bool = True, - timeout: int | None = None, - urs: str = 'podaac-tools.jpl.nasa.gov', - parser = lxml.etree.HTMLParser(), - pattern: str = '', - sort: bool = False - ): + HOST: str | list, + username: str | None = None, + password: str | None = None, + build: bool = True, + timeout: int | None = None, + urs: str = 'podaac-tools.jpl.nasa.gov', + parser=lxml.etree.HTMLParser(), + pattern: str = '', + sort: bool = False, +): """ List a directory on `JPL PO.DAAC `_ or @@ -1471,7 +1511,7 @@ def drive_list( """ # use netrc credentials if build and not (username or password): - username,_,password = netrc.netrc().authenticators(urs) + username, _, password = netrc.netrc().authenticators(urs) # build urllib2 opener and check credentials if build: # build urllib2 opener with credentials @@ -1485,7 +1525,9 @@ def drive_list( try: # Create and submit request. request = urllib2.Request(posixpath.join(*HOST)) - tree = lxml.etree.parse(urllib2.urlopen(request, timeout=timeout),parser) + tree = lxml.etree.parse( + urllib2.urlopen(request, timeout=timeout), parser + ) except (urllib2.HTTPError, urllib2.URLError) as exc: raise Exception('List error from {0}'.format(posixpath.join(*HOST))) else: @@ -1495,34 +1537,35 @@ def drive_list( collastmod = [get_unix_time(i) for i in tree.xpath('//tr/td[3]/text()')] # reduce using regular expression pattern if pattern: - i = [i for i,f in enumerate(colnames) if re.search(pattern,f)] + i = [i for i, f in enumerate(colnames) if re.search(pattern, f)] # reduce list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # sort the list if sort: - i = [i for i,j in sorted(enumerate(colnames), key=lambda i: i[1])] + i = [i for i, j in sorted(enumerate(colnames), key=lambda i: i[1])] # sort list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # return the list of column names and last modified times - return (colnames,collastmod) + return (colnames, collastmod) + # PURPOSE: download a file from a PO.DAAC/ECCO Drive https server def from_drive( - HOST: str | list, - username: str | None = None, - password: str | None = None, - build: bool = True, - timeout: int | None = None, - urs: str = 'podaac-tools.jpl.nasa.gov', - local: str | pathlib.Path | None = None, - hash: str = '', - chunk: int = 16384, - verbose: bool = False, - fid = sys.stdout, - mode: oct = 0o775 - ): + HOST: str | list, + username: str | None = None, + password: str | None = None, + build: bool = True, + timeout: int | None = None, + urs: str = 'podaac-tools.jpl.nasa.gov', + local: str | pathlib.Path | None = None, + hash: str = '', + chunk: int = 16384, + verbose: bool = False, + fid=sys.stdout, + mode: oct = 0o775, +): """ Download a file from a `JPL PO.DAAC `_ or @@ -1565,7 +1608,7 @@ def from_drive( logging.basicConfig(stream=fid, level=loglevel) # use netrc credentials if build and not (username or password): - username,_,password = netrc.netrc().authenticators(urs) + username, _, password = netrc.netrc().authenticators(urs) # build urllib2 opener and check credentials if build: # build urllib2 opener with credentials @@ -1610,15 +1653,16 @@ def from_drive( remote_buffer.seek(0) return remote_buffer + # PURPOSE: retrieve shortnames for GRACE/GRACE-FO products def cmr_product_shortname( - mission: str, - center: str, - release: str, - level: str = 'L2', - version: str = '0', - product: list = ['GAA','GAB','GAC','GAD','GSM'] - ): + mission: str, + center: str, + release: str, + level: str = 'L2', + version: str = '0', + product: list = ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], +): """ Create a list of product shortnames for NASA Common Metadata Repository (CMR) queries @@ -1657,22 +1701,22 @@ def cmr_product_shortname( gracefo_l1_format = 'GRACEFO_{0}_{1}_GRAV_{2}_{3}' gracefo_l2_format = 'GRACEFO_{0}_{1}_MONTHLY_{2}{3}' # dictionary entries for each product level - cmr_shortname['grace']['L1B'] = dict(GFZ={},JPL={}) - cmr_shortname['grace']['L2'] = dict(CSR={},GFZ={},JPL={}) + cmr_shortname['grace']['L1B'] = dict(GFZ={}, JPL={}) + cmr_shortname['grace']['L2'] = dict(CSR={}, GFZ={}, JPL={}) cmr_shortname['grace-fo']['L1A'] = dict(JPL={}) cmr_shortname['grace-fo']['L1B'] = dict(JPL={}) - cmr_shortname['grace-fo']['L2'] = dict(CSR={},GFZ={},JPL={}) + cmr_shortname['grace-fo']['L2'] = dict(CSR={}, GFZ={}, JPL={}) # dictionary entry for GRACE Level-1B dealiasing products # for each data release for rl in ['RL06']: - shortname = grace_l1_format.format('AOD1B','GFZ',rl) + shortname = grace_l1_format.format('AOD1B', 'GFZ', rl) cmr_shortname['grace']['L1B']['GFZ'][rl] = [shortname] # dictionary entries for GRACE Level-1B ranging data products # for each data release - for rl in ['RL02','RL03']: - shortname = grace_l1_format.format('L1B','JPL',rl) + for rl in ['RL02', 'RL03']: + shortname = grace_l1_format.format('L1B', 'JPL', rl) cmr_shortname['grace']['L1B']['JPL'][rl] = [shortname] # dictionary entries for GRACE Level-2 products @@ -1689,29 +1733,29 @@ def cmr_product_shortname( product = [product] # create list of product shortnames for GRACE level-2 products # for each L2 data processing center - for c in ['CSR','GFZ','JPL']: + for c in ['CSR', 'GFZ', 'JPL']: # for each level-2 product for p in product: # skip atmospheric and oceanic dealiasing products for CSR if (c == 'CSR') and p in ('GAA', 'GAB'): continue # shortname for center and product - shortname = grace_l2_format.format(p,'L2',c,rl) + shortname = grace_l2_format.format(p, 'L2', c, rl) cmr_shortname['grace']['L2'][c][rl].append(shortname) # dictionary entries for GRACE-FO Level-1 ranging data products # for each data release for rl in ['RL04']: - for l in ['L1A','L1B']: - shortname = gracefo_l1_format.format(l,'ASCII','JPL',rl) + for l in ['L1A', 'L1B']: + shortname = gracefo_l1_format.format(l, 'ASCII', 'JPL', rl) cmr_shortname['grace-fo'][l]['JPL'][rl] = [shortname] # dictionary entries for GRACE-FO Level-2 products # for each data release for rl in ['RL06']: - rs = re.findall(r'\d+',rl).pop().zfill(3) - for c in ['CSR','GFZ','JPL']: - shortname = gracefo_l2_format.format('L2',c,rs,version) + rs = re.findall(r'\d+', rl).pop().zfill(3) + for c in ['CSR', 'GFZ', 'JPL']: + shortname = gracefo_l2_format.format('L2', c, rs, version) cmr_shortname['grace-fo']['L2'][c][rl] = [shortname] # try to retrieve the shortname for a given mission @@ -1722,12 +1766,10 @@ def cmr_product_shortname( else: return cmr_shortnames + def cmr_readable_granules( - product: str, - level: str = 'L2', - solution: str = 'BA01', - version: str = '0' - ): + product: str, level: str = 'L2', solution: str = 'BA01', version: str = '0' +): """ Create readable granule names pattern for NASA Common Metadata Repository (CMR) queries @@ -1763,7 +1805,7 @@ def cmr_readable_granules( elif (level == 'L2') and (product == 'GSM'): args = (product, solution, version) pattern = '{0}-2_???????-???????_????_?????_{1}_???{2}*'.format(*args) - elif (level == 'L2'): + elif level == 'L2': args = (product, 'BC01', version) pattern = '{0}-2_???????-???????_????_?????_{1}_???{2}*'.format(*args) else: @@ -1771,11 +1813,9 @@ def cmr_readable_granules( # return readable granules pattern return pattern + # PURPOSE: filter the CMR json response for desired data files -def cmr_filter_json( - search_results: dict, - endpoint: str = 'data' - ): +def cmr_filter_json(search_results: dict, endpoint: str = 'data'): """ Filter the NASA Common Metadata Repository (CMR) json response for desired data files @@ -1804,29 +1844,30 @@ def cmr_filter_json( granule_urls = [] granule_mtimes = [] # check that there are urls for request - if ('feed' not in search_results) or ('entry' not in search_results['feed']): - return (granule_names,granule_urls) + if ('feed' not in search_results) or ( + 'entry' not in search_results['feed'] + ): + return (granule_names, granule_urls) # descriptor links for each endpoint rel = {} - rel['data'] = "http://esipfed.org/ns/fedsearch/1.1/data#" - rel['s3'] = "http://esipfed.org/ns/fedsearch/1.1/s3#" + rel['data'] = 'http://esipfed.org/ns/fedsearch/1.1/data#' + rel['s3'] = 'http://esipfed.org/ns/fedsearch/1.1/s3#' # iterate over references and get cmr location for entry in search_results['feed']['entry']: granule_names.append(entry['title']) - granule_mtimes.append(get_unix_time(entry['updated'], - format='%Y-%m-%dT%H:%M:%S.%f%z')) + granule_mtimes.append( + get_unix_time(entry['updated'], format='%Y-%m-%dT%H:%M:%S.%f%z') + ) for link in entry['links']: - if (link['rel'] == rel[endpoint]): + if link['rel'] == rel[endpoint]: granule_urls.append(link['href']) break # return the list of urls, granule ids and modified times - return (granule_names,granule_urls,granule_mtimes) + return (granule_names, granule_urls, granule_mtimes) + # PURPOSE: filter the CMR json response for desired metadata files -def cmr_metadata_json( - search_results: dict, - endpoint: str = 'data' - ): +def cmr_metadata_json(search_results: dict, endpoint: str = 'data'): """ Filter the NASA Common Metadata Repository (CMR) json response for desired metadata files @@ -1850,38 +1891,41 @@ def cmr_metadata_json( # output list of collection urls collection_urls = [] # check that there are urls for request - if ('feed' not in search_results) or ('entry' not in search_results['feed']): + if ('feed' not in search_results) or ( + 'entry' not in search_results['feed'] + ): return collection_urls # descriptor links for each endpoint rel = {} - rel['documentation'] = "http://esipfed.org/ns/fedsearch/1.1/documentation#" - rel['data'] = "http://esipfed.org/ns/fedsearch/1.1/data#" - rel['s3'] = "http://esipfed.org/ns/fedsearch/1.1/s3#" + rel['documentation'] = 'http://esipfed.org/ns/fedsearch/1.1/documentation#' + rel['data'] = 'http://esipfed.org/ns/fedsearch/1.1/data#' + rel['s3'] = 'http://esipfed.org/ns/fedsearch/1.1/s3#' # iterate over references and get cmr location for entry in search_results['feed']['entry']: for link in entry['links']: - if (link['rel'] == rel[endpoint]): + if link['rel'] == rel[endpoint]: collection_urls.append(link['href']) # return the list of urls return collection_urls + # PURPOSE: cmr queries for GRACE/GRACE-FO products def cmr( - mission: str | None = None, - center: str | None = None, - release: str | None = None, - level: str | None = 'L2', - product: str | None = None, - solution: str | None = 'BA01', - version: str | None = '0', - start_date: str | None = None, - end_date: str | None = None, - provider: str | None = 'POCLOUD', - endpoint: str | None = 'data', - context: ssl.SSLContext = _default_ssl_context, - verbose: bool = False, - fid = sys.stdout - ): + mission: str | None = None, + center: str | None = None, + release: str | None = None, + level: str | None = 'L2', + product: str | None = None, + solution: str | None = 'BA01', + version: str | None = '0', + start_date: str | None = None, + end_date: str | None = None, + provider: str | None = 'POCLOUD', + endpoint: str | None = 'data', + context: ssl.SSLContext = _default_ssl_context, + verbose: bool = False, + fid=sys.stdout, +): """ Query the NASA Common Metadata Repository (CMR) for GRACE/GRACE-FO data @@ -1947,8 +1991,11 @@ def cmr( cmr_query_type = 'granules' cmr_format = 'json' cmr_page_size = 2000 - CMR_HOST = ['https://cmr.earthdata.nasa.gov','search', - f'{cmr_query_type}.{cmr_format}'] + CMR_HOST = [ + 'https://cmr.earthdata.nasa.gov', + 'search', + f'{cmr_query_type}.{cmr_format}', + ] # build list of CMR query parameters CMR_KEYS = [] CMR_KEYS.append(f'?provider={provider}') @@ -1956,8 +2003,9 @@ def cmr( CMR_KEYS.append('&sort_key[]=producer_granule_id') CMR_KEYS.append(f'&page_size={cmr_page_size}') # dictionary of product shortnames - short_names = cmr_product_shortname(mission, center, release, - level=level, version=version) + short_names = cmr_product_shortname( + mission, center, release, level=level, version=version + ) for short_name in short_names: CMR_KEYS.append(f'&short_name={short_name}') # append keys for start and end time @@ -1966,13 +2014,14 @@ def cmr( end_date = isoformat(end_date) if end_date else '' CMR_KEYS.append(f'&temporal={start_date},{end_date}') # append keys for querying specific products - CMR_KEYS.append("&options[readable_granule_name][pattern]=true") - CMR_KEYS.append("&options[spatial][or]=true") - readable_granule = cmr_readable_granules(product, - level=level, solution=solution, version=version) - CMR_KEYS.append(f"&readable_granule_name[]={readable_granule}") + CMR_KEYS.append('&options[readable_granule_name][pattern]=true') + CMR_KEYS.append('&options[spatial][or]=true') + readable_granule = cmr_readable_granules( + product, level=level, solution=solution, version=version + ) + CMR_KEYS.append(f'&readable_granule_name[]={readable_granule}') # full CMR query url - cmr_query_url = "".join([posixpath.join(*CMR_HOST),*CMR_KEYS]) + cmr_query_url = ''.join([posixpath.join(*CMR_HOST), *CMR_KEYS]) logging.info(f'CMR request={cmr_query_url}') # output list of granule names and urls granule_names = [] @@ -1987,11 +2036,11 @@ def cmr( logging.debug(f'CMR-Search-After: {cmr_search_after}') response = opener.open(req) # get search after index for next iteration - headers = {k.lower():v for k,v in dict(response.info()).items()} + headers = {k.lower(): v for k, v in dict(response.info()).items()} cmr_search_after = headers.get('cmr-search-after') # read the CMR search as JSON search_page = json.loads(response.read().decode('utf8')) - ids,urls,mtimes = cmr_filter_json(search_page, endpoint=endpoint) + ids, urls, mtimes = cmr_filter_json(search_page, endpoint=endpoint) if not urls or cmr_search_after is None: break # extend lists @@ -2001,20 +2050,21 @@ def cmr( # return the list of granule ids, urls and modification times return (granule_names, granule_urls, granule_mtimes) + # PURPOSE: cmr queries for GRACE/GRACE-FO auxiliary data and documentation def cmr_metadata( - mission: str | None = None, - center: str | None = None, - release: str | None = None, - level: str | None = 'L2', - version: str | None = '0', - provider: str | None = 'POCLOUD', - endpoint: str | None = 'data', - pattern: str | None = '', - context: ssl.SSLContext = _default_ssl_context, - verbose: bool = False, - fid = sys.stdout - ): + mission: str | None = None, + center: str | None = None, + release: str | None = None, + level: str | None = 'L2', + version: str | None = '0', + provider: str | None = 'POCLOUD', + endpoint: str | None = 'data', + pattern: str | None = '', + context: ssl.SSLContext = _default_ssl_context, + verbose: bool = False, + fid=sys.stdout, +): """ Query the NASA Common Metadata Repository (CMR) for GRACE/GRACE-FO auxiliary data and documentation @@ -2071,18 +2121,22 @@ def cmr_metadata( # build CMR query cmr_query_type = 'collections' cmr_format = 'json' - CMR_HOST = ['https://cmr.earthdata.nasa.gov','search', - f'{cmr_query_type}.{cmr_format}'] + CMR_HOST = [ + 'https://cmr.earthdata.nasa.gov', + 'search', + f'{cmr_query_type}.{cmr_format}', + ] # build list of CMR query parameters CMR_KEYS = [] CMR_KEYS.append(f'?provider={provider}') # dictionary of product shortnames - short_names = cmr_product_shortname(mission, center, release, - level=level, version=version) + short_names = cmr_product_shortname( + mission, center, release, level=level, version=version + ) for short_name in short_names: CMR_KEYS.append(f'&short_name={short_name}') # full CMR query url - cmr_query_url = "".join([posixpath.join(*CMR_HOST),*CMR_KEYS]) + cmr_query_url = ''.join([posixpath.join(*CMR_HOST), *CMR_KEYS]) logging.info(f'CMR request={cmr_query_url}') # query CMR for collection metadata req = urllib2.Request(cmr_query_url) @@ -2093,21 +2147,22 @@ def cmr_metadata( collection_urls = cmr_metadata_json(search_page, endpoint=endpoint) # reduce using regular expression pattern if pattern: - i = [i for i,f in enumerate(collection_urls) if re.search(pattern,f)] + i = [i for i, f in enumerate(collection_urls) if re.search(pattern, f)] # reduce list of collection_urls collection_urls = [collection_urls[indice] for indice in i] # return the list of collection urls return collection_urls + # PURPOSE: create and compile regular expression operator to find GRACE files def compile_regex_pattern( - PROC: str, - DREL: str, - DSET: str, - mission: str | None = None, - solution: str | None = r'BA01', - version: str | None = r'\d+' - ): + PROC: str, + DREL: str, + DSET: str, + mission: str | None = None, + solution: str | None = r'BA01', + version: str | None = r'\d+', +): """ Compile regular expressor operators for finding a specified subset of GRACE/GRACE-FO Level-2 spherical harmonic files @@ -2146,57 +2201,57 @@ def compile_regex_pattern( GRACE/GRACE-FO Level-2 data version """ # verify inputs - if mission and mission not in ('GRAC','GRFO'): + if mission and mission not in ('GRAC', 'GRFO'): raise ValueError(f'Unknown mission {mission}') - if PROC not in ('CNES','CSR','GFZ','JPL'): + if PROC not in ('CNES', 'CSR', 'GFZ', 'JPL'): raise ValueError(f'Unknown processing center {PROC}') - if DSET not in ('GAA','GAB','GAC','GAD','GSM'): + if DSET not in ('GAA', 'GAB', 'GAC', 'GAD', 'GSM'): raise ValueError(f'Unknown Level-2 product {DSET}') if isinstance(version, int): version = str(version).zfill(2) # compile regular expression operator for inputs - if ((DSET == 'GSM') and (PROC == 'CSR') and (DREL in ('RL04','RL05'))): + if (DSET == 'GSM') and (PROC == 'CSR') and (DREL in ('RL04', 'RL05')): # CSR GSM: only monthly degree 60 products # not the longterm degree 180, degree 96 dataset or the # special order 30 datasets for the high-resonance months - release, = re.findall(r'\d+', DREL) + (release,) = re.findall(r'\d+', DREL) args = (DSET, int(release)) pattern = r'{0}-2_\d+-\d+_\d+_UTCSR_0060_000{1:d}(\.gz)?$' - elif ((DSET == 'GSM') and (PROC == 'CSR') and (DREL == 'RL06')): + elif (DSET == 'GSM') and (PROC == 'CSR') and (DREL == 'RL06'): # CSR GSM RL06: monthly products for mission and solution - release, = re.findall(r'\d+', DREL) + (release,) = re.findall(r'\d+', DREL) args = (DSET, mission, solution, release.zfill(2), version.zfill(2)) pattern = r'{0}-2_\d+-\d+_{1}_UTCSR_{2}_{3}{4}(\.gz)?$' - elif ((DSET == 'GSM') and (PROC == 'CSR') and (DREL.endswith('LRI'))): + elif (DSET == 'GSM') and (PROC == 'CSR') and (DREL.endswith('LRI')): # CSR GSM LRI solutions: monthly products for mission and solution release, version = re.findall(r'(\d+)\.(\d+)', DREL).pop() args = (DSET, mission, r'EA01', release.zfill(2), version.zfill(2)) pattern = r'{0}-2_\d+-\d+_{1}_UTCSR_{2}_{3}{4}(\.gz)?$' - elif ((DSET == 'GSM') and (PROC == 'GFZ') and (DREL == 'RL04')): + elif (DSET == 'GSM') and (PROC == 'GFZ') and (DREL == 'RL04'): # GFZ RL04: only unconstrained solutions (not GK2 products) args = (DSET,) pattern = r'{0}-2_\d+-\d+_\d+_EIGEN_G---_0004(\.gz)?$' - elif ((DSET == 'GSM') and (PROC == 'GFZ') and (DREL == 'RL05')): + elif (DSET == 'GSM') and (PROC == 'GFZ') and (DREL == 'RL05'): # GFZ RL05: updated RL05a products which are less constrained to # the background model. Allow regularized fields args = (DSET, r'(G---|GK2-)') pattern = r'{0}-2_\d+-\d+_\d+_EIGEN_{1}_005a(\.gz)?$' - elif ((DSET == 'GSM') and (PROC == 'GFZ') and (DREL == 'RL06')): + elif (DSET == 'GSM') and (PROC == 'GFZ') and (DREL == 'RL06'): # GFZ GSM RL06: monthly products for mission and solution - release, = re.findall(r'\d+', DREL) + (release,) = re.findall(r'\d+', DREL) args = (DSET, mission, solution, release.zfill(2), version.zfill(2)) pattern = r'{0}-2_\d+-\d+_{1}_GFZOP_{2}_{3}{4}(\.gz)?$' - elif (PROC == 'JPL') and DREL in ('RL04','RL05'): + elif (PROC == 'JPL') and DREL in ('RL04', 'RL05'): # JPL: RL04a and RL05a products (denoted by 0001) - release, = re.findall(r'\d+', DREL) + (release,) = re.findall(r'\d+', DREL) args = (DSET, int(release)) pattern = r'{0}-2_\d+-\d+_\d+_JPLEM_0001_000{1:d}(\.gz)?$' - elif ((DSET == 'GSM') and (PROC == 'JPL') and (DREL == 'RL06')): + elif (DSET == 'GSM') and (PROC == 'JPL') and (DREL == 'RL06'): # JPL GSM RL06: monthly products for mission and solution - release, = re.findall(r'\d+', DREL) + (release,) = re.findall(r'\d+', DREL) args = (DSET, mission, solution, release.zfill(2), version.zfill(2)) pattern = r'{0}-2_\d+-\d+_{1}_JPLEM_{2}_{3}{4}(\.gz)?$' - elif (PROC == 'CNES'): + elif PROC == 'CNES': # CNES: use products in standard format args = (DSET,) pattern = r'{0}-2_\d+-\d+_\d+_GRGS_([a-zA-Z0-9_\-]+)(\.txt)?(\.gz)?$' @@ -2211,20 +2266,21 @@ def compile_regex_pattern( # return the compiled regular expression operator return re.compile(pattern.format(*args), re.VERBOSE) + # PURPOSE: download geocenter files from Sutterley and Velicogna (2019) # https://doi.org/10.3390/rs11182108 # https://doi.org/10.6084/m9.figshare.7388540 def from_figshare( - directory: str | pathlib.Path, - article: str = '7388540', - timeout: int | None = None, - context: ssl.SSLContext = _default_ssl_context, - chunk: int | None = 16384, - verbose: bool = False, - fid = sys.stdout, - pattern: str = r'(CSR|GFZ|JPL)_(RL\d+)_(.*?)_SLF_iter.txt$', - mode: oct = 0o775 - ): + directory: str | pathlib.Path, + article: str = '7388540', + timeout: int | None = None, + context: ssl.SSLContext = _default_ssl_context, + chunk: int | None = 16384, + verbose: bool = False, + fid=sys.stdout, + pattern: str = r'(CSR|GFZ|JPL)_(RL\d+)_(.*?)_SLF_iter.txt$', + mode: oct = 0o775, +): """ Download :cite:p:`Sutterley:2019bx` geocenter files from `figshare `_ @@ -2251,22 +2307,23 @@ def from_figshare( permissions mode of output local file """ # figshare host - HOST=['https://api.figshare.com','v2','articles',article] + HOST = ['https://api.figshare.com', 'v2', 'articles', article] # recursively create directory if non-existent directory = pathlib.Path(directory).expanduser().absolute() local_dir = directory.joinpath('geocenter') local_dir.mkdir(mode=mode, parents=True, exist_ok=True) # Create and submit request. request = urllib2.Request(posixpath.join(*HOST)) - response = urllib2.urlopen(request, timeout=timeout,context=context) + response = urllib2.urlopen(request, timeout=timeout, context=context) resp = json.loads(response.read()) # reduce list of geocenter files - geocenter_files = [f for f in resp['files'] if re.match(pattern,f['name'])] + geocenter_files = [f for f in resp['files'] if re.match(pattern, f['name'])] for f in geocenter_files: # download geocenter file local_file = local_dir.joinpath(f['name']) original_md5 = get_hash(local_file) - from_http(f['download_url'], + from_http( + f['download_url'], timeout=timeout, context=context, local=local_file, @@ -2274,24 +2331,26 @@ def from_figshare( chunk=chunk, verbose=verbose, fid=fid, - mode=mode) + mode=mode, + ) # verify MD5 checksums computed_md5 = get_hash(local_file) - if (computed_md5 != f['supplied_md5']): + if computed_md5 != f['supplied_md5']: raise Exception(f'Checksum mismatch: {f["download_url"]}') + # PURPOSE: send files to figshare using secure FTP uploader def to_figshare( - files: list, - username: str | None = None, - password: str | None = None, - directory: str | None | pathlib.Path = None, - timeout: int | None = None, - context: ssl.SSLContext = _default_ssl_context, - get_ca_certs: bool = False, - verbose: bool = False, - chunk: int = 8192 - ): + files: list, + username: str | None = None, + password: str | None = None, + directory: str | None | pathlib.Path = None, + timeout: int | None = None, + context: ssl.SSLContext = _default_ssl_context, + get_ca_certs: bool = False, + verbose: bool = False, + chunk: int = 8192, +): """ Send files to figshare using secure `FTP uploader `_ files from NASA Goddard Space Flight Center (GSFC) @@ -2642,7 +2762,8 @@ def from_gsfc( FILE = 'gsfc_slr_5x5c61s61.txt' local_file = directory.joinpath(FILE) original_md5 = get_hash(local_file) - fileID = from_http(posixpath.join(host,FILE), + fileID = from_http( + posixpath.join(host, FILE), timeout=timeout, context=context, local=local_file, @@ -2650,15 +2771,16 @@ def from_gsfc( chunk=chunk, verbose=verbose, fid=fid, - mode=mode) + mode=mode, + ) # create a dated copy for archival purposes if copy: # create copy of file for archiving # read file and extract data date span file_contents = fileID.read().decode('utf-8').splitlines() - data_span, = [l for l in file_contents if l.startswith('Data span:')] + (data_span,) = [l for l in file_contents if l.startswith('Data span:')] # extract start and end of data date span - span_start,span_end = re.findall(r'\d+[\s+]\w{3}[\s+]\d{4}', data_span) + span_start, span_end = re.findall(r'\d+[\s+]\w{3}[\s+]\d{4}', data_span) # create copy of file with date span in filename YM1 = time.strftime('%Y%m', time.strptime(span_start, '%d %b %Y')) YM2 = time.strftime('%Y%m', time.strptime(span_end, '%d %b %Y')) @@ -2667,13 +2789,14 @@ def from_gsfc( # copy modification times and permissions for archive file shutil.copystat(local_file, directory.joinpath(COPY)) + # PURPOSE: list a directory on the GFZ ICGEM https server # http://icgem.gfz-potsdam.de def icgem_list( - host: str = 'http://icgem.gfz-potsdam.de/tom_longtime', - timeout: int | None = None, - parser=lxml.etree.HTMLParser() - ): + host: str = 'http://icgem.gfz-potsdam.de/tom_longtime', + timeout: int | None = None, + parser=lxml.etree.HTMLParser(), +): """ Parse the table of static gravity field models on the GFZ `International Centre for Global Earth Models (ICGEM) `_ @@ -2697,7 +2820,9 @@ def icgem_list( try: # Create and submit request. request = urllib2.Request(host) - tree = lxml.etree.parse(urllib2.urlopen(request, timeout=timeout),parser) + tree = lxml.etree.parse( + urllib2.urlopen(request, timeout=timeout), parser + ) except: raise Exception(f'List error from {host}') else: @@ -2705,5 +2830,8 @@ def icgem_list( colfiles = tree.xpath('//td[@class="tom-cell-modelfile"]//a/@href') # reduce list of files to find gfc files # return the dict of model files mapped by name - return {re.findall(r'(.*?).gfc',posixpath.basename(f)).pop():url_split(f) - for i,f in enumerate(colfiles) if re.search(r'gfc$',f)} + return { + re.findall(r'(.*?).gfc', posixpath.basename(f)).pop(): url_split(f) + for i, f in enumerate(colfiles) + if re.search(r'gfc$', f) + } diff --git a/gravity_toolkit/version.py b/gravity_toolkit/version.py index aeb2a2ef..9a0df012 100644 --- a/gravity_toolkit/version.py +++ b/gravity_toolkit/version.py @@ -1,15 +1,16 @@ #!/usr/bin/env python -u""" +""" version.py (11/2023) Gets version number of a package """ + import importlib.metadata # package metadata -metadata = importlib.metadata.metadata("gravity_toolkit") +metadata = importlib.metadata.metadata('gravity_toolkit') # get version -version = metadata["version"] +version = metadata['version'] # append "v" before the version -full_version = f"v{version}" +full_version = f'v{version}' # get project name -project_name = metadata["Name"] +project_name = metadata['Name'] diff --git a/mapping/plot_AIS_GrIS_maps.py b/mapping/plot_AIS_GrIS_maps.py index 79059f1f..eaaaa51f 100644 --- a/mapping/plot_AIS_GrIS_maps.py +++ b/mapping/plot_AIS_GrIS_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_GrIS_maps.py Written by Tyler Sutterley (10/2023) @@ -63,6 +63,7 @@ Updated 09/2017: add plot scales Written 08/2017 """ + from __future__ import print_function import sys @@ -80,7 +81,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -89,20 +90,21 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # region directory, filename, title and data type region_dir = {} @@ -110,43 +112,92 @@ region_title = {} region_dtype = {} # Greenland ice divides -region_dir['N'] = ['masks','Rignot_GRE'] -region_title['N'] = ['CE','CW','NE','NO','NW','SE','SW'] +region_dir['N'] = ['masks', 'Rignot_GRE'] +region_title['N'] = ['CE', 'CW', 'NE', 'NO', 'NW', 'SE', 'SW'] # Antarctic 2012 basins -region_dir['S'] = ['masks','Rignot_ANT'] -region_title['S'] = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir['S'] = ['masks', 'Rignot_ANT'] +region_title['S'] = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename['N'] = 'divide_{0}_index.ascii' region_filename['S'] = 'basin_{0}_index.ascii' # regional datatypes -region_dtype['N'] = {'names':('lon','lat'),'formats':('f','f')} -region_dtype['S'] = {'names':('lat','lon'),'formats':('f','f')} +region_dtype['N'] = {'names': ('lon', 'lat'), 'formats': ('f', 'f')} +region_dtype['S'] = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins IMBIE_basin_file = {} -IMBIE_basin_file['N']=['masks','GRE_Basins_IMBIE2_v1.3','GRE_Basins_IMBIE2_v1.3.shp'] -IMBIE_basin_file['S']=['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file['N'] = [ + 'masks', + 'GRE_Basins_IMBIE2_v1.3', + 'GRE_Basins_IMBIE2_v1.3.shp', +] +IMBIE_basin_file['S'] = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract IMBIE_title = {} -IMBIE_title['N']=('CW','NE','NO','NW','SE','SW') -IMBIE_title['S']=('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title['N'] = ('CW', 'NE', 'NO', 'NW', 'SE', 'SW') +IMBIE_title['S'] = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics image_file = {} # MODIS mosaic of Greenland -image_file['N'] = ['MOG','mog500_2005_hp1_v1.1.tif'] +image_file['N'] = ['MOG', 'mog500_2005_hp1_v1.1.tif'] # MODIS mosaic of Antarctica -image_file['S'] = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file['S'] = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # coastline files coast_file = {} # Greenland grounded ice -coast_file['N']=['masks','GIMP','grn_ice_sheet_peripheral_glaciers.shp'] +coast_file['N'] = ['masks', 'GIMP', 'grn_ice_sheet_peripheral_glaciers.shp'] # Coastlines for antarctica (islands) -coast_file['S']=['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file['S'] = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # regional plot parameters # x and y limit @@ -165,18 +216,23 @@ projection = {} try: # cartopy transform for polar stereographic south - projection['S'] = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection['S'] = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) # cartopy transform for NSIDC polar stereographic north - projection['N'] = ccrs.Stereographic(central_longitude=-45.0, - central_latitude=+90.0,true_scale_latitude=+70.0) -except (NameError,ValueError) as exc: + projection['N'] = ccrs.Stereographic( + central_longitude=-45.0, + central_latitude=+90.0, + true_scale_latitude=+70.0, + ) +except (NameError, ValueError) as exc: pass # location and size of plot scales scale_params = {} -scale_params['N'] = (700e3,-3408462,800e3,89385,False) -scale_params['S'] = (-292e4,-230e4,1600e3,140e3,False) +scale_params['N'] = (700e3, -3408462, 800e3, 89385, False) +scale_params['S'] = (-292e4, -230e4, 1600e3, 140e3, False) + # PURPOSE: keep track of threads def info(args): @@ -187,18 +243,23 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir, HEM, projection): region_directory = base_dir.joinpath(*region_dir) # for each region for reg in region_title: # read the regional polylines - region_file = region_directory.joinpath(region_filename[HEM].format(reg)) + region_file = region_directory.joinpath( + region_filename[HEM].format(reg) + ) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Greenland and Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir, HEM): @@ -209,50 +270,69 @@ def plot_IMBIE2_basins(ax, base_dir, HEM): shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes - if (HEM == 'S'): + if HEM == 'S': # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title[HEM]] + i = [ + i + for i, a in enumerate(shape_attributes) + if a[1] in IMBIE_title[HEM] + ] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection[HEM]) - elif (HEM == 'N'): + ax.plot( + points[:, 0], points[:, 1], c='k', transform=projection[HEM] + ) + elif HEM == 'N': # no GIC or islands - i=[i for i,a in enumerate(shape_attributes) if a[0] in IMBIE_title[HEM]] + i = [ + i + for i, a in enumerate(shape_attributes) + if a[0] in IMBIE_title[HEM] + ] for indice in i: # extract lat/lon coordinates for record points = np.array(shape_entities[indice].points) # Greenland IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): + for p1, p2 in zip(parts[:-1], parts[1:]): # converting basin lat/lon into plot coordinates - ax.plot(points[p1:p2,0], points[p1:p2,1], color='k', - transform=ccrs.PlateCarree()) + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + color='k', + transform=ccrs.PlateCarree(), + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection['S']) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + c='k', + transform=projection['S'], + ) + # PURPOSE: plot Greenland and Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, HEM, START=1): @@ -260,38 +340,42 @@ def plot_grounded_ice(ax, base_dir, HEM, START=1): shape_input = shapefile.Reader(str(grounded_ice_shape_file)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - if (HEM == 'N'): - for i in range(START,START): + if HEM == 'N': + for i in range(START, START): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) - ax.plot(points[:,0], points[:,1], color='k', - transform=projection[HEM]) - if (HEM == 'S'): - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + ax.plot( + points[:, 0], points[:, 1], color='k', transform=projection[HEM] + ) + if HEM == 'S': + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], color='k', - transform=projection[HEM]) + ax.plot( + points[:, 0], points[:, 1], color='k', transform=projection[HEM] + ) + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, color='k', transform=ccrs.PlateCarree()) + # PURPOSE: plot MODIS mosaic of Antarctica and Greenland as background image def plot_image_mosaic(ax, base_dir, HEM, MASKED=True): # read MODIS mosaic of Antarctica and Greenland @@ -304,12 +388,12 @@ def plot_image_mosaic(ax, base_dir, HEM, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] - if (HEM == 'N'): + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] + if HEM == 'N': # dataset range vmin, vmax = (0, 16386) - elif (HEM == 'S'): + elif HEM == 'S': # dataset range vmin, vmax = (0, 16386) # read as grayscale image @@ -319,36 +403,75 @@ def plot_image_mosaic(ax, base_dir, HEM, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection[HEM]) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection[HEM], + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.2*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, alpha=0.5, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.2 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill( + [x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, alpha=0.5, zorder=4 + ) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=14, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=14, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=14, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=14, + color=fc2, + zorder=6, + ) + # PURPOSE: plot side by side maps of Greenland and Antarctica -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -379,11 +502,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -399,13 +522,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -414,21 +538,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -436,44 +560,48 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # calculate ratio of axes - greenland_ratio = np.float64(region_xlimit['N'][1]-region_xlimit['N'][0]) / \ - np.float64(region_ylimit['N'][1]-region_ylimit['N'][0]) - antarctica_ratio = np.float64(region_xlimit['S'][1]-region_xlimit['S'][0]) / \ - np.float64(region_ylimit['S'][1]-region_ylimit['S'][0]) - width_ratios = greenland_ratio/antarctica_ratio + greenland_ratio = np.float64( + region_xlimit['N'][1] - region_xlimit['N'][0] + ) / np.float64(region_ylimit['N'][1] - region_ylimit['N'][0]) + antarctica_ratio = np.float64( + region_xlimit['S'][1] - region_xlimit['S'][0] + ) / np.float64(region_ylimit['S'][1] - region_ylimit['S'][0]) + width_ratios = greenland_ratio / antarctica_ratio # make figure axes ax1 = {} fig = plt.figure(figsize=(15.5, 7)) - gs = gridspec.GridSpec(1, 2, width_ratios=[1,width_ratios]) - hem_flag = ['S','N'] - for i,HEM in enumerate(hem_flag): + gs = gridspec.GridSpec(1, 2, width_ratios=[1, width_ratios]) + hem_flag = ['S', 'N'] + for i, HEM in enumerate(hem_flag): ax1[i] = plt.subplot(gs[i], projection=projection[HEM]) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 -flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # for each hemisphere - for i,ax in ax1.items(): + for i, ax in ax1.items(): # set hemisphere flag HEM = hem_flag[i] logging.info(f'Hemisphere: {HEM}') @@ -484,20 +612,28 @@ def plot_grid(base_dir, FILENAMES, plot_image_mosaic(ax, base_dir, HEM) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -508,93 +644,137 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection[HEM], - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection[HEM], gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection[HEM]) + im = ax.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection[HEM], + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave=np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - ax.contour(lon,lat,data,[ave],colors='blue',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax, base_dir, HEM, projection[HEM]) start_indice = 1 if HEM == 'S' else 0 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax, base_dir, HEM) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2_subbasin'): + elif BASIN_TYPE == 'IMBIE-2_subbasin': plot_IMBIE2_subbasins(ax, base_dir) start_indice = 1 else: @@ -615,26 +795,37 @@ def plot_grid(base_dir, FILENAMES, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) # add title for each subplot if TITLES is not None: TITLE = ' '.join(TITLES[i].split('_')) - ax.set_title(TITLE.replace('-',u'\u2013'), fontsize=24) + ax.set_title(TITLE.replace('-', '\u2013'), fontsize=24) ax.title.set_y(1.00) # Add figure label for each subplot if LABELS is not None: - at = offsetbox.AnchoredText(LABELS[i], - loc=2, pad=0, frameon=True, - prop=dict(size=24,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.25") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABELS[i], + loc=2, + pad=0, + frameon=True, + prop=dict(size=24, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.25') + at.patch.set_edgecolor('white') ax.axes.add_artist(at) # draw map scale to corners @@ -650,8 +841,9 @@ def plot_grid(base_dir, FILENAMES, cax = fig.add_axes([0.905, 0.05, 0.022, 0.88]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False) + cbar = fig.colorbar( + im, cax=cax, extend=CBEXTEND, extendfrac=0.0375, drawedges=False + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -662,164 +854,273 @@ def plot_grid(base_dir, FILENAMES, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=24, labelsize=24, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=24, labelsize=24, direction='in' + ) # adjust plot and save - fig.subplots_adjust(left=0.02,right=0.89,bottom=0.01,top=0.97, - wspace=0.05,hspace=0.05) + fig.subplots_adjust( + left=0.02, right=0.89, bottom=0.01, top=0.97, wspace=0.05, hspace=0.05 + ) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates GMT-like plots for the Greenland and Antarctic ice sheets on polar stereographic projections """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', nargs=2, + parser.add_argument( + 'infile', + nargs=2, type=pathlib.Path, - help='Input grid files (Antarctica and Greenland)') + help='Input grid files (Antarctica and Greenland)', + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, nargs='+', - default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + nargs='+', + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', nargs=2, - type=str, help='Plot title') - parser.add_argument('--plot-label', nargs=2, - type=str, help='Plot label') + parser.add_argument('--plot-title', nargs=2, type=str, help='Plot title') + parser.add_argument('--plot-label', nargs=2, type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -829,7 +1130,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -858,7 +1161,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -866,6 +1170,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_AIS_grid_3maps.py b/mapping/plot_AIS_grid_3maps.py index 5e3b0bb5..701e4682 100644 --- a/mapping/plot_AIS_grid_3maps.py +++ b/mapping/plot_AIS_grid_3maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_grid_3maps.py Written by Tyler Sutterley (05/2023) Creates 3 GMT-like plots of the Antarctic Ice Sheet @@ -46,6 +46,7 @@ Updated 09/2019: added parameter for specifying if netCDF4 or HDF5 Written 09/2019 """ + from __future__ import print_function import sys @@ -63,7 +64,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -71,56 +72,104 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # Antarctica (AIS) # x and y limit (modified from Bamber 1km DEM) -xlimits = np.array([-3100000,3100000]) -ylimits = np.array([-2600000,2600000]) +xlimits = np.array([-3100000, 3100000]) +ylimits = np.array([-2600000, 2600000]) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -130,6 +179,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -139,9 +189,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -153,7 +205,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -161,30 +213,34 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -193,12 +249,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, MASKED=True): @@ -214,8 +270,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -223,40 +279,77 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # image extents - extents=(xmin,xmax,ymin,ymax) + extents = (xmin, xmax, ymin, ymax) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', extent=extents, - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + extent=extents, + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.2*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.2 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -287,11 +380,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -307,13 +400,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -322,21 +416,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -344,55 +438,65 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup polar stereographic maps - fig, (ax[0],ax[1],ax[2]) = plt.subplots(num=1, ncols=3, figsize=(11,3), - subplot_kw=dict(projection=projection)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, ncols=3, figsize=(11, 3), subplot_kw=dict(projection=projection) + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # plot image of MODIS mosaic of Antarctica as base layer if BASEMAP: # plot MODIS mosaic of Antarctica plot_image_mosaic(ax1, base_dir) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -403,93 +507,137 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave=np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(lon,lat,data,[ave],colors='blue',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2_subbasin'): + elif BASIN_TYPE == 'IMBIE-2_subbasin': plot_IMBIE2_subbasins(ax1, base_dir) start_indice = 1 else: @@ -500,26 +648,38 @@ def plot_grid(base_dir, FILENAMES, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) # add title for each subplot if TITLES is not None: TITLE = ' '.join(TITLES[i].split('_')) - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=14) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=14) ax1.title.set_y(1.00) # Add figure label for each subplot if LABELS is not None: - at = offsetbox.AnchoredText(LABELS[i], - loc=2, pad=0, borderpad=0.25, frameon=True, - prop=dict(size=18,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.1") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABELS[i], + loc=2, + pad=0, + borderpad=0.25, + frameon=True, + prop=dict(size=18, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.1') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # x and y limits, axis = equal @@ -536,15 +696,16 @@ def plot_grid(base_dir, FILENAMES, # draw map scale to corners of axis if DRAW_SCALE: - add_plot_scale(ax[0],-292e4,-215e4,1600e3,140e3,False) + add_plot_scale(ax[0], -292e4, -215e4, 1600e3, 140e3, False) # Add colorbar # Add an axes at position rect [left, bottom, width, height] cbar_ax = fig.add_axes([0.905, 0.045, 0.025, 0.875]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False) + cbar = fig.colorbar( + im, cax=cbar_ax, extend=CBEXTEND, extendfrac=0.0375, drawedges=False + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -554,163 +715,270 @@ def plot_grid(base_dir, FILENAMES, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=19, labelsize=14, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=19, labelsize=14, direction='in' + ) # adjust subplots within figure - fig.subplots_adjust(left=0.01,right=0.89,bottom=0.01,top=0.96,wspace=0.05) + fig.subplots_adjust( + left=0.01, right=0.89, bottom=0.01, top=0.96, wspace=0.05 + ) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates 3 GMT-like plots of the Antarctic ice sheet on a polar stereographic south (EPSG 3031) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', nargs=3, - type=pathlib.Path, - help='Input grid files') + parser.add_argument( + 'infile', nargs=3, type=pathlib.Path, help='Input grid files' + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, nargs='+', - default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + nargs='+', + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', nargs=3, - type=str, help='Plot title') - parser.add_argument('--plot-label', nargs=3, - type=str, help='Plot label') + parser.add_argument('--plot-title', nargs=3, type=str, help='Plot title') + parser.add_argument('--plot-label', nargs=3, type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -720,7 +988,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -749,7 +1019,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -757,6 +1028,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_AIS_grid_4maps.py b/mapping/plot_AIS_grid_4maps.py index a890d684..ea9cbff6 100644 --- a/mapping/plot_AIS_grid_4maps.py +++ b/mapping/plot_AIS_grid_4maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_grid_4maps.py Written by Tyler Sutterley (05/2023) Creates 3 GMT-like plots of the Antarctic Ice Sheet @@ -47,6 +47,7 @@ Updated 09/2019: added parameter for specifying if netCDF4 or HDF5 Written 09/2019 """ + from __future__ import print_function import sys @@ -64,7 +65,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -72,56 +73,104 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # Antarctica (AIS) # x and y limit (modified from Bamber 1km DEM) -xlimits = np.array([-3100000,3100000]) -ylimits = np.array([-2600000,2600000]) +xlimits = np.array([-3100000, 3100000]) +ylimits = np.array([-2600000, 2600000]) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -131,6 +180,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -140,9 +190,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -154,7 +206,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -162,30 +214,34 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -194,12 +250,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, MASKED=True): @@ -215,8 +271,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -224,40 +280,77 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # image extents - extents=(xmin,xmax,ymin,ymax) + extents = (xmin, xmax, ymin, ymax) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', extent=extents, - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + extent=extents, + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.2*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.2 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=10, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=10, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=10, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=10, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -288,11 +381,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -308,13 +401,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -323,21 +417,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -345,54 +439,69 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup polar stereographic maps - fig, ((ax[0],ax[1]),(ax[2],ax[3])) = plt.subplots(num=1, nrows=2, ncols=2, - figsize=(6,6.3), subplot_kw=dict(projection=projection)) + fig, ((ax[0], ax[1]), (ax[2], ax[3])) = plt.subplots( + num=1, + nrows=2, + ncols=2, + figsize=(6, 6.3), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) - for i,ax1 in ax.items(): - # plot image of MODIS mosaic of Antarctica as base layer + for i, ax1 in ax.items(): + # plot image of MODIS mosaic of Antarctica as base layer if BASEMAP: # plot MODIS mosaic of Antarctica plot_image_mosaic(ax1, base_dir) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -403,93 +512,137 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave = np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(lon,lat,data,[ave],colors='blue',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2_subbasin'): + elif BASIN_TYPE == 'IMBIE-2_subbasin': plot_IMBIE2_subbasins(ax1, base_dir) start_indice = 1 else: @@ -500,26 +653,38 @@ def plot_grid(base_dir, FILENAMES, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) # add title for each subplot if TITLES is not None: TITLE = ' '.join(TITLES[i].split('_')) - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=14) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=14) ax1.title.set_y(0.995) # Add figure label for each subplot if LABELS is not None: - at = offsetbox.AnchoredText(LABELS[i], - loc=2, pad=0, borderpad=0.25, frameon=True, - prop=dict(size=18,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.1") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABELS[i], + loc=2, + pad=0, + borderpad=0.25, + frameon=True, + prop=dict(size=18, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.1') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # x and y limits, axis = equal @@ -536,15 +701,21 @@ def plot_grid(base_dir, FILENAMES, # draw map scale to corners of axis if DRAW_SCALE: - add_plot_scale(ax[2],-292e4,-215e4,1600e3,140e3,False) + add_plot_scale(ax[2], -292e4, -215e4, 1600e3, 140e3, False) # Add colorbar # Add an axes at position rect [left, bottom, width, height] cbar_ax = fig.add_axes([0.085, 0.095, 0.83, 0.035]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False, orientation='horizontal') + cbar = fig.colorbar( + im, + cax=cbar_ax, + extend=CBEXTEND, + extendfrac=0.0375, + drawedges=False, + orientation='horizontal', + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -555,164 +726,270 @@ def plot_grid(base_dir, FILENAMES, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=16, labelsize=14, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=16, labelsize=14, direction='in' + ) # adjust subplots within figure - fig.subplots_adjust(left=0.01, right=0.99, bottom=0.14, top=0.97, - hspace=0.05, wspace=0.05) + fig.subplots_adjust( + left=0.01, right=0.99, bottom=0.14, top=0.97, hspace=0.05, wspace=0.05 + ) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates 4 GMT-like plots of the Antarctic ice sheet on a polar stereographic south (EPSG 3031) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', nargs=4, - type=pathlib.Path, - help='Input grid files') + parser.add_argument( + 'infile', nargs=4, type=pathlib.Path, help='Input grid files' + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, nargs='+', - default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + nargs='+', + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', nargs=4, - type=str, help='Plot title') - parser.add_argument('--plot-label', nargs=4, - type=str, help='Plot label') + parser.add_argument('--plot-title', nargs=4, type=str, help='Plot title') + parser.add_argument('--plot-label', nargs=4, type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -722,7 +999,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -751,7 +1030,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -759,6 +1039,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_AIS_grid_maps.py b/mapping/plot_AIS_grid_maps.py index 3902f360..3d8c6c2c 100644 --- a/mapping/plot_AIS_grid_maps.py +++ b/mapping/plot_AIS_grid_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_grid_maps.py Written by Tyler Sutterley (05/2023) Creates GMT-like plots for the Antarctic Ice Sheet @@ -60,6 +60,7 @@ updates to parallel new plot_AIS_grid_movie.py code Written 12/2014 """ + from __future__ import print_function import sys @@ -77,7 +78,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -85,56 +86,104 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # Antarctica (AIS) # x and y limit (modified from Bamber 1km DEM) -xlimits = np.array([-3100000,3100000]) -ylimits = np.array([-2600000,2600000]) +xlimits = np.array([-3100000, 3100000]) +ylimits = np.array([-2600000, 2600000]) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -144,6 +193,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -153,9 +203,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -168,7 +220,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -176,31 +228,35 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -209,12 +265,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, MASKED=True): @@ -230,8 +286,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -239,40 +295,77 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # image extents - extents=(xmin,xmax,ymin,ymax) + extents = (xmin, xmax, ymin, ymax) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', extent=extents, - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + extent=extents, + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.2*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.2 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAME, +def plot_grid( + base_dir, + FILENAME, DATAFORM=None, VARIABLES=[], MASK=None, @@ -302,8 +395,8 @@ def plot_grid(base_dir, FILENAME, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -318,13 +411,14 @@ def plot_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -333,71 +427,85 @@ def plot_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAME, date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAME, + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAME, date=False, - field_mapping=field_mapping) - elif (DATAFORM == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAME, date=False, field_mapping=field_mapping + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAME, date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAME, date=False, field_mapping=field_mapping + ) # create masked array if missing values if MASK is not None: # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # setup stereographic map - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(10,7.5), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(10, 7.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # plot image of MODIS mosaic of Antarctica as base layer if BASEMAP: @@ -405,79 +513,133 @@ def plot_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin, latsin, - data=img, order=order, iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2',linestyles='solid', - transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave = np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(lon,lat,data,[ave],colors='blue',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2_subbasin'): + elif BASIN_TYPE == 'IMBIE-2_subbasin': plot_IMBIE2_subbasins(ax1, base_dir) start_indice = 1 else: @@ -488,11 +650,18 @@ def plot_grid(base_dir, FILENAME, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -502,8 +671,16 @@ def plot_grid(base_dir, FILENAME, # options: neither, both, min, max # shrink = percent size of colorbar # aspect = lengthXwidth aspect of colorbar - cbar = plt.colorbar(im, ax=ax1, pad=0.025, extend=CBEXTEND, - extendfrac=0.0375, shrink=0.925, aspect=20, drawedges=False) + cbar = plt.colorbar( + im, + ax=ax1, + pad=0.025, + extend=CBEXTEND, + extendfrac=0.0375, + shrink=0.925, + aspect=20, + drawedges=False, + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -513,8 +690,9 @@ def plot_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=23, labelsize=24, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=23, labelsize=24, direction='in' + ) # x and y limits, axis = equal ax1.set_xlim(xlimits) @@ -526,182 +704,292 @@ def plot_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=24) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=24) # Add figure label if LABEL is not None: if BASEMAP: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=24,weight='bold')) - at.patch.set_boxstyle("Square,pad=0.25") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=24, weight='bold'), + ) + at.patch.set_boxstyle('Square,pad=0.25') + at.patch.set_edgecolor('white') else: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=False, - prop=dict(size=24,weight='bold')) + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=False, + prop=dict(size=24, weight='bold'), + ) ax1.axes.add_artist(at) # draw map scale to corners if DRAW_SCALE: - add_plot_scale(ax1,-295e4,-236e4,1000e3,85e3,False) + add_plot_scale(ax1, -295e4, -236e4, 1000e3, 85e3, False) # stronger linewidth on frame ax1.spines['geo'].set_linewidth(2.0) ax1.spines['geo'].set_zorder(10) ax1.spines['geo'].set_capstyle('projecting') # adjust subplot within figure - fig.subplots_adjust(left=0.02,right=0.98,bottom=0.01,top=0.97) + fig.subplots_adjust(left=0.02, right=0.98, bottom=0.01, top=0.97) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates GMT-like plots of the Antarctic ice sheet on a polar stereographic south (EPSG 3031) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', - type=pathlib.Path, - help='Input grid file') + parser.add_argument('infile', type=pathlib.Path, help='Input grid file') # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', - type=str, help='Plot title') - parser.add_argument('--plot-label', - type=str, help='Plot label') + parser.add_argument('--plot-title', type=str, help='Plot title') + parser.add_argument('--plot-label', type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -711,7 +999,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -740,7 +1030,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -748,6 +1039,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_AIS_grid_movie.py b/mapping/plot_AIS_grid_movie.py index d040071f..b11409b4 100644 --- a/mapping/plot_AIS_grid_movie.py +++ b/mapping/plot_AIS_grid_movie.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_grid_movie.py Written by Tyler Sutterley (05/2023) Creates GMT-like animations for the Antarctic Ice Sheet @@ -59,6 +59,7 @@ Updated 11/2015: different date label colors if plotting with MODIS Written 05/2015 """ + from __future__ import print_function import sys @@ -77,7 +78,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -86,61 +87,109 @@ import matplotlib.ticker as ticker import matplotlib.animation as animation import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # output file information suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # output units -unit_list = ['cmwe', 'mmGH', 'mmCU', u'\\u03BCGal', 'mbar'] +unit_list = ['cmwe', 'mmGH', 'mmCU', '\\u03BCGal', 'mbar'] # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # Antarctica (AIS) # x and y limit (modified from Bamber 1km DEM) -xlimits = np.array([-3100000,3100000]) -ylimits = np.array([-2600000,2600000]) +xlimits = np.array([-3100000, 3100000]) +ylimits = np.array([-2600000, 2600000]) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -160,9 +209,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -174,7 +225,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -182,30 +233,34 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -214,12 +269,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, MASKED=True): @@ -235,8 +290,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -244,40 +299,77 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # image extents - extents=(xmin,xmax,ymin,ymax) + extents = (xmin, xmax, ymin, ymax) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', extent=extents, - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + extent=extents, + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.2*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.2 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # animate grid program -def animate_grid(base_dir, FILENAME, +def animate_grid( + base_dir, + FILENAME, DATAFORM=None, MASK=None, INTERPOLATION=None, @@ -306,8 +398,8 @@ def animate_grid(base_dir, FILENAME, DRAW_SCALE=False, FIGURE_FILE=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -322,13 +414,14 @@ def animate_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -337,21 +430,21 @@ def animate_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file @@ -360,15 +453,25 @@ def animate_grid(base_dir, FILENAME, # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - dinput = gravtk.spatial().from_file(FILENAME, - format=DATAFORM, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + dinput = gravtk.spatial().from_file( + FILENAME, + format=DATAFORM, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) elif DATAFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,dataform = DATAFORM.split('-') - dinput = gravtk.spatial().from_index(FILENAME, - format=dataform, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + _, dataform = DATAFORM.split('-') + dinput = gravtk.spatial().from_index( + FILENAME, + format=dataform, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) # replace invalid with a new fill value dinput.replace_invalid(fill_value=FILL_VALUE) @@ -377,41 +480,51 @@ def animate_grid(base_dir, FILENAME, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # create movie writer objects FFMpegWriter = animation.writers['ffmpeg'] - metadata = dict(title=pathlib.Path(sys.argv[0]).name, artist='Matplotlib', - date_created=time.strftime('%Y-%m-%d',time.localtime())) + metadata = dict( + title=pathlib.Path(sys.argv[0]).name, + artist='Matplotlib', + date_created=time.strftime('%Y-%m-%d', time.localtime()), + ) # bitrate to be determined automatically by underlying utility - writer = FFMpegWriter(fps=8, metadata=metadata, bitrate=-1, - extra_args=['-vcodec','libx264']) + writer = FFMpegWriter( + fps=8, metadata=metadata, bitrate=-1, extra_args=['-vcodec', 'libx264'] + ) # setup stereographic map - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(10,7.5), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(10, 7.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # plot image of MODIS mosaic of Antarctica as base layer if BASEMAP: @@ -419,44 +532,55 @@ def animate_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # only plot grounded points if MASK is not None: - mask = gravtk.tools.mask_oceans(lonsin,latsin,order=order) + mask = gravtk.tools.mask_oceans(lonsin, latsin, order=order) # add place holder for figure image - im = ax1.imshow(np.zeros((my,mx)), interpolation='nearest', cmap=cmap, - norm=norm, extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - alpha=ALPHA, origin='lower', transform=projection, animated=True) + im = ax1.imshow( + np.zeros((my, mx)), + interpolation='nearest', + cmap=cmap, + norm=norm, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + alpha=ALPHA, + origin='lower', + transform=projection, + animated=True, + ) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2_subbasin'): + elif BASIN_TYPE == 'IMBIE-2_subbasin': plot_IMBIE2_subbasins(ax1, base_dir) start_indice = 1 else: @@ -466,11 +590,18 @@ def animate_grid(base_dir, FILENAME, if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -480,8 +611,16 @@ def animate_grid(base_dir, FILENAME, # options: neither, both, min, max # shrink = percent size of colorbar # aspect = lengthXwidth aspect of colorbar - cbar = plt.colorbar(im, ax=ax1, pad=0.025, extend=CBEXTEND, - extendfrac=0.0375, shrink=0.925, aspect=20, drawedges=False) + cbar = plt.colorbar( + im, + ax=ax1, + pad=0.025, + extend=CBEXTEND, + extendfrac=0.0375, + shrink=0.925, + aspect=20, + drawedges=False, + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -491,8 +630,9 @@ def animate_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=23, labelsize=24, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=23, labelsize=24, direction='in' + ) # x and y limits, axis = equal ax1.set_xlim(xlimits) @@ -504,38 +644,55 @@ def animate_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=24) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=24) # Add figure label if LABEL is not None: if BASEMAP: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=24,weight='bold')) - at.patch.set_boxstyle("Square,pad=0.25") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=24, weight='bold'), + ) + at.patch.set_boxstyle('Square,pad=0.25') + at.patch.set_edgecolor('white') else: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=False, - prop=dict(size=24,weight='bold')) + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=False, + prop=dict(size=24, weight='bold'), + ) ax1.axes.add_artist(at) # draw map scale to corners if DRAW_SCALE: - add_plot_scale(ax1,-295e4,-236e4,1000e3,85e3,False) + add_plot_scale(ax1, -295e4, -236e4, 1000e3, 85e3, False) # add date label (year-calendar month e.g. 2002-01) # if plotting with a background mosaic: use a white time label # else: use a black time label text_color = 'w' if BASEMAP else 'k' - time_text = ax1.text(0.025, 0.025, '', transform=ax1.transAxes, - color=text_color, size=30, ha='left', va='baseline', usetex=True) + time_text = ax1.text( + 0.025, + 0.025, + '', + transform=ax1.transAxes, + color=text_color, + size=30, + ha='left', + va='baseline', + usetex=True, + ) # stronger linewidth on frame ax1.spines['geo'].set_linewidth(2.0) ax1.spines['geo'].set_zorder(10) ax1.spines['geo'].set_capstyle('projecting') # adjust subplot within figure - fig.subplots_adjust(left=0.02,right=0.98,bottom=0.02,top=0.98) + fig.subplots_adjust(left=0.02, right=0.98, bottom=0.02, top=0.98) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) @@ -543,23 +700,45 @@ def animate_grid(base_dir, FILENAME, # create image for each frame with writer.saving(fig, FIGURE_FILE, FIGURE_DPI): # for each input file - for t,gm in enumerate(dinput.month): + for t, gm in enumerate(dinput.month): # data for time t converted to a masked array data = dinput.subset(gm).to_masked_array() # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,data.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,data.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(data.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(data.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and ( + np.max(dinput.lon) > 180 + ): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, data.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, data.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + data.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + data.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # only plot grounded points @@ -574,28 +753,61 @@ def animate_grid(base_dir, FILENAME, contours = [] if CONTOURS and (np.sum(data**2) > 0): # plot line contours - contours.append(ax1.contour(lon, lat, data, reduce_clevs, - colors='0.2', linestyles='solid', - transform=ccrs.PlateCarree())) - contours.append(ax1.contour(lon, lat, data, 0, - colors='red', linestyles='solid', linewidths=1.5, - transform=ccrs.PlateCarree())) + contours.append( + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ) + contours.append( + ax1.contour( + lon, + lat, + data, + 0, + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = ( + (rad_e**2) + * dth + * dphi + * np.cos(np.radians(lat[indy, indx])) + ) # calculate average - ave = np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - contours.append(ax1.contour(lon, lat, data, [ave], - colors='blue', linestyles='solid', linewidths=1.5, - transform=ccrs.PlateCarree())) + contours.append( + ax1.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) + ) # add date label (year-calendar month e.g. 2002-01) year = np.floor(dinput.time[t]).astype(np.int64) - calendar_month = np.int64(((gm-1) % 12)+1) - date_label=r'\textbf{{{0:4d}--{1:02d}}}'.format(year,calendar_month) + calendar_month = np.int64(((gm - 1) % 12) + 1) + date_label = r'\textbf{{{0:4d}--{1:02d}}}'.format( + year, calendar_month + ) time_text.set_text(date_label) # add to movie writer.grab_frame() @@ -604,139 +816,228 @@ def animate_grid(base_dir, FILENAME, # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates GMT-like animations of the Antarctic Ice Sheet on a polar stereographic south (EPSG 3031) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', - type=pathlib.Path, - help='Input grid file') + parser.add_argument('infile', type=pathlib.Path, help='Input grid file') # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', - type=str, help='Plot title') - parser.add_argument('--plot-label', - type=str, help='Plot label') + parser.add_argument('--plot-title', type=str, help='Plot title') + parser.add_argument('--plot-label', type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -746,7 +1047,9 @@ def main(): try: info(args) # run plot program with parameters - animate_grid(args.directory, args.infile, + animate_grid( + args.directory, + args.infile, DATAFORM=args.format, DDEG=args.spacing, INTERVAL=args.interval, @@ -773,7 +1076,8 @@ def main(): DRAW_SCALE=args.draw_scale, FIGURE_FILE=args.figure_file, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -781,6 +1085,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_AIS_regional_maps.py b/mapping/plot_AIS_regional_maps.py index ac5c7b1b..d44c340b 100644 --- a/mapping/plot_AIS_regional_maps.py +++ b/mapping/plot_AIS_regional_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_regional_maps.py Written by Tyler Sutterley (05/2023) Creates GMT-like plots for sub-regions of Antarctica @@ -64,6 +64,7 @@ updates to parallel new plot_AIS_grid_movie.py code Written 07/2014 """ + from __future__ import print_function import sys @@ -81,7 +82,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -89,44 +90,90 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa125_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa125_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # regional plot parameters # figure size @@ -147,56 +194,58 @@ region_sub_adjust = {} # Amundsen Sea Embayment (ASE) -region_figsize['ASE'] = (10,9.5) +region_figsize['ASE'] = (10, 9.5) region_fontsize['ASE'] = 24 region_labelsize['ASE'] = 24 region_xlimit['ASE'] = np.array([-1900000, -900000]) -region_ylimit['ASE'] = np.array([-900000, 200000]) +region_ylimit['ASE'] = np.array([-900000, 200000]) region_cb_axis['ASE'] = [0.865, 0.015, 0.035, 0.94] region_cb_length['ASE'] = 26 -region_plotscale['ASE'] = (-1870e3,-845e3,200e3,25010,False) +region_plotscale['ASE'] = (-1870e3, -845e3, 200e3, 25010, False) region_sub_adjust['ASE'] = dict(left=0.01, right=0.855, bottom=0.01, top=0.96) # Antarctic Peninsula (IIpp) -region_figsize['IIpp'] = (10,10) +region_figsize['IIpp'] = (10, 10) region_fontsize['IIpp'] = 24 region_labelsize['IIpp'] = 24 -region_xlimit['IIpp'] = np.array([-2710000,-1830000]) -region_ylimit['IIpp'] = np.array([760000,1780000]) +region_xlimit['IIpp'] = np.array([-2710000, -1830000]) +region_ylimit['IIpp'] = np.array([760000, 1780000]) region_cb_axis['IIpp'] = [0.87, 0.015, 0.0325, 0.94] region_cb_length['IIpp'] = 23 -region_plotscale['IIpp'] = (-2685e3,809e3,200e3,25010,False) +region_plotscale['IIpp'] = (-2685e3, 809e3, 200e3, 25010, False) region_sub_adjust['IIpp'] = dict(left=0.01, right=0.86, bottom=0.01, top=0.96) # Totten/Moscow/Frost (CpD) -region_figsize['CpD'] = (9.5,10) +region_figsize['CpD'] = (9.5, 10) region_fontsize['CpD'] = 24 region_labelsize['CpD'] = 24 -region_xlimit['CpD'] = np.array([1200000,2650000]) -region_ylimit['CpD'] = np.array([-1800000,0]) +region_xlimit['CpD'] = np.array([1200000, 2650000]) +region_ylimit['CpD'] = np.array([-1800000, 0]) region_cb_axis['CpD'] = [0.855, 0.015, 0.04, 0.94] region_cb_length['CpD'] = 27 -region_plotscale['CpD'] = (1230e3,-1740e3,200e3,28420,False) +region_plotscale['CpD'] = (1230e3, -1740e3, 200e3, 28420, False) region_sub_adjust['CpD'] = dict(left=0.01, right=0.84, bottom=0.01, top=0.96) # Queen Maud Land (QML) -region_figsize['QML'] = (9.5,4.625) +region_figsize['QML'] = (9.5, 4.625) region_fontsize['QML'] = 20 region_labelsize['QML'] = 24 -region_xlimit['QML'] = np.array([-940000,2400000]) -region_ylimit['QML'] = np.array([530000,2300000]) +region_xlimit['QML'] = np.array([-940000, 2400000]) +region_ylimit['QML'] = np.array([530000, 2300000]) region_cb_axis['QML'] = [0.87, 0.03, 0.03, 0.90] region_cb_length['QML'] = 21 -region_plotscale['QML'] = (1705e3,2125e3,600e3,50e3,False) +region_plotscale['QML'] = (1705e3, 2125e3, 600e3, 50e3, False) region_sub_adjust['QML'] = dict(left=0.01, right=0.85, bottom=0.01, top=0.95) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -206,6 +255,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -215,9 +265,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -229,7 +281,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -237,46 +289,51 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Amundsen Sea basins from Mouginot et al. (2014) def plot_amundsen_basins(ax, base_dir): # read Amundsen Sea basin polylines from shapefile - basin_shapefile = base_dir.joinpath('masks','Basins_Admunsen', - 'Basins_admunsen_match_coastline_and_IS.shp') - basin_title = ['pope_smith','haynes','thwaites','pine_island','kohler'] + basin_shapefile = base_dir.joinpath( + 'masks', 'Basins_Admunsen', 'Basins_admunsen_match_coastline_and_IS.shp' + ) + basin_title = ['pope_smith', 'haynes', 'thwaites', 'pine_island', 'kohler'] logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() # for each shape entity - for i,ent in enumerate(shape_entities): + for i, ent in enumerate(shape_entities): # extract lat/lon coordinates for record points = np.array(ent.points) - ax.plot(points[:,0], points[:,1], c='k', - transform=ccrs.PlateCarree()) + ax.plot(points[:, 0], points[:, 1], c='k', transform=ccrs.PlateCarree()) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -285,12 +342,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, xlimits, ylimits, MASKED=True): @@ -302,55 +359,97 @@ def plot_image_mosaic(ax, base_dir, xlimits, ylimits, MASKED=True): info_geotiff = ds.GetGeoTransform() # reduce input image with GDAL # Specify offset and rows and columns to read - xoff = int((xlimits[0] - info_geotiff[0])/info_geotiff[1]) - yoff = int((ylimits[1] - info_geotiff[3])/info_geotiff[5]) - xsize = int((xlimits[1] - xlimits[0])/info_geotiff[1]) + 1 - ysize = int((ylimits[0] - ylimits[1])/info_geotiff[5]) + 1 + xoff = int((xlimits[0] - info_geotiff[0]) / info_geotiff[1]) + yoff = int((ylimits[1] - info_geotiff[3]) / info_geotiff[5]) + xsize = int((xlimits[1] - xlimits[0]) / info_geotiff[1]) + 1 + ysize = int((ylimits[0] - ylimits[1]) / info_geotiff[5]) + 1 # read as grayscale image reducing to xlimit and ylimit - mosaic = np.ma.array(ds.ReadAsArray(xoff=xoff, yoff=yoff, - xsize=xsize, ysize=ysize)) + mosaic = np.ma.array( + ds.ReadAsArray(xoff=xoff, yoff=yoff, xsize=xsize, ysize=ysize) + ) # mask image mosaic if MASKED: # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # reduced x and y limits of image - xmin = info_geotiff[0] + xoff*info_geotiff[1] - xmax = info_geotiff[0] + xoff*info_geotiff[1] + (xsize-1)*info_geotiff[1] - ymax = info_geotiff[3] + yoff*info_geotiff[5] - ymin = info_geotiff[3] + yoff*info_geotiff[5] + (ysize-1)*info_geotiff[5] + xmin = info_geotiff[0] + xoff * info_geotiff[1] + xmax = ( + info_geotiff[0] + xoff * info_geotiff[1] + (xsize - 1) * info_geotiff[1] + ) + ymax = info_geotiff[3] + yoff * info_geotiff[5] + ymin = ( + info_geotiff[3] + yoff * info_geotiff[5] + (ysize - 1) * info_geotiff[5] + ) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.15*dx,Y-1.8*dy,Y+2.4*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.15 * dx, + Y - 1.8 * dy, + Y + 2.4 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAME, +def plot_grid( + base_dir, + FILENAME, REGION=None, DATAFORM=None, VARIABLES=[], @@ -381,8 +480,8 @@ def plot_grid(base_dir, FILENAME, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -397,13 +496,14 @@ def plot_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -412,72 +512,85 @@ def plot_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAME, date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAME, + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAME, date=False, - field_mapping=field_mapping) - elif (DATAFORM == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAME, date=False, field_mapping=field_mapping + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAME, date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAME, date=False, field_mapping=field_mapping + ) # create masked array if missing values if MASK is not None: # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # setup stereographic map - fig,ax1 = plt.subplots(num=1, nrows=1, ncols=1, + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, figsize=region_figsize[REGION], - subplot_kw=dict(projection=projection)) + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # region x and y limits xlimits = region_xlimit[REGION] ylimits = region_ylimit[REGION] @@ -488,82 +601,136 @@ def plot_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir, xlimits, ylimits) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin, latsin, - data=img, order=order, iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2',linestyles='solid', - transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave = np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(lon,lat,data,[ave],colors='blue',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2_subbasin'): + elif BASIN_TYPE == 'IMBIE-2_subbasin': plot_IMBIE2_subbasins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'Amundsen'): + elif BASIN_TYPE == 'Amundsen': plot_amundsen_basins(ax1, base_dir) start_indice = 0 else: @@ -574,11 +741,18 @@ def plot_grid(base_dir, FILENAME, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -587,8 +761,9 @@ def plot_grid(base_dir, FILENAME, cbar_ax = fig.add_axes(region_cb_axis[REGION]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False) + cbar = fig.colorbar( + im, cax=cbar_ax, extend=CBEXTEND, extendfrac=0.0375, drawedges=False + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -598,9 +773,13 @@ def plot_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, - length=region_cb_length[REGION], labelsize=region_fontsize[REGION], - direction='in') + cbar.ax.tick_params( + which='both', + width=1, + length=region_cb_length[REGION], + labelsize=region_fontsize[REGION], + direction='in', + ) # x and y limits, axis = equal ax1.set_xlim(xlimits) @@ -612,20 +791,29 @@ def plot_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), - fontsize=region_fontsize[REGION]) + ax1.set_title( + TITLE.replace('-', '\u2013'), fontsize=region_fontsize[REGION] + ) # Add figure label if LABEL is not None: if BASEMAP: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=region_labelsize[REGION], weight='bold')) - at.patch.set_boxstyle("Square,pad=0.25") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=region_labelsize[REGION], weight='bold'), + ) + at.patch.set_boxstyle('Square,pad=0.25') + at.patch.set_edgecolor('white') else: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=False, - prop=dict(size=region_labelsize[REGION], weight='bold')) + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=False, + prop=dict(size=region_labelsize[REGION], weight='bold'), + ) ax1.axes.add_artist(at) # draw map scale to corners @@ -643,157 +831,265 @@ def plot_grid(base_dir, FILENAME, FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates GMT-like plots of sub-regions of Antarctica on a polar stereographic south (EPSG 3031) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', - type=pathlib.Path, - help='Input grid file') + parser.add_argument('infile', type=pathlib.Path, help='Input grid file') # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # plot regions - parser.add_argument('--region','-r', - metavar='REGION', type=str, choices=sorted(region_figsize.keys()), - required=True, help='Region to plot') + parser.add_argument( + '--region', + '-r', + metavar='REGION', + type=str, + choices=sorted(region_figsize.keys()), + required=True, + help='Region to plot', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', - type=str, help='Plot title') - parser.add_argument('--plot-label', - type=str, help='Plot label') + parser.add_argument('--plot-title', type=str, help='Plot title') + parser.add_argument('--plot-label', type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -803,7 +1099,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, REGION=args.region, DATAFORM=args.format, VARIABLES=args.variables, @@ -833,7 +1131,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -841,6 +1140,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_AIS_regional_movie.py b/mapping/plot_AIS_regional_movie.py index ec5101a2..babbf1a4 100644 --- a/mapping/plot_AIS_regional_movie.py +++ b/mapping/plot_AIS_regional_movie.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_ASE_grid_movie.py Written by Tyler Sutterley (05/2023) Creates GMT-like animations for sub-regions of Antarctica @@ -62,6 +62,7 @@ updates to parallel new plot_AIS_grid_movie.py code Written 11/2014 """ + from __future__ import print_function import sys @@ -80,7 +81,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -89,49 +90,95 @@ import matplotlib.ticker as ticker import matplotlib.animation as animation import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # output file information suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # output units -unit_list = ['cmwe', 'mmGH', 'mmCU', u'\\u03BCGal', 'mbar'] +unit_list = ['cmwe', 'mmGH', 'mmCU', '\\u03BCGal', 'mbar'] # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa125_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa125_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # regional plot parameters # figure size @@ -152,56 +199,58 @@ region_sub_adjust = {} # Amundsen Sea Embayment (ASE) -region_figsize['ASE'] = (10,9.5) +region_figsize['ASE'] = (10, 9.5) region_fontsize['ASE'] = 24 region_labelsize['ASE'] = 24 region_xlimit['ASE'] = np.array([-1900000, -900000]) -region_ylimit['ASE'] = np.array([-900000, 200000]) +region_ylimit['ASE'] = np.array([-900000, 200000]) region_cb_axis['ASE'] = [0.865, 0.015, 0.035, 0.94] region_cb_length['ASE'] = 26 -region_plotscale['ASE'] = (-1870e3,-845e3,200e3,25010,False) +region_plotscale['ASE'] = (-1870e3, -845e3, 200e3, 25010, False) region_sub_adjust['ASE'] = dict(left=0.01, right=0.855, bottom=0.01, top=0.96) # Antarctic Peninsula (IIpp) -region_figsize['IIpp'] = (10,10) +region_figsize['IIpp'] = (10, 10) region_fontsize['IIpp'] = 24 region_labelsize['IIpp'] = 24 -region_xlimit['IIpp'] = np.array([-2710000,-1830000]) -region_ylimit['IIpp'] = np.array([760000,1780000]) +region_xlimit['IIpp'] = np.array([-2710000, -1830000]) +region_ylimit['IIpp'] = np.array([760000, 1780000]) region_cb_axis['IIpp'] = [0.87, 0.015, 0.0325, 0.94] region_cb_length['IIpp'] = 23 -region_plotscale['IIpp'] = (-2685e3,809e3,200e3,25010,False) +region_plotscale['IIpp'] = (-2685e3, 809e3, 200e3, 25010, False) region_sub_adjust['IIpp'] = dict(left=0.01, right=0.86, bottom=0.01, top=0.96) # Totten/Moscow/Frost (CpD) -region_figsize['CpD'] = (9.5,10) +region_figsize['CpD'] = (9.5, 10) region_fontsize['CpD'] = 24 region_labelsize['CpD'] = 24 -region_xlimit['CpD'] = np.array([1200000,2650000]) -region_ylimit['CpD'] = np.array([-1800000,0]) +region_xlimit['CpD'] = np.array([1200000, 2650000]) +region_ylimit['CpD'] = np.array([-1800000, 0]) region_cb_axis['CpD'] = [0.855, 0.015, 0.04, 0.94] region_cb_length['CpD'] = 27 -region_plotscale['CpD'] = (1230e3,-1740e3,200e3,28420,False) +region_plotscale['CpD'] = (1230e3, -1740e3, 200e3, 28420, False) region_sub_adjust['CpD'] = dict(left=0.01, right=0.84, bottom=0.01, top=0.96) # Queen Maud Land (QML) -region_figsize['QML'] = (9.5,4.625) +region_figsize['QML'] = (9.5, 4.625) region_fontsize['QML'] = 20 region_labelsize['QML'] = 24 -region_xlimit['QML'] = np.array([-940000,2400000]) -region_ylimit['QML'] = np.array([530000,2300000]) +region_xlimit['QML'] = np.array([-940000, 2400000]) +region_ylimit['QML'] = np.array([530000, 2300000]) region_cb_axis['QML'] = [0.87, 0.03, 0.03, 0.90] region_cb_length['QML'] = 21 -region_plotscale['QML'] = (1705e3,2125e3,600e3,50e3,False) +region_plotscale['QML'] = (1705e3, 2125e3, 600e3, 50e3, False) region_sub_adjust['QML'] = dict(left=0.01, right=0.85, bottom=0.01, top=0.95) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -221,9 +270,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -235,7 +286,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -243,46 +294,51 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Amundsen Sea basins from Mouginot et al. (2014) def plot_amundsen_basins(ax, base_dir): # read Amundsen Sea basin polylines from shapefile - basin_shapefile = base_dir.joinpath('masks','Basins_Admunsen', - 'Basins_admunsen_match_coastline_and_IS.shp') - basin_title = ['pope_smith','haynes','thwaites','pine_island','kohler'] + basin_shapefile = base_dir.joinpath( + 'masks', 'Basins_Admunsen', 'Basins_admunsen_match_coastline_and_IS.shp' + ) + basin_title = ['pope_smith', 'haynes', 'thwaites', 'pine_island', 'kohler'] logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() # for each shape entity - for i,ent in enumerate(shape_entities): + for i, ent in enumerate(shape_entities): # extract lat/lon coordinates for record points = np.array(ent.points) - ax.plot(points[:,0], points[:,1], c='k', - transform=ccrs.PlateCarree()) + ax.plot(points[:, 0], points[:, 1], c='k', transform=ccrs.PlateCarree()) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -291,12 +347,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, xlimits, ylimits, MASKED=True): @@ -308,55 +364,97 @@ def plot_image_mosaic(ax, base_dir, xlimits, ylimits, MASKED=True): info_geotiff = ds.GetGeoTransform() # reduce input image with GDAL # Specify offset and rows and columns to read - xoff = int((xlimits[0] - info_geotiff[0])/info_geotiff[1]) - yoff = int((ylimits[1] - info_geotiff[3])/info_geotiff[5]) - xsize = int((xlimits[1] - xlimits[0])/info_geotiff[1]) + 1 - ysize = int((ylimits[0] - ylimits[1])/info_geotiff[5]) + 1 + xoff = int((xlimits[0] - info_geotiff[0]) / info_geotiff[1]) + yoff = int((ylimits[1] - info_geotiff[3]) / info_geotiff[5]) + xsize = int((xlimits[1] - xlimits[0]) / info_geotiff[1]) + 1 + ysize = int((ylimits[0] - ylimits[1]) / info_geotiff[5]) + 1 # read as grayscale image reducing to xlimit and ylimit - mosaic = np.ma.array(ds.ReadAsArray(xoff=xoff, yoff=yoff, - xsize=xsize, ysize=ysize)) + mosaic = np.ma.array( + ds.ReadAsArray(xoff=xoff, yoff=yoff, xsize=xsize, ysize=ysize) + ) # mask image mosaic if MASKED: # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # reduced x and y limits of image - xmin = info_geotiff[0] + xoff*info_geotiff[1] - xmax = info_geotiff[0] + xoff*info_geotiff[1] + (xsize-1)*info_geotiff[1] - ymax = info_geotiff[3] + yoff*info_geotiff[5] - ymin = info_geotiff[3] + yoff*info_geotiff[5] + (ysize-1)*info_geotiff[5] + xmin = info_geotiff[0] + xoff * info_geotiff[1] + xmax = ( + info_geotiff[0] + xoff * info_geotiff[1] + (xsize - 1) * info_geotiff[1] + ) + ymax = info_geotiff[3] + yoff * info_geotiff[5] + ymin = ( + info_geotiff[3] + yoff * info_geotiff[5] + (ysize - 1) * info_geotiff[5] + ) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.15*dx,Y-1.8*dy,Y+2.4*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.15 * dx, + Y - 1.8 * dy, + Y + 2.4 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # animate grid program -def animate_grid(base_dir, FILENAME, +def animate_grid( + base_dir, + FILENAME, REGION=None, DATAFORM=None, MASK=None, @@ -386,8 +484,8 @@ def animate_grid(base_dir, FILENAME, DRAW_SCALE=False, FIGURE_FILE=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -402,13 +500,14 @@ def animate_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -417,21 +516,21 @@ def animate_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file @@ -440,15 +539,25 @@ def animate_grid(base_dir, FILENAME, # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - dinput = gravtk.spatial().from_file(FILENAME, - format=DATAFORM, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + dinput = gravtk.spatial().from_file( + FILENAME, + format=DATAFORM, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) elif DATAFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,dataform = DATAFORM.split('-') - dinput = gravtk.spatial().from_index(FILENAME, - format=dataform, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + _, dataform = DATAFORM.split('-') + dinput = gravtk.spatial().from_index( + FILENAME, + format=dataform, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) # replace invalid with a new fill value dinput.replace_invalid(fill_value=FILL_VALUE) @@ -457,42 +566,51 @@ def animate_grid(base_dir, FILENAME, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # create movie writer objects FFMpegWriter = animation.writers['ffmpeg'] - metadata = dict(title=pathlib.Path(sys.argv[0]).name, artist='Matplotlib', - date_created=time.strftime('%Y-%m-%d',time.localtime())) + metadata = dict( + title=pathlib.Path(sys.argv[0]).name, + artist='Matplotlib', + date_created=time.strftime('%Y-%m-%d', time.localtime()), + ) # bitrate to be determined automatically by underlying utility - writer = FFMpegWriter(fps=8, metadata=metadata, bitrate=-1, - extra_args=['-vcodec','libx264']) + writer = FFMpegWriter( + fps=8, metadata=metadata, bitrate=-1, extra_args=['-vcodec', 'libx264'] + ) # setup stereographic map - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, figsize=region_figsize[REGION], - subplot_kw=dict(projection=projection)) + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # region x and y limits xlimits = region_xlimit[REGION] ylimits = region_ylimit[REGION] @@ -503,47 +621,58 @@ def animate_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # only plot grounded points if MASK is not None: - mask = gravtk.tools.mask_oceans(lonsin,latsin,order=order) + mask = gravtk.tools.mask_oceans(lonsin, latsin, order=order) # add place holder for figure image - im = ax1.imshow(np.zeros((my,mx)), interpolation='nearest', cmap=cmap, - norm=norm, extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - alpha=ALPHA, origin='lower', transform=projection, animated=True) + im = ax1.imshow( + np.zeros((my, mx)), + interpolation='nearest', + cmap=cmap, + norm=norm, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + alpha=ALPHA, + origin='lower', + transform=projection, + animated=True, + ) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2_subbasin'): + elif BASIN_TYPE == 'IMBIE-2_subbasin': plot_IMBIE2_subbasins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'Amundsen'): + elif BASIN_TYPE == 'Amundsen': plot_amundsen_basins(ax1, base_dir) start_indice = 0 else: @@ -553,11 +682,18 @@ def animate_grid(base_dir, FILENAME, if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -566,8 +702,9 @@ def animate_grid(base_dir, FILENAME, cbar_ax = fig.add_axes(region_cb_axis[REGION]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False) + cbar = fig.colorbar( + im, cax=cbar_ax, extend=CBEXTEND, extendfrac=0.0375, drawedges=False + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -577,9 +714,13 @@ def animate_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, - length=region_cb_length[REGION], labelsize=region_fontsize[REGION], - direction='in') + cbar.ax.tick_params( + which='both', + width=1, + length=region_cb_length[REGION], + labelsize=region_fontsize[REGION], + direction='in', + ) # x and y limits, axis = equal ax1.set_xlim(xlimits) @@ -591,20 +732,29 @@ def animate_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), - fontsize=region_fontsize[REGION]) + ax1.set_title( + TITLE.replace('-', '\u2013'), fontsize=region_fontsize[REGION] + ) # Add figure label if LABEL is not None: if BASEMAP: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=region_labelsize[REGION], weight='bold')) - at.patch.set_boxstyle("Square,pad=0.25") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=region_labelsize[REGION], weight='bold'), + ) + at.patch.set_boxstyle('Square,pad=0.25') + at.patch.set_edgecolor('white') else: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=False, - prop=dict(size=region_labelsize[REGION], weight='bold')) + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=False, + prop=dict(size=region_labelsize[REGION], weight='bold'), + ) ax1.axes.add_artist(at) # draw map scale to corners @@ -615,9 +765,17 @@ def animate_grid(base_dir, FILENAME, # if plotting with a background mosaic: use a white time label # else: use a black time label text_color = 'w' if BASEMAP else 'k' - time_text = ax1.text(0.025, 0.025, '', transform=ax1.transAxes, - color=text_color, size=region_labelsize[REGION], - ha='left', va='baseline', usetex=True) + time_text = ax1.text( + 0.025, + 0.025, + '', + transform=ax1.transAxes, + color=text_color, + size=region_labelsize[REGION], + ha='left', + va='baseline', + usetex=True, + ) # stronger linewidth on frame ax1.spines['geo'].set_linewidth(2.0) @@ -632,23 +790,45 @@ def animate_grid(base_dir, FILENAME, # create image for each frame with writer.saving(fig, FIGURE_FILE, FIGURE_DPI): # for each input file - for t,gm in enumerate(dinput.month): + for t, gm in enumerate(dinput.month): # data for time t converted to a masked array data = dinput.subset(gm).to_masked_array() # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,data.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,data.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(data.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(data.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and ( + np.max(dinput.lon) > 180 + ): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, data.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, data.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + data.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + data.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # only plot grounded points @@ -663,28 +843,61 @@ def animate_grid(base_dir, FILENAME, contours = [] if CONTOURS and (np.sum(data**2) > 0): # plot line contours - contours.append(ax1.contour(lon, lat, data, reduce_clevs, - colors='0.2', linestyles='solid', - transform=ccrs.PlateCarree())) - contours.append(ax1.contour(lon, lat, data, 0, - colors='red', linestyles='solid', linewidths=1.5, - transform=ccrs.PlateCarree())) + contours.append( + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ) + contours.append( + ax1.contour( + lon, + lat, + data, + 0, + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = ( + (rad_e**2) + * dth + * dphi + * np.cos(np.radians(lat[indy, indx])) + ) # calculate average - ave = np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - contours.append(ax1.contour(lon, lat, data, [ave], - colors='blue', linestyles='solid', linewidths=1.5, - transform=ccrs.PlateCarree())) + contours.append( + ax1.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) + ) # add date label (year-calendar month e.g. 2002-01) year = np.floor(dinput.time[t]).astype(np.int64) - calendar_month = np.int64(((gm-1) % 12)+1) - date_label=r'\textbf{{{0:4d}--{1:02d}}}'.format(year,calendar_month) + calendar_month = np.int64(((gm - 1) % 12) + 1) + date_label = r'\textbf{{{0:4d}--{1:02d}}}'.format( + year, calendar_month + ) time_text.set_text(date_label) # add to movie writer.grab_frame() @@ -693,143 +906,238 @@ def animate_grid(base_dir, FILENAME, # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates GMT-like animations of sub-regions of Antarctica on a polar stereographic south (EPSG 3031) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', - type=pathlib.Path, - help='Input grid file') + parser.add_argument('infile', type=pathlib.Path, help='Input grid file') # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # plot regions - parser.add_argument('--region','-r', - metavar='REGION', type=str, choices=sorted(region_figsize.keys()), - required=True, help='Region to plot') + parser.add_argument( + '--region', + '-r', + metavar='REGION', + type=str, + choices=sorted(region_figsize.keys()), + required=True, + help='Region to plot', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', - type=str, help='Plot title') - parser.add_argument('--plot-label', - type=str, help='Plot label') + parser.add_argument('--plot-title', type=str, help='Plot title') + parser.add_argument('--plot-label', type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -839,7 +1147,9 @@ def main(): try: info(args) # run plot program with parameters - animate_grid(args.directory, args.infile, + animate_grid( + args.directory, + args.infile, REGION=args.region, DATAFORM=args.format, DDEG=args.spacing, @@ -867,7 +1177,8 @@ def main(): DRAW_SCALE=args.draw_scale, FIGURE_FILE=args.figure_file, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -875,6 +1186,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_GrIS_grid_3maps.py b/mapping/plot_GrIS_grid_3maps.py index b0d13750..1c44d2f8 100644 --- a/mapping/plot_GrIS_grid_3maps.py +++ b/mapping/plot_GrIS_grid_3maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_GrIS_grid_3maps.py Written by Tyler Sutterley (05/2023) Creates 3 GMT-like plots for the Greenland ice sheet @@ -49,6 +49,7 @@ Updated 09/2019: added parameter for specifying if netCDF4 or HDF5 Written 09/2019 """ + from __future__ import print_function import sys @@ -66,7 +67,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -74,52 +75,61 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Greenland ice divides # region directory, filename, title and data type -region_dir = ['masks','Rignot_GRE'] -region_title = ['CE','CW','NE','NO','NW','SE','SW'] +region_dir = ['masks', 'Rignot_GRE'] +region_title = ['CE', 'CW', 'NE', 'NO', 'NW', 'SE', 'SW'] # regional filenames region_filename = 'divide_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lon','lat'),'formats':('f','f')} +region_dtype = {'names': ('lon', 'lat'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','GRE_Basins_IMBIE2_v1.3','GRE_Basins_IMBIE2_v1.3.shp'] +IMBIE_basin_file = [ + 'masks', + 'GRE_Basins_IMBIE2_v1.3', + 'GRE_Basins_IMBIE2_v1.3.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('CW','NE','NO','NW','SE','SW') +IMBIE_title = ('CW', 'NE', 'NO', 'NW', 'SE', 'SW') # background image mosaics # MODIS mosaic of Greenland -image_file = ['MOG','mog500_2005_hp1_v1.1.tif'] +image_file = ['MOG', 'mog500_2005_hp1_v1.1.tif'] # Greenland grounded ice -coast_file = ['masks','GIMP','grn_ice_sheet_peripheral_glaciers.shp'] +coast_file = ['masks', 'GIMP', 'grn_ice_sheet_peripheral_glaciers.shp'] # Greenland bounds xlimits = np.array([-1530000, 1610000]) ylimits = np.array([-3600000, -280000]) # cartopy transform for NSIDC polar stereographic north try: - projection = ccrs.Stereographic(central_longitude=-45.0, - central_latitude=+90.0,true_scale_latitude=+70.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=-45.0, + central_latitude=+90.0, + true_scale_latitude=+70.0, + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -129,6 +139,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -138,9 +149,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Greenland drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -152,7 +165,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no GIC or islands - i = [i for i,a in enumerate(shape_attributes) if a[0] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[0] in IMBIE_title] # for each valid shape entity for indice in i: # extract lat/lon coordinates for record @@ -160,10 +173,15 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): + for p1, p2 in zip(parts[:-1], parts[1:]): # converting basin lat/lon into plot coordinates - ax.plot(points[p1:p2,0], points[p1:p2,1], color='k', - transform=ccrs.PlateCarree()) + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + color='k', + transform=ccrs.PlateCarree(), + ) + # PURPOSE: plot Greenland grounded ice delineation from GIMP def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): @@ -172,12 +190,18 @@ def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], c='k', lw=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + c='k', + lw=LINEWIDTH, + transform=projection, + ) + # PURPOSE: plot glaciated regions from Randolph Glacier Inventory def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): @@ -187,36 +211,43 @@ def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): RGI_files.append('06_rgi60_Iceland') RGI_files.append('07_rgi60_Svalbard') for f in RGI_files: - RGI_shapefile = base_dir.joinpath('RGI',f,f'{f}_plot.shp') + RGI_shapefile = base_dir.joinpath('RGI', f, f'{f}_plot.shp') logging.debug(str(RGI_shapefile)) shape_input = shapefile.Reader(str(RGI_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], color='k', linewidth=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + color='k', + linewidth=LINEWIDTH, + transform=projection, + ) + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, color='k', transform=ccrs.PlateCarree()) + # plot the MODIS Mosaic of Greenland as a background image def plot_image_mosaic(ax, base_dir, MASKED=True): # read MODIS mosaic of Greenland @@ -231,8 +262,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -240,38 +271,75 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # dataset range vmin, vmax = (0, 18770) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.15*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.15 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -303,12 +371,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -324,13 +391,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -339,21 +407,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -361,55 +429,68 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup polar stereographic maps - fig, (ax[0],ax[1],ax[2]) = plt.subplots(num=1, ncols=3, figsize=(10.25,3.5), - subplot_kw=dict(projection=projection)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, + ncols=3, + figsize=(10.25, 3.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # plot image of MODIS mosaic of Greenland as base layer if BASEMAP: # plot MODIS mosaic of Greenland plot_image_mosaic(ax1, base_dir) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -420,92 +501,136 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave=np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(lon,lat,data,[ave],colors='blue',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot the coastline and grounded ice files plot_coastline(ax1, base_dir) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 else: @@ -518,26 +643,38 @@ def plot_grid(base_dir, FILENAMES, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) # add title for each subplot if TITLES is not None: TITLE = ' '.join(TITLES[i].split('_')) - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=14) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=14) ax1.title.set_y(1.00) # Add figure label for each subplot if LABELS is not None: - at = offsetbox.AnchoredText(LABELS[i], - loc=2, pad=0, borderpad=0.25, frameon=True, - prop=dict(size=18,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.1") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABELS[i], + loc=2, + pad=0, + borderpad=0.25, + frameon=True, + prop=dict(size=18, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.1') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # x and y limits, axis = equal @@ -554,15 +691,16 @@ def plot_grid(base_dir, FILENAMES, # draw map scale to corners of axis if DRAW_SCALE: - add_plot_scale(ax[0],620e3,-3365e3,800e3,70e3,False) + add_plot_scale(ax[0], 620e3, -3365e3, 800e3, 70e3, False) # Add colorbar # Add an axes at position rect [left, bottom, width, height] cbar_ax = fig.add_axes([0.905, 0.055, 0.025, 0.875]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False) + cbar = fig.colorbar( + im, cax=cbar_ax, extend=CBEXTEND, extendfrac=0.0375, drawedges=False + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -572,166 +710,276 @@ def plot_grid(base_dir, FILENAMES, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=19, labelsize=14, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=19, labelsize=14, direction='in' + ) # adjust subplots within figure - fig.subplots_adjust(left=0.01,right=0.89,bottom=0.01,top=0.96,wspace=0.05) + fig.subplots_adjust( + left=0.01, right=0.89, bottom=0.01, top=0.96, wspace=0.05 + ) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates 3 GMT-like plots of the Greenland ice sheet on a NSIDC polar stereographic north (EPSG 3413) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', nargs=3, - type=pathlib.Path, - help='Input grid files') + parser.add_argument( + 'infile', nargs=3, type=pathlib.Path, help='Input grid files' + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, nargs='+', - default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + nargs='+', + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', nargs=3, - type=str, help='Plot title') - parser.add_argument('--plot-label', nargs=3, - type=str, help='Plot label') + parser.add_argument('--plot-title', nargs=3, type=str, help='Plot title') + parser.add_argument('--plot-label', nargs=3, type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--glacier-margins', - default=False, action='store_true', - help='Add delineations for glacier margins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--glacier-margins', + default=False, + action='store_true', + help='Add delineations for glacier margins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -741,7 +989,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -771,7 +1021,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -779,6 +1030,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_GrIS_grid_5maps.py b/mapping/plot_GrIS_grid_5maps.py index 55e02466..4b2a7cf3 100644 --- a/mapping/plot_GrIS_grid_5maps.py +++ b/mapping/plot_GrIS_grid_5maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_GrIS_grid_5maps.py Written by Tyler Sutterley (10/2023) Creates 5 GMT-like plots for the Greenland ice sheet @@ -36,6 +36,7 @@ UPDATE HISTORY: Written 10/2023 """ + from __future__ import print_function import sys @@ -53,7 +54,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -61,52 +62,61 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Greenland ice divides # region directory, filename, title and data type -region_dir = ['masks','Rignot_GRE'] -region_title = ['CE','CW','NE','NO','NW','SE','SW'] +region_dir = ['masks', 'Rignot_GRE'] +region_title = ['CE', 'CW', 'NE', 'NO', 'NW', 'SE', 'SW'] # regional filenames region_filename = 'divide_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lon','lat'),'formats':('f','f')} +region_dtype = {'names': ('lon', 'lat'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','GRE_Basins_IMBIE2_v1.3','GRE_Basins_IMBIE2_v1.3.shp'] +IMBIE_basin_file = [ + 'masks', + 'GRE_Basins_IMBIE2_v1.3', + 'GRE_Basins_IMBIE2_v1.3.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('CW','NE','NO','NW','SE','SW') +IMBIE_title = ('CW', 'NE', 'NO', 'NW', 'SE', 'SW') # background image mosaics # MODIS mosaic of Greenland -image_file = ['MOG','mog500_2005_hp1_v1.1.tif'] +image_file = ['MOG', 'mog500_2005_hp1_v1.1.tif'] # Greenland grounded ice -coast_file = ['masks','GIMP','grn_ice_sheet_peripheral_glaciers.shp'] +coast_file = ['masks', 'GIMP', 'grn_ice_sheet_peripheral_glaciers.shp'] # Greenland bounds (Bamber extended for GIMP) -xlimits = (-160.*5e3, 172.*5e3) -ylimits = (-680.*5e3,-131.*5e3) +xlimits = (-160.0 * 5e3, 172.0 * 5e3) +ylimits = (-680.0 * 5e3, -131.0 * 5e3) # cartopy transform for NSIDC polar stereographic north try: - projection = ccrs.Stereographic(central_longitude=-45.0, - central_latitude=+90.0,true_scale_latitude=+70.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=-45.0, + central_latitude=+90.0, + true_scale_latitude=+70.0, + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -116,6 +126,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -125,9 +136,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Greenland drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -139,7 +152,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no GIC or islands - i = [i for i,a in enumerate(shape_attributes) if a[0] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[0] in IMBIE_title] # for each valid shape entity for indice in i: # extract lat/lon coordinates for record @@ -147,10 +160,15 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): + for p1, p2 in zip(parts[:-1], parts[1:]): # converting basin lat/lon into plot coordinates - ax.plot(points[p1:p2,0], points[p1:p2,1], color='k', - transform=ccrs.PlateCarree()) + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + color='k', + transform=ccrs.PlateCarree(), + ) + # PURPOSE: plot Greenland grounded ice delineation from GIMP def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): @@ -159,12 +177,18 @@ def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], c='k', lw=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + c='k', + lw=LINEWIDTH, + transform=projection, + ) + # PURPOSE: plot glaciated regions from Randolph Glacier Inventory def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): @@ -174,36 +198,43 @@ def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): RGI_files.append('06_rgi60_Iceland') RGI_files.append('07_rgi60_Svalbard') for f in RGI_files: - RGI_shapefile = base_dir.joinpath('RGI',f,f'{f}_plot.shp') + RGI_shapefile = base_dir.joinpath('RGI', f, f'{f}_plot.shp') logging.debug(str(RGI_shapefile)) shape_input = shapefile.Reader(str(RGI_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], color='k', linewidth=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + color='k', + linewidth=LINEWIDTH, + transform=projection, + ) + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, color='k', transform=ccrs.PlateCarree()) + # plot the MODIS Mosaic of Greenland as a background image def plot_image_mosaic(ax, base_dir, MASKED=True): # read MODIS mosaic of Greenland @@ -218,8 +249,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -227,35 +258,63 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # dataset range vmin, vmax = (0, 18770) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.05*dx,X+1.05*dx,Y-0.5*dy,Y+4.5*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=1) - for i,c in enumerate([fc1,fc2,fc1,fc2,fc1]): - x1,x2,y1,y2 = [X+0.2*i*dx,X+0.2*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=4) - ax.plot([X,X,X+dx,X+dx], [Y+1.5*dy,Y,Y,Y+1.5*dy], fc2, zorder=4) - ax.text(X+0.5*dx, Y+1.3*dy, f'{dx/1e3:0.0f} km', - ha='center', va='bottom', fontsize=10, color=fc2) + x1, x2, y1, y2 = [ + X - 0.05 * dx, + X + 1.05 * dx, + Y - 0.5 * dy, + Y + 4.5 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=1) + for i, c in enumerate([fc1, fc2, fc1, fc2, fc1]): + x1, x2, y1, y2 = [X + 0.2 * i * dx, X + 0.2 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=4) + ax.plot( + [X, X, X + dx, X + dx], + [Y + 1.5 * dy, Y, Y, Y + 1.5 * dy], + fc2, + zorder=4, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + f'{dx / 1e3:0.0f} km', + ha='center', + va='bottom', + fontsize=10, + color=fc2, + ) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -287,11 +346,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -307,13 +366,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -322,21 +382,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -344,56 +404,68 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup polar stereographic maps - fig, (ax[0],ax[1],ax[2],ax[3],ax[4]) = plt.subplots( - num=1, ncols=5, figsize=(11.25,3.5), - subplot_kw=dict(projection=projection)) + fig, (ax[0], ax[1], ax[2], ax[3], ax[4]) = plt.subplots( + num=1, + ncols=5, + figsize=(11.25, 3.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # plot image of MODIS mosaic of Greenland as base layer if BASEMAP: # plot MODIS mosaic of Greenland plot_image_mosaic(ax1, base_dir) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -404,92 +476,136 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave=np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(lon,lat,data,[ave],colors='blue',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot the coastline and grounded ice files plot_coastline(ax1, base_dir) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 else: @@ -502,26 +618,38 @@ def plot_grid(base_dir, FILENAMES, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) # add title for each subplot if TITLES is not None: TITLE = ' '.join(TITLES[i].split('_')) - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=14) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=14) ax1.title.set_y(1.00) # Add figure label for each subplot if LABELS is not None: - at = offsetbox.AnchoredText(LABELS[i], - loc=2, pad=0, borderpad=0.25, frameon=True, - prop=dict(size=18,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.1") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABELS[i], + loc=2, + pad=0, + borderpad=0.25, + frameon=True, + prop=dict(size=18, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.1') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # x and y limits, axis = equal @@ -538,15 +666,16 @@ def plot_grid(base_dir, FILENAMES, # draw map scale to corners of axis if DRAW_SCALE: - add_plot_scale(ax[0],285e3,-334e4,500e3,40e3,False) + add_plot_scale(ax[0], 285e3, -334e4, 500e3, 40e3, False) # Add colorbar # Add an axes at position rect [left, bottom, width, height] cbar_ax = fig.add_axes([0.905, 0.05, 0.0225, 0.875]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False) + cbar = fig.colorbar( + im, cax=cbar_ax, extend=CBEXTEND, extendfrac=0.0375, drawedges=False + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -556,166 +685,276 @@ def plot_grid(base_dir, FILENAMES, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=19, labelsize=14, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=19, labelsize=14, direction='in' + ) # adjust subplots within figure - fig.subplots_adjust(left=0.01,right=0.89,bottom=0.005,top=0.955,wspace=0.05) + fig.subplots_adjust( + left=0.01, right=0.89, bottom=0.005, top=0.955, wspace=0.05 + ) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates 5 GMT-like plots of the Greenland ice sheet on a NSIDC polar stereographic north (EPSG 3413) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', nargs=5, - type=pathlib.Path, - help='Input grid files') + parser.add_argument( + 'infile', nargs=5, type=pathlib.Path, help='Input grid files' + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, nargs='+', - default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + nargs='+', + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', nargs=5, - type=str, help='Plot title') - parser.add_argument('--plot-label', nargs=5, - type=str, help='Plot label') + parser.add_argument('--plot-title', nargs=5, type=str, help='Plot title') + parser.add_argument('--plot-label', nargs=5, type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--glacier-margins', - default=False, action='store_true', - help='Add delineations for glacier margins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--glacier-margins', + default=False, + action='store_true', + help='Add delineations for glacier margins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -725,7 +964,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -755,7 +996,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -763,6 +1005,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_GrIS_grid_maps.py b/mapping/plot_GrIS_grid_maps.py index a6ae0082..729ff5a2 100644 --- a/mapping/plot_GrIS_grid_maps.py +++ b/mapping/plot_GrIS_grid_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_GrIS_grid_maps.py Written by Tyler Sutterley (05/2023) Creates GMT-like plots for the Greenland ice sheet @@ -62,6 +62,7 @@ Updated 06/2015: no bounding box Written 05/2015 """ + from __future__ import print_function import sys @@ -79,7 +80,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -87,52 +88,61 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Greenland ice divides # region directory, filename, title and data type -region_dir = ['masks','Rignot_GRE'] -region_title = ['CE','CW','NE','NO','NW','SE','SW'] +region_dir = ['masks', 'Rignot_GRE'] +region_title = ['CE', 'CW', 'NE', 'NO', 'NW', 'SE', 'SW'] # regional filenames region_filename = 'divide_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lon','lat'),'formats':('f','f')} +region_dtype = {'names': ('lon', 'lat'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','GRE_Basins_IMBIE2_v1.3','GRE_Basins_IMBIE2_v1.3.shp'] +IMBIE_basin_file = [ + 'masks', + 'GRE_Basins_IMBIE2_v1.3', + 'GRE_Basins_IMBIE2_v1.3.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('CW','NE','NO','NW','SE','SW') +IMBIE_title = ('CW', 'NE', 'NO', 'NW', 'SE', 'SW') # background image mosaics # MODIS mosaic of Greenland -image_file = ['MOG','mog500_2005_hp1_v1.1.tif'] +image_file = ['MOG', 'mog500_2005_hp1_v1.1.tif'] # Greenland grounded ice -coast_file = ['masks','GIMP','grn_ice_sheet_peripheral_glaciers.shp'] +coast_file = ['masks', 'GIMP', 'grn_ice_sheet_peripheral_glaciers.shp'] # Greenland bounds xlimits = np.array([-1530000, 1610000]) ylimits = np.array([-3600000, -280000]) # cartopy transform for NSIDC polar stereographic north try: - projection = ccrs.Stereographic(central_longitude=-45.0, - central_latitude=+90.0,true_scale_latitude=+70.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=-45.0, + central_latitude=+90.0, + true_scale_latitude=+70.0, + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -142,6 +152,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -151,9 +162,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Greenland drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -165,7 +178,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no GIC or islands - i = [i for i,a in enumerate(shape_attributes) if a[0] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[0] in IMBIE_title] # for each valid shape entity for indice in i: # extract lat/lon coordinates for record @@ -173,10 +186,15 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): + for p1, p2 in zip(parts[:-1], parts[1:]): # converting basin lat/lon into plot coordinates - ax.plot(points[p1:p2,0], points[p1:p2,1], color='k', - transform=ccrs.PlateCarree()) + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + color='k', + transform=ccrs.PlateCarree(), + ) + # PURPOSE: plot Greenland grounded ice delineation from GIMP def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): @@ -185,12 +203,18 @@ def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], c='k', lw=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + c='k', + lw=LINEWIDTH, + transform=projection, + ) + # PURPOSE: plot glaciated regions from Randolph Glacier Inventory def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): @@ -200,36 +224,43 @@ def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): RGI_files.append('06_rgi60_Iceland') RGI_files.append('07_rgi60_Svalbard') for f in RGI_files: - RGI_shapefile = base_dir.joinpath('RGI',f,f'{f}_plot.shp') + RGI_shapefile = base_dir.joinpath('RGI', f, f'{f}_plot.shp') logging.debug(str(RGI_shapefile)) shape_input = shapefile.Reader(str(RGI_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], color='k', linewidth=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + color='k', + linewidth=LINEWIDTH, + transform=projection, + ) + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, color='k', transform=ccrs.PlateCarree()) + # plot the MODIS Mosaic of Greenland as a background image def plot_image_mosaic(ax, base_dir, MASKED=True): # read MODIS mosaic of Greenland @@ -244,8 +275,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -253,38 +284,75 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # dataset range vmin, vmax = (0, 18770) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.15*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.15 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAME, +def plot_grid( + base_dir, + FILENAME, DATAFORM=None, VARIABLES=[], MASK=None, @@ -315,8 +383,8 @@ def plot_grid(base_dir, FILENAME, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -331,13 +399,14 @@ def plot_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -346,71 +415,85 @@ def plot_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAME, date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAME, + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAME, date=False, - field_mapping=field_mapping) - elif (DATAFORM == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAME, date=False, field_mapping=field_mapping + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAME, date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAME, date=False, field_mapping=field_mapping + ) # create masked array if missing values if MASK is not None: # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # setup stereographic map - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(9.875,9), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(9.875, 9), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # plot image of MODIS mosaic of Greenland as base layer if BASEMAP: @@ -418,78 +501,132 @@ def plot_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin, latsin, - data=img, order=order, iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2',linestyles='solid', - transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave = np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(lon,lat,data,[ave],colors='blue',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot the coastline and grounded ice files plot_coastline(ax1, base_dir) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 else: @@ -502,11 +639,18 @@ def plot_grid(base_dir, FILENAME, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -516,8 +660,16 @@ def plot_grid(base_dir, FILENAME, # options: neither, both, min, max # shrink = percent size of colorbar # aspect = lengthXwidth aspect of colorbar - cbar = plt.colorbar(im, ax=ax1, pad=0.025, extend=CBEXTEND, - extendfrac=0.0375, shrink=0.98, aspect=22.5, drawedges=False) + cbar = plt.colorbar( + im, + ax=ax1, + pad=0.025, + extend=CBEXTEND, + extendfrac=0.0375, + shrink=0.98, + aspect=22.5, + drawedges=False, + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -527,8 +679,9 @@ def plot_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=26, labelsize=24, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=26, labelsize=24, direction='in' + ) # x and y limits, axis = equal ax1.set_xlim(xlimits) @@ -540,185 +693,298 @@ def plot_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=24) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=24) # Add figure label if LABEL is not None: if BASEMAP: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=24,weight='bold')) - at.patch.set_boxstyle("Square,pad=0.25") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=24, weight='bold'), + ) + at.patch.set_boxstyle('Square,pad=0.25') + at.patch.set_edgecolor('white') else: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=False, - prop=dict(size=24,weight='bold')) + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=False, + prop=dict(size=24, weight='bold'), + ) ax1.axes.add_artist(at) # draw map scale to corners if DRAW_SCALE: - add_plot_scale(ax1,1110e3,-3460e3,400e3,55e3,False) + add_plot_scale(ax1, 1110e3, -3460e3, 400e3, 55e3, False) # stronger linewidth on frame ax1.spines['geo'].set_linewidth(2.0) ax1.spines['geo'].set_zorder(10) ax1.spines['geo'].set_capstyle('projecting') # adjust subplot within figure - fig.subplots_adjust(left=0.02,right=0.99,bottom=0.01,top=0.95) + fig.subplots_adjust(left=0.02, right=0.99, bottom=0.01, top=0.95) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates GMT-like plots of the Greenland ice sheet on a NSIDC polar stereographic north (EPSG 3413) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', - type=pathlib.Path, - help='Input grid file') + parser.add_argument('infile', type=pathlib.Path, help='Input grid file') # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', - type=str, help='Plot title') - parser.add_argument('--plot-label', - type=str, help='Plot label') + parser.add_argument('--plot-title', type=str, help='Plot title') + parser.add_argument('--plot-label', type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--glacier-margins', - default=False, action='store_true', - help='Add delineations for glacier margins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--glacier-margins', + default=False, + action='store_true', + help='Add delineations for glacier margins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -728,7 +994,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -758,7 +1026,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -766,6 +1035,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_GrIS_grid_movie.py b/mapping/plot_GrIS_grid_movie.py index 296ead8d..3f13ee58 100644 --- a/mapping/plot_GrIS_grid_movie.py +++ b/mapping/plot_GrIS_grid_movie.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_GrIS_grid_movie.py Written by Tyler Sutterley (05/2023) Creates GMT-like animations for the Greenland Ice Sheet @@ -66,6 +66,7 @@ Updated 10/2015: updated for GIMP background mosaic Written 05/2015 """ + from __future__ import print_function import sys @@ -84,7 +85,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -93,59 +94,68 @@ import matplotlib.ticker as ticker import matplotlib.animation as animation import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # output file information suffix = ['txt', 'nc', 'H5'] # output units -unit_list = ['cmwe', 'mmGH', 'mmCU', u'\\u03BCGal', 'mbar'] +unit_list = ['cmwe', 'mmGH', 'mmCU', '\\u03BCGal', 'mbar'] # Greenland ice divides # region directory, filename, title and data type -region_dir = ['masks','Rignot_GRE'] -region_title = ['CE','CW','NE','NO','NW','SE','SW'] +region_dir = ['masks', 'Rignot_GRE'] +region_title = ['CE', 'CW', 'NE', 'NO', 'NW', 'SE', 'SW'] # regional filenames region_filename = 'divide_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lon','lat'),'formats':('f','f')} +region_dtype = {'names': ('lon', 'lat'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins IMBIE_basin_file = {} -IMBIE_basin_file=['masks','GRE_Basins_IMBIE2_v1.3','GRE_Basins_IMBIE2_v1.3.shp'] +IMBIE_basin_file = [ + 'masks', + 'GRE_Basins_IMBIE2_v1.3', + 'GRE_Basins_IMBIE2_v1.3.shp', +] # basin titles within shapefile to extract IMBIE_title = {} -IMBIE_title=('CW','NE','NO','NW','SE','SW') +IMBIE_title = ('CW', 'NE', 'NO', 'NW', 'SE', 'SW') # background image mosaics # MODIS mosaic of Greenland -image_file = ['MOG','mog500_2005_hp1_v1.1.tif'] +image_file = ['MOG', 'mog500_2005_hp1_v1.1.tif'] # Greenland grounded ice -coast_file=['masks','GIMP','grn_ice_sheet_peripheral_glaciers.shp'] +coast_file = ['masks', 'GIMP', 'grn_ice_sheet_peripheral_glaciers.shp'] # Greenland bounds xlimits = np.array([-1530000, 1610000]) ylimits = np.array([-3600000, -280000]) # cartopy transform for NSIDC polar stereographic north try: - projection = ccrs.Stereographic(central_longitude=-45.0, - central_latitude=+90.0,true_scale_latitude=+70.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=-45.0, + central_latitude=+90.0, + true_scale_latitude=+70.0, + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -155,6 +165,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -164,9 +175,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Greenland drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -178,7 +191,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no GIC or islands - i = [i for i,a in enumerate(shape_attributes) if a[0] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[0] in IMBIE_title] # for each valid shape entity for indice in i: # extract lat/lon coordinates for record @@ -186,10 +199,15 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): + for p1, p2 in zip(parts[:-1], parts[1:]): # converting basin lat/lon into plot coordinates - ax.plot(points[p1:p2,0], points[p1:p2,1], color='k', - transform=ccrs.PlateCarree()) + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + color='k', + transform=ccrs.PlateCarree(), + ) + # PURPOSE: plot Greenland grounded ice delineation from GIMP def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): @@ -198,12 +216,18 @@ def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], c='k', lw=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + c='k', + lw=LINEWIDTH, + transform=projection, + ) + # PURPOSE: plot glaciated regions from Randolph Glacier Inventory def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): @@ -213,36 +237,43 @@ def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): RGI_files.append('06_rgi60_Iceland') RGI_files.append('07_rgi60_Svalbard') for f in RGI_files: - RGI_shapefile = base_dir.joinpath('RGI',f,f'{f}_plot.shp') + RGI_shapefile = base_dir.joinpath('RGI', f, f'{f}_plot.shp') logging.debug(str(RGI_shapefile)) shape_input = shapefile.Reader(str(RGI_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], color='k', linewidth=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + color='k', + linewidth=LINEWIDTH, + transform=projection, + ) + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, color='k', transform=ccrs.PlateCarree()) + # plot the MODIS Mosaic of Greenland as a background image def plot_image_mosaic(ax, base_dir, MASKED=True): # read MODIS mosaic of Greenland @@ -257,8 +288,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -266,38 +297,75 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # dataset range vmin, vmax = (0, 18770) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.15*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.15 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # animate grid program -def animate_grid(base_dir, FILENAME, +def animate_grid( + base_dir, + FILENAME, DATAFORM=None, MASK=None, INTERPOLATION=None, @@ -327,8 +395,8 @@ def animate_grid(base_dir, FILENAME, DRAW_SCALE=False, FIGURE_FILE=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -343,13 +411,14 @@ def animate_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -358,21 +427,21 @@ def animate_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file @@ -381,15 +450,25 @@ def animate_grid(base_dir, FILENAME, # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - dinput = gravtk.spatial().from_file(FILENAME, - format=DATAFORM, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + dinput = gravtk.spatial().from_file( + FILENAME, + format=DATAFORM, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) elif DATAFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,dataform = DATAFORM.split('-') - dinput = gravtk.spatial().from_index(FILENAME, - format=dataform, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + _, dataform = DATAFORM.split('-') + dinput = gravtk.spatial().from_index( + FILENAME, + format=dataform, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) # replace invalid with a new fill value dinput.replace_invalid(fill_value=FILL_VALUE) @@ -398,41 +477,51 @@ def animate_grid(base_dir, FILENAME, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # create movie writer objects FFMpegWriter = animation.writers['ffmpeg'] - metadata = dict(title=pathlib.Path(sys.argv[0]).name, artist='Matplotlib', - date_created=time.strftime('%Y-%m-%d',time.localtime())) + metadata = dict( + title=pathlib.Path(sys.argv[0]).name, + artist='Matplotlib', + date_created=time.strftime('%Y-%m-%d', time.localtime()), + ) # bitrate to be determined automatically by underlying utility - writer = FFMpegWriter(fps=8, metadata=metadata, bitrate=-1, - extra_args=['-vcodec','libx264']) + writer = FFMpegWriter( + fps=8, metadata=metadata, bitrate=-1, extra_args=['-vcodec', 'libx264'] + ) # setup stereographic map - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(9.875,9), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(9.875, 9), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # plot image of MODIS mosaic of Greenland as base layer if BASEMAP: @@ -440,43 +529,54 @@ def animate_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # only plot grounded points if MASK is not None: - mask = gravtk.tools.mask_oceans(lonsin,latsin,order=order) + mask = gravtk.tools.mask_oceans(lonsin, latsin, order=order) # add place holder for figure image - im = ax1.imshow(np.zeros((my,mx)), interpolation='nearest', cmap=cmap, - norm=norm, extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - alpha=ALPHA, origin='lower', transform=projection, animated=True) + im = ax1.imshow( + np.zeros((my, mx)), + interpolation='nearest', + cmap=cmap, + norm=norm, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + alpha=ALPHA, + origin='lower', + transform=projection, + animated=True, + ) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # plot the coastline and grounded ice files plot_coastline(ax1, base_dir) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 else: @@ -488,11 +588,18 @@ def animate_grid(base_dir, FILENAME, if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -502,8 +609,16 @@ def animate_grid(base_dir, FILENAME, # options: neither, both, min, max # shrink = percent size of colorbar # aspect = lengthXwidth aspect of colorbar - cbar = plt.colorbar(im, ax=ax1, pad=0.025, extend=CBEXTEND, - extendfrac=0.0375, shrink=0.98, aspect=22.5, drawedges=False) + cbar = plt.colorbar( + im, + ax=ax1, + pad=0.025, + extend=CBEXTEND, + extendfrac=0.0375, + shrink=0.98, + aspect=22.5, + drawedges=False, + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -513,8 +628,9 @@ def animate_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=26, labelsize=24, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=26, labelsize=24, direction='in' + ) # x and y limits, axis = equal ax1.set_xlim(xlimits) @@ -526,38 +642,55 @@ def animate_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=24) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=24) # Add figure label if LABEL is not None: if BASEMAP: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=24,weight='bold')) - at.patch.set_boxstyle("Square,pad=0.25") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=24, weight='bold'), + ) + at.patch.set_boxstyle('Square,pad=0.25') + at.patch.set_edgecolor('white') else: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=False, - prop=dict(size=24,weight='bold')) + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=False, + prop=dict(size=24, weight='bold'), + ) ax1.axes.add_artist(at) # draw map scale to corners if DRAW_SCALE: - add_plot_scale(ax1,1110e3,-3460e3,400e3,55e3,False) + add_plot_scale(ax1, 1110e3, -3460e3, 400e3, 55e3, False) # add date label (year-calendar month e.g. 2002-01) # if plotting with a background mosaic: use a white time label # else: use a black time label text_color = 'w' if BASEMAP else 'k' - time_text = ax1.text(0.775, 0.025, '', transform=ax1.transAxes, - color=text_color, size=30, ha='left', va='baseline', usetex=True) + time_text = ax1.text( + 0.775, + 0.025, + '', + transform=ax1.transAxes, + color=text_color, + size=30, + ha='left', + va='baseline', + usetex=True, + ) # stronger linewidth on frame ax1.spines['geo'].set_linewidth(2.0) ax1.spines['geo'].set_zorder(10) ax1.spines['geo'].set_capstyle('projecting') # adjust subplot within figure - fig.subplots_adjust(left=0.02,right=0.99,bottom=0.02,top=0.96) + fig.subplots_adjust(left=0.02, right=0.99, bottom=0.02, top=0.96) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) @@ -565,23 +698,45 @@ def animate_grid(base_dir, FILENAME, # create image for each frame with writer.saving(fig, FIGURE_FILE, FIGURE_DPI): # for each input file - for t,gm in enumerate(dinput.month): + for t, gm in enumerate(dinput.month): # data for time t converted to a masked array data = dinput.subset(gm).to_masked_array() # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,data.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,data.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(data.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(data.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and ( + np.max(dinput.lon) > 180 + ): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, data.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, data.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + data.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + data.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # only plot grounded points @@ -596,28 +751,61 @@ def animate_grid(base_dir, FILENAME, contours = [] if CONTOURS and (np.sum(data**2) > 0): # plot line contours - contours.append(ax1.contour(lon, lat, data, reduce_clevs, - colors='0.2', linestyles='solid', - transform=ccrs.PlateCarree())) - contours.append(ax1.contour(lon, lat, data, 0, - colors='red', linestyles='solid', linewidths=1.5, - transform=ccrs.PlateCarree())) + contours.append( + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ) + contours.append( + ax1.contour( + lon, + lat, + data, + 0, + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = ( + (rad_e**2) + * dth + * dphi + * np.cos(np.radians(lat[indy, indx])) + ) # calculate average - ave = np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - contours.append(ax1.contour(lon, lat, data, [ave], - colors='blue', linestyles='solid', linewidths=1.5, - transform=ccrs.PlateCarree())) + contours.append( + ax1.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) + ) # add date label (year-calendar month e.g. 2002-01) year = np.floor(dinput.time[t]).astype(np.int64) - calendar_month = np.int64(((gm-1) % 12)+1) - date_label=r'\textbf{{{0:4d}--{1:02d}}}'.format(year,calendar_month) + calendar_month = np.int64(((gm - 1) % 12) + 1) + date_label = r'\textbf{{{0:4d}--{1:02d}}}'.format( + year, calendar_month + ) time_text.set_text(date_label) # add to movie writer.grab_frame() @@ -626,142 +814,234 @@ def animate_grid(base_dir, FILENAME, # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates GMT-like animations of the Greenland ice sheet on a NSIDC polar stereographic north (EPSG 3413) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', - type=pathlib.Path, - help='Input grid file') + parser.add_argument('infile', type=pathlib.Path, help='Input grid file') # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', - type=str, help='Plot title') - parser.add_argument('--plot-label', - type=str, help='Plot label') + parser.add_argument('--plot-title', type=str, help='Plot title') + parser.add_argument('--plot-label', type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--glacier-margins', - default=False, action='store_true', - help='Add delineations for glacier margins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--glacier-margins', + default=False, + action='store_true', + help='Add delineations for glacier margins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -771,7 +1051,9 @@ def main(): try: info(args) # run plot program with parameters - animate_grid(args.directory, args.infile, + animate_grid( + args.directory, + args.infile, DATAFORM=args.format, DDEG=args.spacing, INTERVAL=args.interval, @@ -799,7 +1081,8 @@ def main(): DRAW_SCALE=args.draw_scale, FIGURE_FILE=args.figure_file, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -807,6 +1090,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_QML_grid_3maps.py b/mapping/plot_QML_grid_3maps.py index d2bda2df..25eaad3d 100644 --- a/mapping/plot_QML_grid_3maps.py +++ b/mapping/plot_QML_grid_3maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_grid_3maps.py Written by Tyler Sutterley (05/2023) Creates 3 GMT-like plots for Queen Maud Land (QML) in Antarctica @@ -51,6 +51,7 @@ Updated 12/2018: added parameter CBEXTEND for colorbar extension triangles Written 10/2018 """ + from __future__ import print_function import sys @@ -68,7 +69,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -76,55 +77,103 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # Queen Maud Land bounds xlimits = np.array([-940000, 2400000]) ylimits = np.array([530000, 2300000]) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -134,6 +183,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -143,9 +193,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -157,7 +209,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -165,30 +217,34 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -197,12 +253,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, MASKED=True): @@ -214,55 +270,97 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): info_geotiff = ds.GetGeoTransform() # reduce input image with GDAL # Specify offset and rows and columns to read - xoff = int((xlimits[0] - info_geotiff[0])/info_geotiff[1]) - yoff = int((ylimits[1] - info_geotiff[3])/info_geotiff[5]) - xsize = int((xlimits[1] - xlimits[0])/info_geotiff[1]) + 1 - ysize = int((ylimits[0] - ylimits[1])/info_geotiff[5]) + 1 + xoff = int((xlimits[0] - info_geotiff[0]) / info_geotiff[1]) + yoff = int((ylimits[1] - info_geotiff[3]) / info_geotiff[5]) + xsize = int((xlimits[1] - xlimits[0]) / info_geotiff[1]) + 1 + ysize = int((ylimits[0] - ylimits[1]) / info_geotiff[5]) + 1 # read as grayscale image reducing to xlimit and ylimit - mosaic = np.ma.array(ds.ReadAsArray(xoff=xoff, yoff=yoff, - xsize=xsize, ysize=ysize)) + mosaic = np.ma.array( + ds.ReadAsArray(xoff=xoff, yoff=yoff, xsize=xsize, ysize=ysize) + ) # mask image mosaic if MASKED: # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # reduced x and y limits of image - xmin = info_geotiff[0] + xoff*info_geotiff[1] - xmax = info_geotiff[0] + xoff*info_geotiff[1] + (xsize-1)*info_geotiff[1] - ymax = info_geotiff[3] + yoff*info_geotiff[5] - ymin = info_geotiff[3] + yoff*info_geotiff[5] + (ysize-1)*info_geotiff[5] + xmin = info_geotiff[0] + xoff * info_geotiff[1] + xmax = ( + info_geotiff[0] + xoff * info_geotiff[1] + (xsize - 1) * info_geotiff[1] + ) + ymax = info_geotiff[3] + yoff * info_geotiff[5] + ymin = ( + info_geotiff[3] + yoff * info_geotiff[5] + (ysize - 1) * info_geotiff[5] + ) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.3*dx,X+1.2*dx,Y-3.5*dy,Y+3.4*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.3 * dx, + X + 1.2 * dx, + Y - 3.5 * dy, + Y + 3.4 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -293,11 +391,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -313,13 +411,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -328,21 +427,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -350,55 +449,68 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup polar stereographic maps - fig, (ax[0],ax[1],ax[2]) = plt.subplots(num=1, nrows=3, figsize=(6.5,9.0), - subplot_kw=dict(projection=projection)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, + nrows=3, + figsize=(6.5, 9.0), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # plot image of MODIS mosaic of Antarctica as base layer if BASEMAP: # plot MODIS mosaic of Antarctica plot_image_mosaic(ax1, base_dir) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -409,93 +521,137 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(data.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave=np.sum(area*data[indy,indx])/np.sum(area) + ave = np.sum(area * data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(lon,lat,data,[ave],colors='blue',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2_subbasin'): + elif BASIN_TYPE == 'IMBIE-2_subbasin': plot_IMBIE2_subbasins(ax1, base_dir) start_indice = 1 else: @@ -506,26 +662,38 @@ def plot_grid(base_dir, FILENAMES, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) # add title for each subplot if TITLES is not None: TITLE = ' '.join(TITLES[i].split('_')) - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=14) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=14) ax1.title.set_y(1.00) # Add figure label for each subplot if LABELS is not None: - at = offsetbox.AnchoredText(LABELS[i], - loc=2, pad=0, borderpad=0.25, frameon=True, - prop=dict(size=18,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.1") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABELS[i], + loc=2, + pad=0, + borderpad=0.25, + frameon=True, + prop=dict(size=18, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.1') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # x and y limits, axis = equal @@ -542,15 +710,16 @@ def plot_grid(base_dir, FILENAMES, # draw map scale to corners of axis if DRAW_SCALE: - add_plot_scale(ax[0],1695e3,2115e3,600e3,50e3,False) + add_plot_scale(ax[0], 1695e3, 2115e3, 600e3, 50e3, False) # Add colorbar # Add an axes at position rect [left, bottom, width, height] cbar_ax = fig.add_axes([0.84, 0.045, 0.045, 0.875]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False) + cbar = fig.colorbar( + im, cax=cbar_ax, extend=CBEXTEND, extendfrac=0.0375, drawedges=False + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -560,163 +729,270 @@ def plot_grid(base_dir, FILENAMES, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=21, labelsize=14, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=21, labelsize=14, direction='in' + ) # adjust subplots within figure - fig.subplots_adjust(left=0.01,right=0.83,bottom=0.01,top=0.97,hspace=0.12) + fig.subplots_adjust( + left=0.01, right=0.83, bottom=0.01, top=0.97, hspace=0.12 + ) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates 3 GMT-like plots of Queen Maud Land on a polar stereographic south (EPSG 3031) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', nargs=3, - type=pathlib.Path, - help='Input grid files') + parser.add_argument( + 'infile', nargs=3, type=pathlib.Path, help='Input grid files' + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, nargs='+', - default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + nargs='+', + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', nargs=3, - type=str, help='Plot title') - parser.add_argument('--plot-label', nargs=3, - type=str, help='Plot label') + parser.add_argument('--plot-title', nargs=3, type=str, help='Plot title') + parser.add_argument('--plot-label', nargs=3, type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--basemap', - default=False, action='store_true', - help='Add background basemap image') - parser.add_argument('--basin-type', - type=str, default='', - help='Add delineations for glacier drainage basins') - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--draw-scale', - default=False, action='store_true', - help='Add map scale bar') + parser.add_argument( + '--basemap', + default=False, + action='store_true', + help='Add background basemap image', + ) + parser.add_argument( + '--basin-type', + type=str, + default='', + help='Add delineations for glacier drainage basins', + ) + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--draw-scale', + default=False, + action='store_true', + help='Add map scale bar', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -726,7 +1002,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -755,7 +1033,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -763,6 +1042,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_global_grid_3maps.py b/mapping/plot_global_grid_3maps.py index b3f9c145..53110858 100644 --- a/mapping/plot_global_grid_3maps.py +++ b/mapping/plot_global_grid_3maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_3maps.py Written by Tyler Sutterley (05/2023) Creates 3 GMT-like plots in a Plate Carree (Equirectangular) projection @@ -46,6 +46,7 @@ Updated 11/2017: can plot a contour of the global average with MEAN Written 08/2017 """ + from __future__ import print_function import sys @@ -65,7 +66,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -73,23 +74,25 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -99,44 +102,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -164,11 +176,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -178,13 +190,14 @@ def plot_grid(base_dir, FILENAMES, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -193,21 +206,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -215,50 +228,63 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup Plate Carree projection - fig, (ax[0],ax[1],ax[2]) = plt.subplots(num=1, nrows=3, figsize=(6.75,9.0), - subplot_kw=dict(projection=projection)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, + nrows=3, + figsize=(6.75, 9.0), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -269,92 +295,132 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom(np.invert(img.mask), 5, order=1, output=bool) data.mask = np.invert(mask) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(data,reduce_clevs,colors='0.2',linestyles='solid', - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) - ax1.contour(data,[0],colors='red',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(dinput.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(dinput.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave = np.sum(area*dinput.data[indy,indx])/np.sum(area) + ave = np.sum(area * dinput.data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(data,[ave],colors='blue',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # draw coastlines plot_coastline(ax1, base_dir) @@ -364,26 +430,38 @@ def plot_grid(base_dir, FILENAMES, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=projection, draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=projection, + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) # add title for each subplot if TITLES is not None: TITLE = ' '.join(TITLES[i].split('_')) - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=18) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=18) ax1.title.set_y(1.00) # Add figure label for each subplot if LABELS is not None: - at = offsetbox.AnchoredText(LABELS[i], - loc=2, pad=0, borderpad=0.25, frameon=True, - prop=dict(size=18,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.1") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABELS[i], + loc=2, + pad=0, + borderpad=0.25, + frameon=True, + prop=dict(size=18, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.1') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # axis = equal @@ -401,8 +479,9 @@ def plot_grid(base_dir, FILENAMES, cbar_ax = fig.add_axes([0.82, 0.07, 0.05, 0.86]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False) + cbar = fig.colorbar( + im, cax=cbar_ax, extend=CBEXTEND, extendfrac=0.0375, drawedges=False + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -412,154 +491,252 @@ def plot_grid(base_dir, FILENAMES, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=24, labelsize=18, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=24, labelsize=18, direction='in' + ) # adjust subplots within figure - fig.subplots_adjust(left=0.01,right=0.79,bottom=0.01,top=0.97,hspace=0.10) + fig.subplots_adjust( + left=0.01, right=0.79, bottom=0.01, top=0.97, hspace=0.10 + ) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( - description=u"""Creates 3 GMT-like plots on a global Plate Carr\u00E9e + description="""Creates 3 GMT-like plots on a global Plate Carr\u00e9e (Equirectangular) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', nargs=3, - type=pathlib.Path, - help='Input grid files') + parser.add_argument( + 'infile', nargs=3, type=pathlib.Path, help='Input grid files' + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, nargs='+', - default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + nargs='+', + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', nargs=3, - type=str, help='Plot title') - parser.add_argument('--plot-label', nargs=3, - type=str, help='Plot label') + parser.add_argument('--plot-title', nargs=3, type=str, help='Plot title') + parser.add_argument('--plot-label', nargs=3, type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -569,7 +746,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -595,7 +774,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -603,6 +783,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_global_grid_4maps.py b/mapping/plot_global_grid_4maps.py index 88e64a85..5fb99dd6 100644 --- a/mapping/plot_global_grid_4maps.py +++ b/mapping/plot_global_grid_4maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_4maps.py Written by Tyler Sutterley (05/2023) Creates 4 GMT-like plots in a Plate Carree (Equirectangular) projection @@ -46,6 +46,7 @@ Updated 11/2017: can plot a contour of the global average with MEAN Written 08/2017 """ + from __future__ import print_function import sys @@ -65,7 +66,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -73,23 +74,25 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -99,44 +102,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -164,11 +176,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -178,13 +190,14 @@ def plot_grid(base_dir, FILENAMES, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -193,21 +206,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -215,50 +228,64 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup Plate Carree projection - fig, ((ax[0],ax[1]),(ax[2],ax[3])) = plt.subplots(num=1, nrows=2, ncols=2, - figsize=(10.375,7.0), subplot_kw=dict(projection=projection)) + fig, ((ax[0], ax[1]), (ax[2], ax[3])) = plt.subplots( + num=1, + nrows=2, + ncols=2, + figsize=(10.375, 7.0), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -269,92 +296,132 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom(np.invert(img.mask), 5, order=1, output=bool) data.mask = np.invert(mask) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(data,reduce_clevs,colors='0.2',linestyles='solid', - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) - ax1.contour(data,[0],colors='red',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(dinput.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(dinput.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave = np.sum(area*dinput.data[indy,indx])/np.sum(area) + ave = np.sum(area * dinput.data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(data,[ave],colors='blue',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # draw coastlines plot_coastline(ax1, base_dir) @@ -364,26 +431,38 @@ def plot_grid(base_dir, FILENAMES, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=projection, draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=projection, + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) # add title for each subplot if TITLES is not None: TITLE = ' '.join(TITLES[i].split('_')) - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=18) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=18) ax1.title.set_y(1.01) # Add figure label for each subplot if LABELS is not None: - at = offsetbox.AnchoredText(LABELS[i], - loc=2, pad=0, borderpad=0.25, frameon=True, - prop=dict(size=18,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.1") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABELS[i], + loc=2, + pad=0, + borderpad=0.25, + frameon=True, + prop=dict(size=18, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.1') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # axis = equal @@ -401,8 +480,14 @@ def plot_grid(base_dir, FILENAMES, cbar_ax = fig.add_axes([0.095, 0.105, 0.81, 0.045]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False, orientation='horizontal') + cbar = fig.colorbar( + im, + cax=cbar_ax, + extend=CBEXTEND, + extendfrac=0.0375, + drawedges=False, + orientation='horizontal', + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -413,155 +498,252 @@ def plot_grid(base_dir, FILENAMES, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=22, labelsize=18, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=22, labelsize=18, direction='in' + ) # adjust subplots within figure - fig.subplots_adjust(left=0.01, right=0.99, bottom=0.16, top=0.97, - hspace=0.05, wspace=0.05) + fig.subplots_adjust( + left=0.01, right=0.99, bottom=0.16, top=0.97, hspace=0.05, wspace=0.05 + ) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( - description=u"""Creates 4 GMT-like plots on a global Plate Carr\u00E9e + description="""Creates 4 GMT-like plots on a global Plate Carr\u00e9e (Equirectangular) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', nargs=4, - type=pathlib.Path, - help='Input grid files') + parser.add_argument( + 'infile', nargs=4, type=pathlib.Path, help='Input grid files' + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, nargs='+', - default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + nargs='+', + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', nargs=4, - type=str, help='Plot title') - parser.add_argument('--plot-label', nargs=4, - type=str, help='Plot label') + parser.add_argument('--plot-title', nargs=4, type=str, help='Plot title') + parser.add_argument('--plot-label', nargs=4, type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -571,7 +753,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -597,7 +781,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -605,6 +790,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_global_grid_5maps.py b/mapping/plot_global_grid_5maps.py index f6a4b10e..9e770b64 100644 --- a/mapping/plot_global_grid_5maps.py +++ b/mapping/plot_global_grid_5maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_5maps.py Written by Tyler Sutterley (05/2023) Creates 5 GMT-like plots in a Plate Carree (Equirectangular) projection @@ -46,6 +46,7 @@ Updated 11/2017: can plot a contour of the global average with MEAN Written 08/2017 """ + from __future__ import print_function import sys @@ -65,7 +66,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -74,23 +75,25 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -100,44 +103,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -165,11 +177,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -179,13 +191,14 @@ def plot_grid(base_dir, FILENAMES, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -194,21 +207,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -216,55 +229,64 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup Plate Carree projection - fig = plt.figure(figsize=(10.375,12.5)) - gs = gridspec.GridSpec(3, 2, height_ratios=[2,1,1]) - ax[0] = plt.subplot(gs[0,:], projection=projection) - ax[1] = plt.subplot(gs[1,0], projection=projection) - ax[2] = plt.subplot(gs[1,1], projection=projection) - ax[3] = plt.subplot(gs[2,0], projection=projection) - ax[4] = plt.subplot(gs[2,1], projection=projection) + fig = plt.figure(figsize=(10.375, 12.5)) + gs = gridspec.GridSpec(3, 2, height_ratios=[2, 1, 1]) + ax[0] = plt.subplot(gs[0, :], projection=projection) + ax[1] = plt.subplot(gs[1, 0], projection=projection) + ax[2] = plt.subplot(gs[1, 1], projection=projection) + ax[3] = plt.subplot(gs[2, 0], projection=projection) + ax[4] = plt.subplot(gs[2, 1], projection=projection) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -275,92 +297,132 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom(np.invert(img.mask), 5, order=1, output=bool) data.mask = np.invert(mask) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(data,reduce_clevs,colors='0.2',linestyles='solid', - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) - ax1.contour(data,[0],colors='red',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(dinput.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(dinput.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave = np.sum(area*dinput.data[indy,indx])/np.sum(area) + ave = np.sum(area * dinput.data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(data,[ave],colors='blue',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # draw coastlines plot_coastline(ax1, base_dir) @@ -370,26 +432,38 @@ def plot_grid(base_dir, FILENAMES, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=projection, draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=projection, + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) # add title for each subplot if TITLES is not None: TITLE = ' '.join(TITLES[i].split('_')) - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=18) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=18) ax1.title.set_y(1.01) # Add figure label for each subplot if LABELS is not None: - at = offsetbox.AnchoredText(LABELS[i], - loc=2, pad=0, borderpad=0.25, frameon=True, - prop=dict(size=18,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.1") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABELS[i], + loc=2, + pad=0, + borderpad=0.25, + frameon=True, + prop=dict(size=18, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.1') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # axis = equal @@ -407,8 +481,14 @@ def plot_grid(base_dir, FILENAMES, cbar_ax = fig.add_axes([0.095, 0.065, 0.81, 0.025]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False, orientation='horizontal') + cbar = fig.colorbar( + im, + cax=cbar_ax, + extend=CBEXTEND, + extendfrac=0.0375, + drawedges=False, + orientation='horizontal', + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -419,155 +499,252 @@ def plot_grid(base_dir, FILENAMES, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=22, labelsize=18, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=22, labelsize=18, direction='in' + ) # adjust subplots within figure - fig.subplots_adjust(left=0.01, right=0.99, bottom=0.10, top=0.97, - hspace=0.1, wspace=0.05) + fig.subplots_adjust( + left=0.01, right=0.99, bottom=0.10, top=0.97, hspace=0.1, wspace=0.05 + ) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( - description=u"""Creates 5 GMT-like plots on a global Plate Carr\u00E9e + description="""Creates 5 GMT-like plots on a global Plate Carr\u00e9e (Equirectangular) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', nargs=5, - type=pathlib.Path, - help='Input grid files') + parser.add_argument( + 'infile', nargs=5, type=pathlib.Path, help='Input grid files' + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, nargs='+', - default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + nargs='+', + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', nargs=5, - type=str, help='Plot title') - parser.add_argument('--plot-label', nargs=5, - type=str, help='Plot label') + parser.add_argument('--plot-title', nargs=5, type=str, help='Plot title') + parser.add_argument('--plot-label', nargs=5, type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -577,7 +754,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -603,7 +782,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -611,6 +791,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_global_grid_9maps.py b/mapping/plot_global_grid_9maps.py index 064847bb..771ffcec 100644 --- a/mapping/plot_global_grid_9maps.py +++ b/mapping/plot_global_grid_9maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_9maps.py Written by Tyler Sutterley (05/2023) Creates 9 GMT-like plots in a Plate Carree (Equirectangular) projection @@ -46,6 +46,7 @@ Updated 11/2017: can plot a contour of the global average with MEAN Written 08/2017 """ + from __future__ import print_function import sys @@ -65,7 +66,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -73,23 +74,25 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -99,44 +102,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -164,11 +176,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -178,13 +190,14 @@ def plot_grid(base_dir, FILENAMES, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -193,21 +206,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -215,51 +228,67 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup Plate Carree projection - fig, ((ax[0],ax[1],ax[2]),(ax[3],ax[4],ax[5]),(ax[6],ax[7],ax[8])) = \ - plt.subplots(num=1, nrows=3, ncols=3, figsize=(10.375,7.125), - subplot_kw=dict(projection=projection)) + ( + fig, + ((ax[0], ax[1], ax[2]), (ax[3], ax[4], ax[5]), (ax[6], ax[7], ax[8])), + ) = plt.subplots( + num=1, + nrows=3, + ncols=3, + figsize=(10.375, 7.125), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -270,92 +299,132 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom(np.invert(img.mask), 5, order=1, output=bool) data.mask = np.invert(mask) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(data,reduce_clevs,colors='0.2',linestyles='solid', - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) - ax1.contour(data,[0],colors='red',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(dinput.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(dinput.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave = np.sum(area*dinput.data[indy,indx])/np.sum(area) + ave = np.sum(area * dinput.data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(data,[ave],colors='blue',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # draw coastlines plot_coastline(ax1, base_dir) @@ -365,26 +434,38 @@ def plot_grid(base_dir, FILENAMES, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=projection, draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=projection, + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) # add title for each subplot if TITLES is not None: TITLE = ' '.join(TITLES[i].split('_')) - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=18) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=18) ax1.title.set_y(0.99) # Add figure label for each subplot if LABELS is not None: - at = offsetbox.AnchoredText(LABELS[i], - loc=2, pad=0, borderpad=0.25, frameon=True, - prop=dict(size=18,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.1") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABELS[i], + loc=2, + pad=0, + borderpad=0.25, + frameon=True, + prop=dict(size=18, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.1') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # axis = equal @@ -402,8 +483,14 @@ def plot_grid(base_dir, FILENAMES, cbar_ax = fig.add_axes([0.105, 0.095, 0.81, 0.045]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False, orientation='horizontal') + cbar = fig.colorbar( + im, + cax=cbar_ax, + extend=CBEXTEND, + extendfrac=0.0375, + drawedges=False, + orientation='horizontal', + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -414,155 +501,252 @@ def plot_grid(base_dir, FILENAMES, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=22, labelsize=18, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=22, labelsize=18, direction='in' + ) # adjust subplots within figure - fig.subplots_adjust(left=0.01, right=0.99, bottom=0.15, top=0.98, - hspace=0.05, wspace=0.05) + fig.subplots_adjust( + left=0.01, right=0.99, bottom=0.15, top=0.98, hspace=0.05, wspace=0.05 + ) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( - description=u"""Creates 9 GMT-like plots on a global Plate Carr\u00E9e + description="""Creates 9 GMT-like plots on a global Plate Carr\u00e9e (Equirectangular) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', nargs=9, - type=pathlib.Path, - help='Input grid files') + parser.add_argument( + 'infile', nargs=9, type=pathlib.Path, help='Input grid files' + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, nargs='+', - default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + nargs='+', + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', nargs=9, - type=str, help='Plot title') - parser.add_argument('--plot-label', nargs=9, - type=str, help='Plot label') + parser.add_argument('--plot-title', nargs=9, type=str, help='Plot title') + parser.add_argument('--plot-label', nargs=9, type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:3.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:3.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -572,7 +756,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -598,7 +784,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -606,6 +793,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_global_grid_maps.py b/mapping/plot_global_grid_maps.py index afa9f76b..8fe8f966 100644 --- a/mapping/plot_global_grid_maps.py +++ b/mapping/plot_global_grid_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_maps.py Written by Tyler Sutterley (05/2023) Creates GMT-like plots in a Plate Carree (Equirectangular) projection @@ -47,6 +47,7 @@ Updated 02/2017: direction="in" for matplotlib2.0 color bar ticks Written 03/2016 """ + from __future__ import print_function import sys @@ -66,7 +67,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -74,23 +75,25 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -100,44 +103,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # plot grid program -def plot_grid(base_dir, FILENAME, +def plot_grid( + base_dir, + FILENAME, DATAFORM=None, VARIABLES=[], MASK=None, @@ -166,8 +178,8 @@ def plot_grid(base_dir, FILENAME, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -176,13 +188,14 @@ def plot_grid(base_dir, FILENAME, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -191,144 +204,209 @@ def plot_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAME, date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAME, + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAME, date=False, - field_mapping=field_mapping) - elif (DATAFORM == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAME, date=False, field_mapping=field_mapping + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAME, date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAME, date=False, field_mapping=field_mapping + ) # create masked array if missing values if MASK is not None: # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # setup Plate Carree projection - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(5.5,3.5), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(5.5, 3.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin, latsin, - data=img, order=order, iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom(np.invert(img.mask), 5, order=1, output=bool) data.mask = np.invert(mask) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(data,reduce_clevs,colors='0.2',linestyles='solid', - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) - ax1.contour(data,[0],colors='red',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(dinput.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(dinput.mask)) + area = (rad_e**2) * dth * dphi * np.cos(np.radians(lat[indy, indx])) # calculate average - ave = np.sum(area*dinput.data[indy,indx])/np.sum(area) + ave = np.sum(area * dinput.data[indy, indx]) / np.sum(area) # plot line contour of global average - ax1.contour(data,[ave],colors='blue',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # draw coastlines plot_coastline(ax1, base_dir) @@ -338,11 +416,18 @@ def plot_grid(base_dir, FILENAME, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=projection, draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=projection, + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -352,9 +437,17 @@ def plot_grid(base_dir, FILENAME, # options: neither, both, min, max # shrink = percent size of colorbar # aspect = lengthXwidth aspect of colorbar - cbar = plt.colorbar(im, ax=ax1, extend=CBEXTEND, - extendfrac=0.0375, orientation='horizontal', pad=0.025, - shrink=0.90, aspect=22, drawedges=False) + cbar = plt.colorbar( + im, + ax=ax1, + extend=CBEXTEND, + extendfrac=0.0375, + orientation='horizontal', + pad=0.025, + shrink=0.90, + aspect=22, + drawedges=False, + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -365,8 +458,9 @@ def plot_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=15, labelsize=13, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=15, labelsize=13, direction='in' + ) # axis = equal ax1.set_aspect('equal', adjustable='box') @@ -376,26 +470,39 @@ def plot_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=13) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=13) ax1.title.set_y(1.01) # Add figure label if LABEL is not None: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=13,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.2") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=13, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.2') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # Set Projection label if ADD_PROJECTION: - projection_text = u"Projection centered on 0.00\u00B0E" - ax1.annotate(projection_text, xy=(0.01,0.016), - xycoords='figure fraction', fontsize=7) + projection_text = 'Projection centered on 0.00\u00b0E' + ax1.annotate( + projection_text, + xy=(0.01, 0.016), + xycoords='figure fraction', + fontsize=7, + ) # Set Min-Max label if ADD_MINMAX: - text = f"Data Min = {data.min():0.1f}, Max = {data.max():0.1f}" - ax1.annotate(text, xy=(0.99,0.016), - xycoords='figure fraction', ha='right', fontsize=7) + text = f'Data Min = {data.min():0.1f}, Max = {data.max():0.1f}' + ax1.annotate( + text, + xy=(0.99, 0.016), + xycoords='figure fraction', + ha='right', + fontsize=7, + ) # stronger linewidth on frame ax1.spines['geo'].set_linewidth(2.0) @@ -403,155 +510,254 @@ def plot_grid(base_dir, FILENAME, ax1.spines['geo'].set_capstyle('projecting') # adjust subplot within figure - fig.subplots_adjust(left=0.04,right=0.96,bottom=0.05,top=0.96) + fig.subplots_adjust(left=0.04, right=0.96, bottom=0.05, top=0.96) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # save to file logging.info(str(FIGURE_FILE)) - plt.savefig(FIGURE_FILE, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, - dpi=FIGURE_DPI, format=FIGURE_FORMAT) + plt.savefig( + FIGURE_FILE, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, + dpi=FIGURE_DPI, + format=FIGURE_FORMAT, + ) plt.clf() # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( - description=u"""Creates GMT-like plots on a global Plate Carr\u00E9e + description="""Creates GMT-like plots on a global Plate Carr\u00e9e (Equirectangular) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', - type=pathlib.Path, - help='Input grid file') + parser.add_argument('infile', type=pathlib.Path, help='Input grid file') # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # variable names (for ascii names of columns) - parser.add_argument('--variables','-v', - type=str, nargs='+', default=['lon','lat','z'], - help='Variable names of data in input file') + parser.add_argument( + '--variables', + '-v', + type=str, + nargs='+', + default=['lon', 'lat', 'z'], + help='Variable names of data in input file', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', - type=str, help='Plot title') - parser.add_argument('--plot-label', - type=str, help='Plot label') + parser.add_argument('--plot-title', type=str, help='Plot title') + parser.add_argument('--plot-label', type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:0.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:0.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') - parser.add_argument('--add-projection', - default=False, action='store_true', - help='Add map projection label') - parser.add_argument('--add-min-max', - default=False, action='store_true', - help='Add label for data minimum and maximum') + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) + parser.add_argument( + '--add-projection', + default=False, + action='store_true', + help='Add map projection label', + ) + parser.add_argument( + '--add-min-max', + default=False, + action='store_true', + help='Add label for data minimum and maximum', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='png', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='png', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -561,7 +767,9 @@ def main(): try: info(args) # run plot program with parameters - plot_grid(args.directory, args.infile, + plot_grid( + args.directory, + args.infile, DATAFORM=args.format, VARIABLES=args.variables, DDEG=args.spacing, @@ -589,7 +797,8 @@ def main(): FIGURE_FILE=args.figure_file, FIGURE_FORMAT=args.figure_format, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -597,6 +806,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/mapping/plot_global_grid_movie.py b/mapping/plot_global_grid_movie.py index 0f11713f..0b212bd0 100644 --- a/mapping/plot_global_grid_movie.py +++ b/mapping/plot_global_grid_movie.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_maps.py Written by Tyler Sutterley (05/2023) Creates GMT-like animations in a Plate Carree (Equirectangular) projection @@ -47,6 +47,7 @@ Updated 02/2017: direction="in" for matplotlib2.0 color bar ticks Written 12/2015 """ + from __future__ import print_function import sys @@ -67,7 +68,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -76,23 +77,25 @@ import matplotlib.ticker as ticker import matplotlib.animation as animation import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -102,44 +105,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # animate grid program -def animate_grid(base_dir, FILENAME, +def animate_grid( + base_dir, + FILENAME, DATAFORM=None, MASK=None, INTERPOLATION=None, @@ -165,8 +177,8 @@ def animate_grid(base_dir, FILENAME, GRID=None, FIGURE_FILE=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -175,13 +187,14 @@ def animate_grid(base_dir, FILENAME, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -190,21 +203,21 @@ def animate_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file @@ -213,15 +226,25 @@ def animate_grid(base_dir, FILENAME, # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - dinput = gravtk.spatial().from_file(FILENAME, - format=DATAFORM, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + dinput = gravtk.spatial().from_file( + FILENAME, + format=DATAFORM, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) elif DATAFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,dataform = DATAFORM.split('-') - dinput = gravtk.spatial().from_index(FILENAME, - format=dataform, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + _, dataform = DATAFORM.split('-') + dinput = gravtk.spatial().from_index( + FILENAME, + format=dataform, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) # replace invalid with a new fill value dinput.replace_invalid(fill_value=FILL_VALUE) @@ -230,73 +253,93 @@ def animate_grid(base_dir, FILENAME, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # create movie writer objects FFMpegWriter = animation.writers['ffmpeg'] - metadata = dict(title=pathlib.Path(sys.argv[0]).name, artist='Matplotlib', - date_created=time.strftime('%Y-%m-%d',time.localtime())) + metadata = dict( + title=pathlib.Path(sys.argv[0]).name, + artist='Matplotlib', + date_created=time.strftime('%Y-%m-%d', time.localtime()), + ) # bitrate to be determined automatically by underlying utility - writer = FFMpegWriter(fps=8, metadata=metadata, bitrate=-1, - extra_args=['-vcodec','libx264']) + writer = FFMpegWriter( + fps=8, metadata=metadata, bitrate=-1, extra_args=['-vcodec', 'libx264'] + ) # setup Plate Carree projection - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(5.5,3.5), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(5.5, 3.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # only plot grounded points if MASK is not None: - mask = gravtk.tools.mask_oceans(lonsin,latsin,order=order) + mask = gravtk.tools.mask_oceans(lonsin, latsin, order=order) # add place holder for figure image - im = ax1.imshow(np.zeros((my,mx)), interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + np.zeros((my, mx)), + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # draw coastlines plot_coastline(ax1, base_dir) @@ -306,11 +349,18 @@ def animate_grid(base_dir, FILENAME, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=projection, draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=projection, + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -320,9 +370,17 @@ def animate_grid(base_dir, FILENAME, # options: neither, both, min, max # shrink = percent size of colorbar # aspect = lengthXwidth aspect of colorbar - cbar = plt.colorbar(im, ax=ax1, extend=CBEXTEND, - extendfrac=0.0375, orientation='horizontal', pad=0.025, - shrink=0.90, aspect=22, drawedges=False) + cbar = plt.colorbar( + im, + ax=ax1, + extend=CBEXTEND, + extendfrac=0.0375, + orientation='horizontal', + pad=0.025, + shrink=0.90, + aspect=22, + drawedges=False, + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -333,8 +391,9 @@ def animate_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=15, labelsize=13, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=15, labelsize=13, direction='in' + ) # axis = equal ax1.set_aspect('equal', adjustable='box') @@ -344,20 +403,33 @@ def animate_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=13) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=13) ax1.title.set_y(1.01) # Add figure label if LABEL is not None: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=13,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.2") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=13, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.2') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # add date label (year-calendar month e.g. 2002-01) - time_text = ax1.text(0.02, 0.015, '', transform=fig.transFigure, - color='k', size=18, ha='left', va='baseline', usetex=True) + time_text = ax1.text( + 0.02, + 0.015, + '', + transform=fig.transFigure, + color='k', + size=18, + ha='left', + va='baseline', + usetex=True, + ) # stronger linewidth on frame ax1.spines['geo'].set_linewidth(2.0) @@ -365,32 +437,54 @@ def animate_grid(base_dir, FILENAME, ax1.spines['geo'].set_capstyle('projecting') # adjust subplot within figure - fig.subplots_adjust(left=0.04,right=0.96,bottom=0.05,top=0.96) + fig.subplots_adjust(left=0.04, right=0.96, bottom=0.05, top=0.96) - # create output directory if non-existent + # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # replace data and contours to create movie frames # create image for each frame with writer.saving(fig, FIGURE_FILE, FIGURE_DPI): # for each input file - for t,gm in enumerate(dinput.month): + for t, gm in enumerate(dinput.month): # data for time t converted to a masked array subset = dinput.subset(gm) data = subset.to_masked_array() # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,data.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,data.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(data.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(data.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and ( + np.max(dinput.lon) > 180 + ): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, data.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, data.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + data.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + data.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # only plot grounded points @@ -402,38 +496,70 @@ def animate_grid(base_dir, FILENAME, # set data to image with transparency using normalization im.set_data(img) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom( + np.invert(img.mask), 5, order=1, output=bool + ) data.mask = np.invert(mask) # plot line contours contours = [] if CONTOURS and (np.sum(data**2) > 0): # plot line contours - contours.append(ax1.contour(data, reduce_clevs, - colors='0.2', linestyles='solid', - extent=(xmin,xmax,ymin,ymax), origin='lower', - transform=projection)) - contours.append(ax1.contour(data, [0], - colors='red', linestyles='solid', linewidths=1.5, - extent=(xmin,xmax,ymin,ymax), origin='lower', - transform=projection)) + contours.append( + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ) + contours.append( + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (np.radians(dlon), np.radians(dlat)) - indy,indx = np.nonzero(np.logical_not(subset.mask)) - area = (rad_e**2)*dth*dphi*np.cos(np.radians(lat[indy,indx])) + dphi, dth = (np.radians(dlon), np.radians(dlat)) + indy, indx = np.nonzero(np.logical_not(subset.mask)) + area = ( + (rad_e**2) + * dth + * dphi + * np.cos(np.radians(lat[indy, indx])) + ) # calculate average - ave = np.sum(area*subset.data[indy,indx])/np.sum(area) + ave = np.sum(area * subset.data[indy, indx]) / np.sum(area) # plot line contour of global average - contours.append(ax1.contour(data, [ave], - colors='blue', linestyles='solid', linewidths=1.5, - extent=(xmin,xmax,ymin,ymax), origin='lower', - transform=projection)) + contours.append( + ax1.contour( + data, + [ave], + colors='blue', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ) # add date label (year-calendar month e.g. 2002-01) year = np.floor(dinput.time[t]).astype(np.int64) - calendar_month = np.int64(((gm-1) % 12)+1) - date_label=r'\textbf{{{0:4d}--{1:02d}}}'.format(year,calendar_month) + calendar_month = np.int64(((gm - 1) % 12) + 1) + date_label = r'\textbf{{{0:4d}--{1:02d}}}'.format( + year, calendar_month + ) time_text.set_text(date_label) # add to movie writer.grab_frame() @@ -442,130 +568,210 @@ def animate_grid(base_dir, FILENAME, # change the permissions mode FIGURE_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( - description=u"""Creates GMT-like animations on a global Plate - Carr\u00E9e (Equirectangular) projection + description="""Creates GMT-like animations on a global Plate + Carr\u00e9e (Equirectangular) projection """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', - type=pathlib.Path, - help='Input grid file') + parser.add_argument('infile', type=pathlib.Path, help='Input grid file') # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', type=pathlib.Path, default=lsmask, help='Land-sea mask' + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Input grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Input grid interval (1: global, 2: centered global)'), + ) # Interpolation method - parser.add_argument('--interpolation','-I', - type=str, default='bilinear', choices=['nearest','bilinear','cubic'], - help='Interpolation method') + parser.add_argument( + '--interpolation', + '-I', + type=str, + default='bilinear', + choices=['nearest', 'bilinear', 'cubic'], + help='Interpolation method', + ) # scale factor - parser.add_argument('--scale-factor','-s', - type=float, default=1.0, - help='Multiplicative scale factor for converting to plot units') + parser.add_argument( + '--scale-factor', + '-s', + type=float, + default=1.0, + help='Multiplicative scale factor for converting to plot units', + ) # plot range - parser.add_argument('--plot-range','-R', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Plot range and step size for normalization') - parser.add_argument('--boundary','-B', - type=float, nargs='+', - help='Plot boundary for normalization') + parser.add_argument( + '--plot-range', + '-R', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Plot range and step size for normalization', + ) + parser.add_argument( + '--boundary', + '-B', + type=float, + nargs='+', + help='Plot boundary for normalization', + ) # color palette table or named color map try: cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) except (ValueError, NameError) as exc: cmap_set = [] - parser.add_argument('--colormap','-m', - metavar='COLORMAP', type=str, default='viridis', + parser.add_argument( + '--colormap', + '-m', + metavar='COLORMAP', + type=str, + default='viridis', choices=sorted(cmap_set), - help='Named Matplotlib colormap') - parser.add_argument('--cpt-file','-c', + help='Named Matplotlib colormap', + ) + parser.add_argument( + '--cpt-file', + '-c', type=pathlib.Path, - help='Input Color Palette Table (.cpt) file') + help='Input Color Palette Table (.cpt) file', + ) # color map alpha - parser.add_argument('--alpha','-a', - type=float, default=1.0, - help='Named Matplotlib colormap') + parser.add_argument( + '--alpha', + '-a', + type=float, + default=1.0, + help='Named Matplotlib colormap', + ) # plot contour parameters - parser.add_argument('--plot-contours', - default=False, action='store_true', - help='Plot contours') - parser.add_argument('--contour-range', - type=float, nargs=3, metavar=('MIN','MAX','STEP'), - help='Contour range and step size') - parser.add_argument('--mean-contour', - default=False, action='store_true', - help='Plot contours for mean of dataset') + parser.add_argument( + '--plot-contours', + default=False, + action='store_true', + help='Plot contours', + ) + parser.add_argument( + '--contour-range', + type=float, + nargs=3, + metavar=('MIN', 'MAX', 'STEP'), + help='Contour range and step size', + ) + parser.add_argument( + '--mean-contour', + default=False, + action='store_true', + help='Plot contours for mean of dataset', + ) # title and label - parser.add_argument('--plot-title', - type=str, help='Plot title') - parser.add_argument('--plot-label', - type=str, help='Plot label') + parser.add_argument('--plot-title', type=str, help='Plot title') + parser.add_argument('--plot-label', type=str, help='Plot label') # colorbar parameters - parser.add_argument('--cbextend', - type=str, default='both', + parser.add_argument( + '--cbextend', + type=str, + default='both', choices=['neither', 'both', 'min', 'max'], - help='Add extension triangles to colorbar') - parser.add_argument('--cbtitle', - type=str, default='', - help='Title label for colorbar') - parser.add_argument('--cbunits', - type=str, default='', - help='Units label for colorbar') - parser.add_argument('--cbformat', - type=str, default='{0:0.0f}', - help='Tick format for colorbar') + help='Add extension triangles to colorbar', + ) + parser.add_argument( + '--cbtitle', type=str, default='', help='Title label for colorbar' + ) + parser.add_argument( + '--cbunits', type=str, default='', help='Units label for colorbar' + ) + parser.add_argument( + '--cbformat', + type=str, + default='{0:0.0f}', + help='Tick format for colorbar', + ) # additional parameters - parser.add_argument('--draw-grid-lines', - default=False, action='store_true', - help='Add map grid lines') - parser.add_argument('--grid-lines', - type=float, nargs='+', default=(15,15), - help='Input grid spacing for meridians and parallels') + parser.add_argument( + '--draw-grid-lines', + default=False, + action='store_true', + help='Add map grid lines', + ) + parser.add_argument( + '--grid-lines', + type=float, + nargs='+', + default=(15, 15), + help='Input grid spacing for meridians and parallels', + ) # output file, format and dpi - parser.add_argument('--figure-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') + parser.add_argument( + '--figure-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -575,7 +781,9 @@ def main(): try: info(args) # run plot program with parameters - animate_grid(args.directory, args.infile, + animate_grid( + args.directory, + args.infile, DATAFORM=args.format, DDEG=args.spacing, INTERVAL=args.interval, @@ -599,7 +807,8 @@ def main(): GRID=args.grid_lines, FIGURE_FILE=args.figure_file, FIGURE_DPI=args.figure_dpi, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -607,6 +816,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/calc_SLR_RMS.py b/scripts/calc_SLR_RMS.py index 6cc48de9..c38fa155 100644 --- a/scripts/calc_SLR_RMS.py +++ b/scripts/calc_SLR_RMS.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" calc_SLR_RMS.py (05/2023) Reads low-degree zonal harmonics from Satellite Laser Ranging (SLR) and estimates the uncertainty as the RMS off the monthly mean field @@ -29,6 +29,7 @@ Updated 12/2021: can use variable loglevels for verbose output Written 11/2021 """ + from __future__ import print_function, division import sys @@ -38,25 +39,27 @@ import numpy as np import gravity_toolkit as gravtk -# PURPOSE: read SLR low-degree coefficients and calculate RMS -def calc_SLR_RMS(base_dir, START_MON, END_MON, MISSING, DATAFORM=None, - MODE=0o775): +# PURPOSE: read SLR low-degree coefficients and calculate RMS +def calc_SLR_RMS( + base_dir, START_MON, END_MON, MISSING, DATAFORM=None, MODE=0o775 +): # directory setup base_dir = pathlib.Path(base_dir).expanduser().absolute() # output data file format suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # GRACE/GRACE-FO mission gap - GAP = [187,188,189,190,191,192,193,194,195,196,197] + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] missing = sorted(set(MISSING) | set(GAP)) # CSR 5x5 monthly harmonics SLR_file = base_dir.joinpath('CSR_Monthly_5x5_Gravity_Harmonics.txt') CSR55 = gravtk.read_SLR_harmonics(SLR_file, HEADER=True) # converting from MJD into month, day and year to calculate GRACE month - YY,MM,*_ = gravtk.time.convert_julian(CSR55['MJD'] + 2400000.5, - format='tuple') - CSR55['month'] = gravtk.time.calendar_to_grace(YY,month=MM) + YY, MM, *_ = gravtk.time.convert_julian( + CSR55['MJD'] + 2400000.5, format='tuple' + ) + CSR55['month'] = gravtk.time.calendar_to_grace(YY, month=MM) CSR55['month'] = gravtk.time.adjust_months(CSR55['month']) # CSR TN-11 coefficients # SLR_file = base_dir.joinpath('TN-11_C20_SLR.txt') @@ -74,17 +77,29 @@ def calc_SLR_RMS(base_dir, START_MON, END_MON, MISSING, DATAFORM=None, # GFZ GravIS coefficients SLR_file = base_dir.joinpath('GFZ_RL06_C20_SLR.dat') GFZ_C20 = gravtk.SLR.C20(SLR_file) - SLR_file = base_dir.joinpath('GRAVIS-2B_GFZOP_GRACE+SLR_LOW_DEGREES_0002.dat') + SLR_file = base_dir.joinpath( + 'GRAVIS-2B_GFZOP_GRACE+SLR_LOW_DEGREES_0002.dat' + ) GFZ_C30 = gravtk.SLR.C30(SLR_file) GFZ_CS21 = gravtk.SLR.CS2(SLR_file) # GFZ GRACE coefficients to d/o 5 - GFZ55 = gravtk.grace_input_months(base_dir, 'GFZ', 'RL06', 'GSM', 5, - START_MON, END_MON, missing, None, None) + GFZ55 = gravtk.grace_input_months( + base_dir, + 'GFZ', + 'RL06', + 'GSM', + 5, + START_MON, + END_MON, + missing, + None, + None, + ) # calculate common months - month = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + month = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) common_months = np.copy(month) - for v in [CSR55,GSFC_C30,GFZ_C30]: + for v in [CSR55, GSFC_C30, GFZ_C30]: common_months = sorted(set(common_months) & set(v['month'])) # number of common months nt = len(common_months) @@ -96,77 +111,87 @@ def calc_SLR_RMS(base_dir, START_MON, END_MON, MISSING, DATAFORM=None, variance_Ylms = gravtk.harmonics().zeros(lmax=5, mmax=5, nt=nt) variance_Ylms.month[:] = np.copy(common_months) # for each set of parameters - parameters = [('clm',2,0),('clm',3,0), - ('clm',4,0),('clm',5,0), - ('clm',2,1),('slm',2,1), - ('clm',2,2),('slm',2,2)] + parameters = [ + ('clm', 2, 0), + ('clm', 3, 0), + ('clm', 4, 0), + ('clm', 5, 0), + ('clm', 2, 1), + ('slm', 2, 1), + ('clm', 2, 2), + ('slm', 2, 2), + ] # parameters for degree and order for a, (cs, l, m) in enumerate(parameters): - if (a == 0): + if a == 0: # C20 - centers = [CSR_C20,GSFC_C20,GFZ_C20] - elif (a == 1): + centers = [CSR_C20, GSFC_C20, GFZ_C20] + elif a == 1: # C30 - CSR55['data'] = CSR55[cs][l,m,:].copy() - centers = [CSR55,GSFC_C30,GFZ_C30] - elif (a == 2): + CSR55['data'] = CSR55[cs][l, m, :].copy() + centers = [CSR55, GSFC_C30, GFZ_C30] + elif a == 2: # C40 - CSR55['data'] = CSR55[cs][l,m,:].copy() - GFZ55['data'] = GFZ55[cs][l,m,:].copy() - centers = [CSR55,GSFC_C40,GFZ55] - elif (a == 3): + CSR55['data'] = CSR55[cs][l, m, :].copy() + GFZ55['data'] = GFZ55[cs][l, m, :].copy() + centers = [CSR55, GSFC_C40, GFZ55] + elif a == 3: # C50 - CSR55['data'] = CSR55[cs][l,m,:].copy() - GFZ55['data'] = GFZ55[cs][l,m,:].copy() - centers = [CSR55,GSFC_C50,GFZ55] - elif (a == 4): + CSR55['data'] = CSR55[cs][l, m, :].copy() + GFZ55['data'] = GFZ55[cs][l, m, :].copy() + centers = [CSR55, GSFC_C50, GFZ55] + elif a == 4: # C21 - CSR55['data'] = CSR55[cs][l,m,:].copy() + CSR55['data'] = CSR55[cs][l, m, :].copy() GSFC_CS21['data'] = GSFC_CS21['C2m'].copy() GFZ_CS21['data'] = GFZ_CS21['C2m'].copy() - centers = [CSR55,GSFC_CS21,GFZ_CS21] - elif (a == 5): + centers = [CSR55, GSFC_CS21, GFZ_CS21] + elif a == 5: # S21 - CSR55['data'] = CSR55[cs][l,m,:].copy() + CSR55['data'] = CSR55[cs][l, m, :].copy() GSFC_CS21['data'] = GSFC_CS21['S2m'].copy() GFZ_CS21['data'] = GFZ_CS21['S2m'].copy() - centers = [CSR55,GFZ_CS21,GFZ_CS21] - elif (a == 6): + centers = [CSR55, GFZ_CS21, GFZ_CS21] + elif a == 6: # C22 - CSR55['data'] = CSR55[cs][l,m,:].copy() + CSR55['data'] = CSR55[cs][l, m, :].copy() GSFC_CS22['data'] = GSFC_CS22['C2m'].copy() - GFZ55['data'] = GFZ55[cs][l,m,:].copy() - centers = [CSR55,GSFC_CS22,GFZ55] - elif (a == 7): + GFZ55['data'] = GFZ55[cs][l, m, :].copy() + centers = [CSR55, GSFC_CS22, GFZ55] + elif a == 7: # S22 - CSR55['data'] = CSR55[cs][l,m,:].copy() + CSR55['data'] = CSR55[cs][l, m, :].copy() GSFC_CS22['data'] = GSFC_CS22['S2m'].copy() - GFZ55['data'] = GFZ55[cs][l,m,:].copy() - centers = [CSR55,GSFC_CS22,GFZ55] + GFZ55['data'] = GFZ55[cs][l, m, :].copy() + centers = [CSR55, GSFC_CS22, GFZ55] # calculate the mean field for degree and order - for i,v in enumerate(centers): - ii = [i for i,m in enumerate(v['month']) if m in common_months] - tmp = v['data'][ii]-v['data'][ii].mean() + for i, v in enumerate(centers): + ii = [i for i, m in enumerate(v['month']) if m in common_months] + tmp = v['data'][ii] - v['data'][ii].mean() mean_Ylms.time[:] = v['time'][ii].copy() - if (cs == 'clm'): - mean_Ylms.clm[l,m,:] += tmp - elif (cs == 'slm'): - mean_Ylms.slm[l,m,:] += tmp + if cs == 'clm': + mean_Ylms.clm[l, m, :] += tmp + elif cs == 'slm': + mean_Ylms.slm[l, m, :] += tmp # calculate variance off mean harmonics for degree and order - for i,v in enumerate(centers): - ii = [i for i,m in enumerate(v['month']) if m in common_months] - tmp = v['data'][ii]-v['data'][ii].mean() + for i, v in enumerate(centers): + ii = [i for i, m in enumerate(v['month']) if m in common_months] + tmp = v['data'][ii] - v['data'][ii].mean() variance_Ylms.time[:] = v['time'][ii].copy() - if (cs == 'clm'): - variance_Ylms.clm[l,m,:] += (tmp - mean_Ylms.clm[l,m,:]/3.0)**2 - elif (cs == 'slm'): - variance_Ylms.slm[l,m,:] += (tmp - mean_Ylms.slm[l,m,:]/3.0)**2 + if cs == 'clm': + variance_Ylms.clm[l, m, :] += ( + tmp - mean_Ylms.clm[l, m, :] / 3.0 + ) ** 2 + elif cs == 'slm': + variance_Ylms.slm[l, m, :] += ( + tmp - mean_Ylms.slm[l, m, :] / 3.0 + ) ** 2 # calculate mean of harmonics - mean_Ylms = mean_Ylms.scale(1.0/3.0) - variance_Ylms = variance_Ylms.scale(1.0/3.0).power(0.5) + mean_Ylms = mean_Ylms.scale(1.0 / 3.0) + variance_Ylms = variance_Ylms.scale(1.0 / 3.0).power(0.5) # calculate RMS RMS_Ylms = gravtk.harmonics().zeros(lmax=5, mmax=5) RMS_Ylms.time = np.mean(variance_Ylms.time) @@ -174,7 +199,7 @@ def calc_SLR_RMS(base_dir, START_MON, END_MON, MISSING, DATAFORM=None, for Ylms in variance_Ylms: RMS_Ylms.add(Ylms.power(2.0)) # convert from variance to RMS - RMS_Ylms = RMS_Ylms.scale(1.0/nt).power(0.5) + RMS_Ylms = RMS_Ylms.scale(1.0 / nt).power(0.5) # attributes for output files attributes = {} attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' @@ -185,6 +210,7 @@ def calc_SLR_RMS(base_dir, START_MON, END_MON, MISSING, DATAFORM=None, # change the permissions mode FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -194,50 +220,109 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series') - parser.add_argument('--end','-E', - type=int, default=231, - help='Ending GRACE/GRACE-FO month for time series') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167,172, - 177,178,182,200,201] - parser.add_argument('--missing', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months in time series') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=231, + help='Ending GRACE/GRACE-FO month for time series', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 200, + 201, + ] + parser.add_argument( + '--missing', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months in time series', + ) # output data format - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=('ascii','netCDF4','HDF5'), - help='Output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=('ascii', 'netCDF4', 'HDF5'), + help='Output data format', + ) # print information about each output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permission mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permission mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger for verbosity level loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] logging.basicConfig(level=loglevels[args.verbose]) # run program with parameters - calc_SLR_RMS(args.directory, args.start, args.end, args.missing, - DATAFORM=args.format, MODE=args.mode) + calc_SLR_RMS( + args.directory, + args.start, + args.end, + args.missing, + DATAFORM=args.format, + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/scripts/calc_harmonic_resolution.py b/scripts/calc_harmonic_resolution.py index ed32d69a..a720e351 100755 --- a/scripts/calc_harmonic_resolution.py +++ b/scripts/calc_harmonic_resolution.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" calc_harmonic_resolution.py Written by Tyler Sutterley (04/2022) @@ -36,9 +36,11 @@ Updated 08/2013: changed SPH_CAP option to (Y/N) Written 01/2013 """ + import argparse import numpy as np + # PURPOSE: Calculates minimum spatial resolution that can be resolved # from spherical harmonics of a maximum degree def calc_harmonic_resolution(LMAX, RADIUS=6371.0008, SPH_CAP=False): @@ -59,39 +61,56 @@ def calc_harmonic_resolution(LMAX, RADIUS=6371.0008, SPH_CAP=False): # Smallest diameter of a spherical cap that can be resolved by the # harmonics. Size of the smallest bump, half-wavelength, which can # be produced by the clm/slm - psi_min = 4.0*RADIUS*np.arcsin(1.0/(LMAX+1.0)) + psi_min = 4.0 * RADIUS * np.arcsin(1.0 / (LMAX + 1.0)) else: # Shortest half-wavelength that can be resolved by the clm/slm # This estimation is based on the number of possible zeros along # the equator - psi_min = np.pi*RADIUS/LMAX + psi_min = np.pi * RADIUS / LMAX return psi_min + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser() - parser.add_argument('--lmax','-l', metavar='LMAX', - type=int, nargs='+', - help='maximum degree of spherical harmonics') - parser.add_argument('--radius','-R', - type=float, default=6371.0008, - help='Average radius of the Earth in kilometers') - parser.add_argument('--cap','-C', - default=False, action='store_true', - help='Calculate smallest possible bump that can be resolved') + parser.add_argument( + '--lmax', + '-l', + metavar='LMAX', + type=int, + nargs='+', + help='maximum degree of spherical harmonics', + ) + parser.add_argument( + '--radius', + '-R', + type=float, + default=6371.0008, + help='Average radius of the Earth in kilometers', + ) + parser.add_argument( + '--cap', + '-C', + default=False, + action='store_true', + help='Calculate smallest possible bump that can be resolved', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # for each entered spherical harmonic degree for LMAX in args.lmax: - psi_min = calc_harmonic_resolution(LMAX, - RADIUS=args.radius, SPH_CAP=args.cap) - print('{0:5d}: {1:0.4f} km'.format(LMAX,psi_min)) + psi_min = calc_harmonic_resolution( + LMAX, RADIUS=args.radius, SPH_CAP=args.cap + ) + print('{0:5d}: {1:0.4f} km'.format(LMAX, psi_min)) + # run main program if __name__ == '__main__': diff --git a/scripts/calc_mascon.py b/scripts/calc_mascon.py index 560afbb5..525cb0db 100644 --- a/scripts/calc_mascon.py +++ b/scripts/calc_mascon.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" calc_mascon.py Written by Tyler Sutterley (05/2023) @@ -247,6 +247,7 @@ Updated 02/2012: Added sensitivity kernels Written 02/2012 """ + from __future__ import print_function, division import sys @@ -261,6 +262,7 @@ import scipy.linalg import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -270,9 +272,16 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate a regional time-series through a least # squares mascon process -def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, +def calc_mascon( + base_dir, + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -309,8 +318,8 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, RECONSTRUCT_FILE=None, LANDMASK=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # directory setup base_dir = pathlib.Path(base_dir).expanduser().absolute() # recursively create output directory if not currently existing @@ -329,8 +338,9 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, parser = re.compile(r'^(?!\#|\%|$)', re.VERBOSE) # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) @@ -348,19 +358,18 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, order_str = f'M{MMAX:d}' if (MMAX != LMAX) else '' # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) gw_str = '' # Read Ocean function and convert to Ylms for redistribution - if (REDISTRIBUTE_MASCONS | REDISTRIBUTE_REMOVED): + if REDISTRIBUTE_MASCONS | REDISTRIBUTE_REMOVED: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) ocean_str = '_OCN' else: # not distributing uniformly over ocean @@ -370,18 +379,36 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # 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 = gravtk.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_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + Ylms = gravtk.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_C40=SLR_C40, + 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 = gravtk.harmonics().from_dict(Ylms) # 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 = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GRACE_Ylms.subtract(mean_Ylms) else: @@ -395,7 +422,9 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # using standard GRACE/GRACE-FO harmonics ds_str = '' # full path to directory for specific GRACE/GRACE-FO product - GRACE_Ylms.directory = pathlib.Path(Ylms['directory']).expanduser().absolute() + GRACE_Ylms.directory = ( + pathlib.Path(Ylms['directory']).expanduser().absolute() + ) # date information of GRACE/GRACE-FO coefficients n_files = len(GRACE_Ylms.time) @@ -415,35 +444,37 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, 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) + 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'): + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - Ylms = gravtk.harmonics().from_file(REMOVE_FILE, - format=REMOVEFORM) - elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'): + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,removeform = REMOVEFORM.split('-') + _, removeform = REMOVEFORM.split('-') # index containing files in data format - Ylms = gravtk.harmonics().from_index(REMOVE_FILE, - format=removeform) + Ylms = gravtk.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) + 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] + 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 + 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] + 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() @@ -458,14 +489,17 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, construct_Ylms.month[:] = np.copy(GRACE_Ylms.month) if RECONSTRUCT: # input index for reconstructed spherical harmonic datafiles - RECONSTRUCT_FILE = pathlib.Path(RECONSTRUCT_FILE).expanduser().absolute() + RECONSTRUCT_FILE = ( + pathlib.Path(RECONSTRUCT_FILE).expanduser().absolute() + ) with RECONSTRUCT_FILE.open(mode='r', encoding='utf8') as f: file_list = [l for l in f.read().splitlines() if parser.match(l)] # for each valid file in the index (iterate over mascons) for reconstruct_file in file_list: # read reconstructed spherical harmonics - Ylms = gravtk.harmonics().from_file(reconstruct_file, - format=DATAFORM) + Ylms = gravtk.harmonics().from_file( + reconstruct_file, format=DATAFORM + ) # truncate clm and slm matrices to LMAX/MMAX # add harmonics object to total construct_Ylms.add(Ylms.truncate(lmax=LMAX, mmax=MMAX)) @@ -490,24 +524,25 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, mascon_name = [] # for each valid file in the index (iterate over mascons) mascon_list = [] - for k,fi in enumerate(mascon_files): + for k, fi in enumerate(mascon_files): # read mascon spherical harmonics - Ylms = gravtk.harmonics().from_file(fi, - format=MASCON_FORMAT, date=False) + Ylms = gravtk.harmonics().from_file( + fi, format=MASCON_FORMAT, 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 + 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] + 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 + 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] + 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)) # stem is the mascon file without directory or suffix @@ -522,8 +557,18 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # calculating GRACE/GRACE-FO error (Wahr et al. 2006) # output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) - fargs = (PROC,DREL,DSET,LMAX,order_str,ds_str,atm_str,GRACE_Ylms.month[0], - GRACE_Ylms.month[-1], suffix[DATAFORM]) + fargs = ( + 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 = GRACE_Ylms.directory.joinpath(delta_format.format(*fargs)) # check full path of the GRACE directory for delta file @@ -535,38 +580,41 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # Delta coefficients of GRACE time series (Error components) delta_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - delta_Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - delta_Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + 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'))): + 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 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 + 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']): + 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 = gravtk.time_series.smooth(GRACE_Ylms.time, - val1[l,m,:], HFWTH=HFWTH) + smth = gravtk.time_series.smooth( + GRACE_Ylms.time, val1[l, m, :], HFWTH=HFWTH + ) # number of smoothed points nsmth = len(smth['data']) tsmth = np.mean(smth['time']) # GRACE/GRACE-FO delta Ylms # variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) - val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth) + val2[l, m] = np.sqrt(np.sum(smth['noise'] ** 2) / nsmth) # attributes for output files attributes = {} attributes['title'] = 'GRACE/GRACE-FO Spherical Harmonic Errors' - attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # save GRACE/GRACE-FO delta harmonics to file delta_Ylms.time = np.copy(tsmth) delta_Ylms.month = np.int64(nsmth) @@ -577,8 +625,7 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, output_files.append(DELTA_FILE) else: # read GRACE/GRACE-FO delta harmonics from file - delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, - format=DATAFORM) + delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, format=DATAFORM) # 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) @@ -586,7 +633,9 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # 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)) + n_harm = np.int64( + LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX) ** 2 - (LMAX - MMAX) + ) # Initialing harmonics for least squares fitting # mascon kernel @@ -614,7 +663,7 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # Creating column array of clm/slm coefficients # Order is [C00...C6060,S11...S6060] # Switching between Cosine and Sine Stokes - for cs,csharm in enumerate(['clm','slm']): + for cs, csharm in enumerate(['clm', 'slm']): # copy cosine and sin harmonics mascon_harm = getattr(mascon_Ylms, csharm) grace_harm = getattr(GRACE_Ylms, csharm) @@ -624,72 +673,87 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, delta_harm = getattr(delta_Ylms, csharm) # for each spherical harmonic degree # +1 to include LMAX - for l in range(LMIN,LMAX+1): + for l in range(LMIN, LMAX + 1): # for each spherical harmonic order # Sine Stokes for (m=0) = 0 - mm = np.min([MMAX,l]) + mm = np.min([MMAX, l]) # +1 to include l or MMAX (whichever is smaller) - for m in range(cs,mm+1): + for m in range(cs, mm + 1): # Mascon Spherical Harmonics - M_lm[ii,:] = np.copy(mascon_harm[l,m,:]) + 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,:] + 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]) + 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 + LOVE.kl[l]) + fact[ii] = (2.0 * l + 1.0) / (1.0 + LOVE.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): + 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 - elif (FIT_METHOD == 2): + MA_lm[i, :] = M_lm[i, :] * wt_lm[i] * fact[i] + fit_factor = wt_lm * fact + elif FIT_METHOD == 2: # 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)) + 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] + kern_i[i] = 1.0 * fit_factor[i] # spherical harmonics solution for the # mascon sensitivity kernels - if (SOLVER == 'inv'): + if SOLVER == 'inv': kern_lm = np.dot(np.linalg.inv(MA_lm), kern_i) - elif (SOLVER == 'lstsq'): + elif SOLVER == 'lstsq': kern_lm = np.linalg.lstsq(MA_lm, kern_i, rcond=-1)[0] elif SOLVER in ('gelsd', 'gelsy', 'gelss'): - kern_lm, res, rnk, s = scipy.linalg.lstsq(MA_lm, kern_i, - lapack_driver=SOLVER) + kern_lm, res, rnk, s = scipy.linalg.lstsq( + MA_lm, kern_i, lapack_driver=SOLVER + ) # calculate the sensitivity kernel for each mascon for k in range(n_mas): - A_lm[i,k] = kern_lm[k]*total_area[k] + 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 + 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 - fargs = (mascon_name[k], dset_str, gia_str.upper(), atm_str, ocean_str, - LMAX, order_str, gw_str, ds_str, construct_str) + fargs = ( + mascon_name[k], + dset_str, + gia_str.upper(), + atm_str, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + construct_str, + ) file_format = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}{9}.txt' output_file = OUTPUT_DIRECTORY.joinpath(file_format.format(*fargs)) @@ -700,14 +764,20 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, fid = output_file.open(mode='w', encoding='utf8') # 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): + 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 + 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) + 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() @@ -719,10 +789,11 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # return the list of output files return output_files + # PURPOSE: print a file log for the GRACE mascon analysis def output_log_file(input_arguments, output_files): # format: calc_mascon_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'calc_mascon_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -739,10 +810,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE mascon analysis def output_error_log_file(input_arguments): # format: calc_mascon_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'calc_mascon_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -758,6 +830,7 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -765,76 +838,165 @@ def arguments(): through a least-squares mascon procedure from GRACE/GRACE-FO time-variable gravity data """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # Data processing center or satellite mission - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # minimum spherical harmonic degree - parser.add_argument('--lmin', - type=int, default=1, - help='Minimum spherical harmonic degree') + parser.add_argument( + '--lmin', type=int, default=1, help='Minimum spherical harmonic degree' + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -850,21 +1012,32 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # correct for pole tide drift follow Wahr et al. (2015) - parser.add_argument('--pole-tide', - default=False, action='store_true', - help='Correct for pole tide drift') + parser.add_argument( + '--pole-tide', + default=False, + action='store_true', + help='Correct for pole tide drift', + ) # Update Degree 1 coefficients with SLR or derived values # Tellus: GRACE/GRACE-FO TN-13 from PO.DAAC # https://grace.jpl.nasa.gov/data/get-data/geocenter/ @@ -876,114 +1049,205 @@ def arguments(): # https://doi.org/10.1029/2007JB005338 # GFZ: GRACE/GRACE-FO coefficients from GFZ GravIS # http://gravis.gfz-potsdam.de/corrections - parser.add_argument('--geocenter', - metavar='DEG1', type=str, - choices=['Tellus','SLR','SLF','UCI','Swenson','GFZ'], - help='Update Degree 1 coefficients with SLR or derived values') - parser.add_argument('--geocenter-file', + parser.add_argument( + '--geocenter', + metavar='DEG1', + type=str, + choices=['Tellus', 'SLR', 'SLF', 'UCI', 'Swenson', 'GFZ'], + help='Update Degree 1 coefficients with SLR or derived values', + ) + parser.add_argument( + '--geocenter-file', type=pathlib.Path, - help='Specific geocenter file if not default') - parser.add_argument('--interpolate-geocenter', - default=False, action='store_true', - help='Least-squares model missing Degree 1 coefficients') + help='Specific geocenter file if not default', + ) + parser.add_argument( + '--interpolate-geocenter', + default=False, + action='store_true', + help='Least-squares model missing Degree 1 coefficients', + ) # replace low degree harmonics with values from Satellite Laser Ranging - parser.add_argument('--slr-c20', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C20 coefficients with SLR values') - parser.add_argument('--slr-21', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C21 and S21 coefficients with SLR values') - parser.add_argument('--slr-22', - type=str, default=None, choices=['CSR','GSFC'], - help='Replace C22 and S22 coefficients with SLR values') - parser.add_argument('--slr-c30', - type=str, default=None, choices=['CSR','GFZ','GSFC','LARES'], - help='Replace C30 coefficients with SLR values') - parser.add_argument('--slr-c40', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C40 coefficients with SLR values') - parser.add_argument('--slr-c50', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C50 coefficients with SLR values') + parser.add_argument( + '--slr-c20', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C20 coefficients with SLR values', + ) + parser.add_argument( + '--slr-21', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C21 and S21 coefficients with SLR values', + ) + parser.add_argument( + '--slr-22', + type=str, + default=None, + choices=['CSR', 'GSFC'], + help='Replace C22 and S22 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c30', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC', 'LARES'], + help='Replace C30 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c40', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C40 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c50', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C50 coefficients with SLR values', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format for auxiliary files') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format for auxiliary files', + ) # mean file to remove - parser.add_argument('--mean-file', + parser.add_argument( + '--mean-file', type=pathlib.Path, - help='GRACE/GRACE-FO mean file to remove from the harmonic data') + help='GRACE/GRACE-FO mean file to remove from the harmonic data', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--mean-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5','gfc'], - help='Input data format for GRACE/GRACE-FO mean file') + parser.add_argument( + '--mean-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5', 'gfc'], + help='Input data format for GRACE/GRACE-FO mean file', + ) # mascon index file and parameters - parser.add_argument('--mascon-file', + parser.add_argument( + '--mascon-file', type=pathlib.Path, - help='Index file of mascons spherical harmonics') - parser.add_argument('--mascon-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format for mascon files') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + help='Index file of mascons spherical harmonics', + ) + parser.add_argument( + '--mascon-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format for mascon files', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # 1: mass coefficients # 2: geoid coefficients - parser.add_argument('--fit-method', - type=int, default=1, choices=(1,2), - help='Method for fitting sensitivity kernel to harmonics') + parser.add_argument( + '--fit-method', + type=int, + default=1, + choices=(1, 2), + help='Method for fitting sensitivity kernel to harmonics', + ) # least squares solver - choices = ('inv','lstsq','gelsd', 'gelsy', 'gelss') - parser.add_argument('--solver','-s', - type=str, default='lstsq', choices=choices, - help='Least squares solver for sensitivity kernel solutions') + choices = ('inv', 'lstsq', 'gelsd', 'gelsy', 'gelss') + parser.add_argument( + '--solver', + '-s', + type=str, + default='lstsq', + choices=choices, + help='Least squares solver for sensitivity kernel solutions', + ) # monthly files to be removed from the GRACE/GRACE-FO data - parser.add_argument('--remove-file', - type=pathlib.Path, nargs='+', - help='Monthly files to be removed from the GRACE/GRACE-FO data') + parser.add_argument( + '--remove-file', + type=pathlib.Path, + nargs='+', + help='Monthly files to be removed from the GRACE/GRACE-FO data', + ) choices = [] - choices.extend(['ascii','netCDF4','HDF5']) - choices.extend(['index-ascii','index-netCDF4','index-HDF5']) - parser.add_argument('--remove-format', - type=str, nargs='+', choices=choices, - help='Input data format for files to be removed') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + choices.extend(['ascii', 'netCDF4', 'HDF5']) + choices.extend(['index-ascii', 'index-netCDF4', 'index-HDF5']) + parser.add_argument( + '--remove-format', + type=str, + nargs='+', + choices=choices, + help='Input data format for files to be removed', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # mascon reconstruct parameters - parser.add_argument('--remove-reconstruct', - default=False, action='store_true', - help='Remove reconstructed mascon time series fields') - parser.add_argument('--reconstruct-file', + parser.add_argument( + '--remove-reconstruct', + default=False, + action='store_true', + help='Remove reconstructed mascon time series fields', + ) + parser.add_argument( + '--reconstruct-file', type=pathlib.Path, - help='Reconstructed mascon time series file to be removed') + help='Reconstructed mascon time series file to be removed', + ) # land-sea mask for redistributing mascon mass and land water flux - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing mascon mass and land water flux') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing mascon mass and land water flux', + ) # Output log file for each job in forms # calc_mascon_run_2002-04-01_PID-00000.log # calc_mascon_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -1036,18 +1300,20 @@ def main(): RECONSTRUCT_FILE=args.reconstruct_file, LANDMASK=args.mask, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/calc_sensitivity_kernel.py b/scripts/calc_sensitivity_kernel.py index 9bae8abe..c6e9e215 100644 --- a/scripts/calc_sensitivity_kernel.py +++ b/scripts/calc_sensitivity_kernel.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" calc_sensitivity_kernel.py Written by Tyler Sutterley (05/2023) @@ -145,6 +145,7 @@ Updated 03/2012: edited to use new gen_stokes time-series option Written 02/2012 """ + from __future__ import print_function, division import sys @@ -159,6 +160,7 @@ import scipy.linalg import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -168,9 +170,12 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate a regional time-series through a least # squares mascon process -def calc_sensitivity_kernel(LMAX, RAD, +def calc_sensitivity_kernel( + LMAX, + RAD, LMIN=None, MMAX=None, LOVE_NUMBERS=0, @@ -186,8 +191,8 @@ def calc_sensitivity_kernel(LMAX, RAD, INTERVAL=None, BOUNDS=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # file information suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')[DATAFORM] # file parser for reading index files @@ -204,8 +209,9 @@ def calc_sensitivity_kernel(LMAX, RAD, output_files = [] # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) @@ -219,19 +225,18 @@ def calc_sensitivity_kernel(LMAX, RAD, order_str = f'M{MMAX:d}' if (MMAX != LMAX) else '' # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' else: # else = 1 - wt = np.ones((LMAX+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 = gravtk.ocean_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) ocean_str = '_OCN' else: # not distributing uniformly over ocean @@ -249,24 +254,23 @@ def calc_sensitivity_kernel(LMAX, RAD, mascon_name = [] # for each valid file in the index (iterate over mascons) mascon_list = [] - for k,fi in enumerate(mascon_files): + for k, fi in enumerate(mascon_files): # read mascon spherical harmonics - Ylms = gravtk.harmonics().from_file(fi, - format=DATAFORM, date=False) + Ylms = gravtk.harmonics().from_file(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 + 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] + 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 + 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] + 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)) # stem is the mascon file without directory or suffix @@ -281,7 +285,9 @@ def calc_sensitivity_kernel(LMAX, RAD, # 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)) + n_harm = np.int64( + LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX) ** 2 - (LMAX - MMAX) + ) # Initialing harmonics for least squares fitting # mascon kernel @@ -303,42 +309,42 @@ def calc_sensitivity_kernel(LMAX, RAD, # Creating column array of clm/slm coefficients # Order is [C00...C6060,S11...S6060] # Switching between Cosine and Sine Stokes - for cs,csharm in enumerate(['clm','slm']): + 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 l in range(LMIN, LMAX + 1): # for each spherical harmonic order # Sine Stokes for (m=0) = 0 - mm = np.min([MMAX,l]) + mm = np.min([MMAX, l]) # +1 to include l or MMAX (whichever is smaller) - for m in range(cs,mm+1): + for m in range(cs, mm + 1): # Mascon Spherical Harmonics - M_lm[ii,:] = np.copy(mascon_harm[l,m,:]) + 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 + LOVE.kl[l]) + fact[ii] = (2.0 * l + 1.0) / (1.0 + LOVE.kl[l]) # degree dependent factor to convert from mass - coeff_inv = 0.75/(np.pi*rho_e*rad_e**3) - fact_inv[ii] = coeff_inv*(1.0 + LOVE.kl[l])/(2.0*l + 1.0) + coeff_inv = 0.75 / (np.pi * rho_e * rad_e**3) + fact_inv[ii] = coeff_inv * (1.0 + LOVE.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): + 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 + MA_lm[i, :] = M_lm[i, :] * wt_lm[i] * fact[i] + fit_factor = wt_lm * fact inv_fit_factor = np.copy(fact_inv) - elif (FIT_METHOD == 2): + elif FIT_METHOD == 2: # 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)) + 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 @@ -346,19 +352,20 @@ def calc_sensitivity_kernel(LMAX, RAD, # 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] + kern_i[i] = 1.0 * fit_factor[i] # spherical harmonics solution for the # mascon sensitivity kernels - if (SOLVER == 'inv'): + if SOLVER == 'inv': kern_lm = np.dot(np.linalg.inv(MA_lm), kern_i) - elif (SOLVER == 'lstsq'): + elif SOLVER == 'lstsq': kern_lm = np.linalg.lstsq(MA_lm, kern_i, rcond=-1)[0] elif SOLVER in ('gelsd', 'gelsy', 'gelss'): - kern_lm, res, rnk, s = scipy.linalg.lstsq(MA_lm, kern_i, - lapack_driver=SOLVER) + kern_lm, res, rnk, s = scipy.linalg.lstsq( + MA_lm, kern_i, lapack_driver=SOLVER + ) # calculate the sensitivity kernel for each mascon for k in range(n_mas): - A_lm[i,k] = kern_lm[k]*total_area[k] + A_lm[i, k] = kern_lm[k] * total_area[k] # free up larger variables del M_lm, MA_lm, wt_lm, fact, fact_inv, fit_factor @@ -367,24 +374,24 @@ def calc_sensitivity_kernel(LMAX, RAD, # kernel calculated as outlined in Tiwari (2009) and Jacobs (2012) # Initializing output sensitivity kernel (both spatial and Ylms) kern_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - kern_Ylms.clm = np.zeros((LMAX+1, MMAX+1, n_mas)) - kern_Ylms.slm = np.zeros((LMAX+1, MMAX+1, n_mas)) + kern_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1, n_mas)) + kern_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1, n_mas)) kern_Ylms.time = np.copy(total_area) # counter variable for deconstructing the mascon column arrays ii = 0 # Switching between Cosine and Sine Stokes - for cs,csharm in enumerate(['clm','slm']): + for cs, csharm in enumerate(['clm', 'slm']): # for each spherical harmonic degree # +1 to include LMAX - for l in range(LMIN,LMAX+1): + for l in range(LMIN, LMAX + 1): # for each spherical harmonic order # Sine Stokes for (m=0) = 0 - mm = np.min([MMAX,l]) + mm = np.min([MMAX, l]) # +1 to include l or MMAX (whichever is smaller) - for m in range(cs,mm+1): + 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,:] + temp[l, m, :] = inv_fit_factor[ii] * A_lm[ii, :] # add 1 to counter ii += 1 # free up larger variables @@ -398,11 +405,10 @@ def calc_sensitivity_kernel(LMAX, RAD, # get harmonics for mascon Ylms = kern_Ylms.index(k, date=False) # output sensitivity kernel to file - args = (mascon_name[k],ocean_str,LMAX,order_str,gw_str,suffix) + args = (mascon_name[k], ocean_str, LMAX, order_str, gw_str, suffix) FILE1 = '{0}_SKERNEL_CLM{1}_L{2:d}{3}{4}.{5}'.format(*args) output_file = OUTPUT_DIRECTORY.joinpath(FILE1) - Ylms.to_file(output_file, format=DATAFORM, - date=False, **attributes) + Ylms.to_file(output_file, format=DATAFORM, date=False, **attributes) # change the permissions mode output_file.chmod(mode=MODE) # add output files to list object @@ -418,25 +424,29 @@ def calc_sensitivity_kernel(LMAX, RAD, # Output spatial data object grid = gravtk.spatial() # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = ( + (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) + ) # Output Degree Interval - if (INTERVAL == 1): + 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): + 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) + 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) - elif (INTERVAL == 3): + 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) + 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 + ) n_lon = len(grid.lon) n_lat = len(grid.lat) @@ -449,15 +459,21 @@ def calc_sensitivity_kernel(LMAX, RAD, # get harmonics for mascon Ylms = kern_Ylms.index(k, date=False) # convert spherical harmonics to output spatial grid - grid.data = gravtk.harmonic_summation(Ylms.clm, Ylms.slm, - grid.lon, grid.lat, LMAX=LMAX, MMAX=MMAX, PLM=PLM).T + grid.data = gravtk.harmonic_summation( + Ylms.clm, + Ylms.slm, + grid.lon, + grid.lat, + LMAX=LMAX, + MMAX=MMAX, + PLM=PLM, + ).T grid.mask = np.zeros_like(grid.data, dtype=bool) # output sensitivity kernel to file - args = (mascon_name[k],ocean_str,LMAX,order_str,gw_str,suffix) + args = (mascon_name[k], ocean_str, LMAX, order_str, gw_str, suffix) FILE2 = '{0}_SKERNEL{1}_L{2:d}{3}{4}.{5}'.format(*args) output_file = OUTPUT_DIRECTORY.joinpath(FILE2) - grid.to_file(output_file, format=DATAFORM, - date=False, **attributes) + grid.to_file(output_file, format=DATAFORM, date=False, **attributes) # change the permissions mode output_file.chmod(mode=MODE) # add output files to list object @@ -466,10 +482,11 @@ def calc_sensitivity_kernel(LMAX, RAD, # return the list of output files return output_files + # PURPOSE: print a file log for the mascon sensitivity kernel analysis def output_log_file(input_arguments, output_files): # format: calc_skernel_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'calc_skernel_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -486,10 +503,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the mascon sensitivity kernel analysis def output_error_log_file(input_arguments): # format: calc_skernel_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'calc_skernel_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -505,112 +523,193 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates spatial sensitivity kernels through a least-squares mascon procedure """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # minimum spherical harmonic degree - parser.add_argument('--lmin', - type=int, default=1, - help='Minimum spherical harmonic degree') + parser.add_argument( + '--lmin', type=int, default=1, help='Minimum spherical harmonic degree' + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format for auxiliary files') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format for auxiliary files', + ) # mascon index file and parameters - parser.add_argument('--mascon-file', + parser.add_argument( + '--mascon-file', type=pathlib.Path, - help='Index file of mascons spherical harmonics') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + help='Index file of mascons spherical harmonics', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # 1: mass coefficients # 2: geoid coefficients - parser.add_argument('--fit-method', - type=int, default=1, choices=(1,2), - help='Method for fitting sensitivity kernel to harmonics') + parser.add_argument( + '--fit-method', + type=int, + default=1, + choices=(1, 2), + help='Method for fitting sensitivity kernel to harmonics', + ) # least squares solver - choices = ('inv','lstsq','gelsd', 'gelsy', 'gelss') - parser.add_argument('--solver','-s', - type=str, default='lstsq', choices=choices, - help='Least squares solver for sensitivity kernel solutions') + choices = ('inv', 'lstsq', 'gelsd', 'gelsy', 'gelss') + parser.add_argument( + '--solver', + '-s', + type=str, + default='lstsq', + choices=choices, + help='Least squares solver for sensitivity kernel solutions', + ) # land-sea mask for redistributing mascon mass - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing mascon mass') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing mascon mass', + ) # output spatial grid - parser.add_argument('--spatial','-o', - default=False, action='store_true', - help='Output spatial grid file for each mascon') + parser.add_argument( + '--spatial', + '-o', + default=False, + action='store_true', + help='Output spatial grid file for each mascon', + ) # output grid parameters - parser.add_argument('--spacing','-S', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval','-I', - type=int, default=2, choices=[1,2,3], - help=('Output grid interval ' - '(1: global, 2: centered global, 3: non-global)')) - parser.add_argument('--bounds','-B', - type=float, nargs=4, metavar=('lon_min','lon_max','lat_min','lat_max'), - help='Bounding box for non-global grid') + parser.add_argument( + '--spacing', + '-S', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + '-I', + type=int, + default=2, + choices=[1, 2, 3], + help=( + 'Output grid interval ' + '(1: global, 2: centered global, 3: non-global)' + ), + ) + parser.add_argument( + '--bounds', + '-B', + type=float, + nargs=4, + metavar=('lon_min', 'lon_max', 'lat_min', 'lat_max'), + help='Bounding box for non-global grid', + ) # Output log file for each job in forms # calc_skernel_run_2002-04-01_PID-00000.log # calc_skernel_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -638,18 +737,20 @@ def main(): INTERVAL=args.interval, BOUNDS=args.bounds, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/combine_HEX_ATM_errors.py b/scripts/combine_HEX_ATM_errors.py index 04cdd797..ffd5ce0e 100644 --- a/scripts/combine_HEX_ATM_errors.py +++ b/scripts/combine_HEX_ATM_errors.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_HEX_ATM_errors.py (04/2024) calculates the time-series of atmospheric mass leakage for spherical cap mascons @@ -49,6 +49,7 @@ output average time series in equivalent surface pressure difference Written 03/2018 """ + from __future__ import print_function import sys @@ -65,102 +66,280 @@ # Regions region = [] -region.extend(['AAp', 'ApB', 'BC', 'CCp', 'CpD', 'DDp', 'DpE', 'EEp', 'EpFp', - 'FpG', 'GH', 'HHp', 'HpI', 'IIpp', 'IppJ', 'JJpp', 'JppK','KKp', 'KpA', - 'INTERIOR', 'AIS', 'EAIS', 'WAIS', 'APIS', 'QML', 'NoQML', 'CpDc','CpDi', - 'DDpc','DDpi','GH2','HHp2','JJpp2','GH3','TM','PIG','THSPK']) -region.extend(['NW','NN','NE','SW','SE','GIS','CDE','CBI','ICL','SVB','ALK', - 'DEN','PNW','PAT','FJL','SZEM','NZEM','RUS','ARC','GIC']) +region.extend( + [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', + 'INTERIOR', + 'AIS', + 'EAIS', + 'WAIS', + 'APIS', + 'QML', + 'NoQML', + 'CpDc', + 'CpDi', + 'DDpc', + 'DDpi', + 'GH2', + 'HHp2', + 'JJpp2', + 'GH3', + 'TM', + 'PIG', + 'THSPK', + ] +) +region.extend( + [ + 'NW', + 'NN', + 'NE', + 'SW', + 'SE', + 'GIS', + 'CDE', + 'CBI', + 'ICL', + 'SVB', + 'ALK', + 'DEN', + 'PNW', + 'PAT', + 'FJL', + 'SZEM', + 'NZEM', + 'RUS', + 'ARC', + 'GIC', + ] +) # Cap numbers for each basin cap = {} # the contents of the cap variable will be referenced to the basin name # the cap numbers are the global numbers for the spherical cap RADius -cap['AAp'] = np.array([25,26,39,40,41,53,54]) -cap['ApB'] = np.array([55,56,67,68,69,70,83,97]) -cap['BC'] = np.array([80,81,82,93,94,95,96,107,108,109,121,122]) -cap['CCp'] = np.array([110,123,124,136,137,138,150,151]) -cap['CpD'] = np.array([135,148,149,162,163,164,175,176,177,189,190]) -cap['DDp'] = np.array([174,186,187,188,200,201,202]) -cap['DpE'] = np.array([158,159,160,161,172,173,185,199]) -cap['EEp'] = np.array([106,117,118,119,120,131,132,133,134,145,146,147]) -cap['EpFp'] = np.array([102,103,115,116,128,129,130,142,143]) -cap['FpG'] = np.array([127,141]) -cap['GH'] = np.array([86,100,101,113,114]) -cap['HHp'] = np.array([72,85,99]) -cap['HpI'] = np.array([45,59]) -cap['IIpp'] = np.array([4,17,18,31,32]) +cap['AAp'] = np.array([25, 26, 39, 40, 41, 53, 54]) +cap['ApB'] = np.array([55, 56, 67, 68, 69, 70, 83, 97]) +cap['BC'] = np.array([80, 81, 82, 93, 94, 95, 96, 107, 108, 109, 121, 122]) +cap['CCp'] = np.array([110, 123, 124, 136, 137, 138, 150, 151]) +cap['CpD'] = np.array([135, 148, 149, 162, 163, 164, 175, 176, 177, 189, 190]) +cap['DDp'] = np.array([174, 186, 187, 188, 200, 201, 202]) +cap['DpE'] = np.array([158, 159, 160, 161, 172, 173, 185, 199]) +cap['EEp'] = np.array( + [106, 117, 118, 119, 120, 131, 132, 133, 134, 145, 146, 147] +) +cap['EpFp'] = np.array([102, 103, 115, 116, 128, 129, 130, 142, 143]) +cap['FpG'] = np.array([127, 141]) +cap['GH'] = np.array([86, 100, 101, 113, 114]) +cap['HHp'] = np.array([72, 85, 99]) +cap['HpI'] = np.array([45, 59]) +cap['IIpp'] = np.array([4, 17, 18, 31, 32]) cap['IppJ'] = np.array([46]) -cap['JJpp'] = np.array([60,73,74,75,87,88,89]) -cap['JppK'] = np.array([49,50,51,52,62,63,64,65,66,76,77,78,79,90,91,92,104,105]) -cap['KKp'] = np.array([23,36,37]) -cap['KpA'] = np.array([24,38]) -cap['SHELVES'] = np.array([61,144,157]) +cap['JJpp'] = np.array([60, 73, 74, 75, 87, 88, 89]) +cap['JppK'] = np.array( + [49, 50, 51, 52, 62, 63, 64, 65, 66, 76, 77, 78, 79, 90, 91, 92, 104, 105] +) +cap['KKp'] = np.array([23, 36, 37]) +cap['KpA'] = np.array([24, 38]) +cap['SHELVES'] = np.array([61, 144, 157]) # Grouping Basins into larger regions (AIS, EAIS, WAIS and APEN) -cap['AIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['EpFp'], cap['FpG'], \ - cap['GH'], cap['HHp'], cap['HpI'], cap['IIpp'], cap['IppJ'], cap['JJpp'], \ - cap['JppK'], cap['KKp'], cap['KpA']),axis=0) #, cap['SHELVES'] -cap['EAIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp'], \ - cap['KpA']),axis=0) -cap['WAIS'] = np.concatenate((cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']),axis=0) -cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']),axis=0) -cap['INTERIOR'] = np.array([38,51,52,53,65,66,67,68,78,79,80,81,91,92,93,94,\ - 104,105,106,107,108,118,119,120,121,122,123,133,134,135,136,147,148,149,\ - 160,161,162,174,175]) -cap['QML'] = np.concatenate((cap['KpA'],cap['AAp'],cap['ApB']),axis=0) -cap['NoQML'] = np.concatenate((cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp']),axis=0) +cap['AIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['EpFp'], + cap['FpG'], + cap['GH'], + cap['HHp'], + cap['HpI'], + cap['IIpp'], + cap['IppJ'], + cap['JJpp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) # , cap['SHELVES'] +cap['EAIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) +cap['WAIS'] = np.concatenate( + (cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']), axis=0 +) +cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']), axis=0) +cap['INTERIOR'] = np.array( + [ + 38, + 51, + 52, + 53, + 65, + 66, + 67, + 68, + 78, + 79, + 80, + 81, + 91, + 92, + 93, + 94, + 104, + 105, + 106, + 107, + 108, + 118, + 119, + 120, + 121, + 122, + 123, + 133, + 134, + 135, + 136, + 147, + 148, + 149, + 160, + 161, + 162, + 174, + 175, + ] +) +cap['QML'] = np.concatenate((cap['KpA'], cap['AAp'], cap['ApB']), axis=0) +cap['NoQML'] = np.concatenate( + ( + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + ), + axis=0, +) cap['ISLAND'] = np.array([62]) -cap['CpDc'] = np.array([163,164,176,177,189,190]) -#cap['CpDc'] = np.array([163,164,177]) -cap['CpDi'] = np.array([135,148,149,162,175]) -cap['TM'] = np.array([135,148,149,162,163,176,177,190]) -cap['DDpc'] = np.array([186,187,188,200,201,202]) -#cap['DDpi'] = np.array([174]) -cap['DDpi'] = np.concatenate((cap['DDp'],cap['DpE']),axis=0) +cap['CpDc'] = np.array([163, 164, 176, 177, 189, 190]) +# cap['CpDc'] = np.array([163,164,177]) +cap['CpDi'] = np.array([135, 148, 149, 162, 175]) +cap['TM'] = np.array([135, 148, 149, 162, 163, 176, 177, 190]) +cap['DDpc'] = np.array([186, 187, 188, 200, 201, 202]) +# cap['DDpi'] = np.array([174]) +cap['DDpi'] = np.concatenate((cap['DDp'], cap['DpE']), axis=0) # different version of west ant -cap['GH2'] = np.array([86,87,99,100,101,113,114]) -cap['HHp2'] = np.array([72,85]) -cap['JJpp2'] = np.array([60,73,74,75,88,89]) -cap['GH3'] = np.array([86,99,100,101,113,114]) +cap['GH2'] = np.array([86, 87, 99, 100, 101, 113, 114]) +cap['HHp2'] = np.array([72, 85]) +cap['JJpp2'] = np.array([60, 73, 74, 75, 88, 89]) +cap['GH3'] = np.array([86, 99, 100, 101, 113, 114]) # Amundsen Sea Embayment regions -cap['PIG'] = np.array([86,87,99]) -cap['THSPK'] = np.array([100,101,113,114]) +cap['PIG'] = np.array([86, 87, 99]) +cap['THSPK'] = np.array([100, 101, 113, 114]) # Greenland Mascons -cap['NW'] = np.array([308,309,312,313,316,317]) -cap['NN'] = np.array([301,302,303,304,305,306]) -cap['NE'] = np.array([307,310,311,314,315]) -cap['SE'] = np.array([318,319,322,323,325,327]) -cap['SW'] = np.array([320,321,324,326]) -cap['GIS'] = np.concatenate((cap['NW'],cap['NN'],cap['NE'],cap['SW'],cap['SE']),axis=0) +cap['NW'] = np.array([308, 309, 312, 313, 316, 317]) +cap['NN'] = np.array([301, 302, 303, 304, 305, 306]) +cap['NE'] = np.array([307, 310, 311, 314, 315]) +cap['SE'] = np.array([318, 319, 322, 323, 325, 327]) +cap['SW'] = np.array([320, 321, 324, 326]) +cap['GIS'] = np.concatenate( + (cap['NW'], cap['NN'], cap['NE'], cap['SW'], cap['SE']), axis=0 +) # Canadian Archipelago -cap['CDE'] = np.array([328,329,330,331,332,333]) -cap['CBI'] = np.array([334,335,336,337,338]) +cap['CDE'] = np.array([328, 329, 330, 331, 332, 333]) +cap['CBI'] = np.array([334, 335, 336, 337, 338]) # Iceland, Svalbard, Franz Josef Land, Svernaya Zemlya and Novaya Zemlya cap['ICL'] = np.array([339]) cap['SVB'] = np.array([340]) cap['FJL'] = np.array([341]) cap['SZEM'] = np.array([342]) -cap['NZEM'] = np.array([343,344]) +cap['NZEM'] = np.array([343, 344]) # Alaska (Denali and Pacific Northwest) -cap['ALK'] = np.arange(345,368) -cap['DEN'] = np.array([345,346,348,349,350,351,352,353,354,355,356,367]) -cap['PNW'] = np.array([347,357,358,359,360,361,362,363,364,365,366]) +cap['ALK'] = np.arange(345, 368) +cap['DEN'] = np.array( + [345, 346, 348, 349, 350, 351, 352, 353, 354, 355, 356, 367] +) +cap['PNW'] = np.array([347, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366]) # Patagonia -cap['PAT'] = np.arange(400,407) +cap['PAT'] = np.arange(400, 407) # All Arctic and all GIC (with patagonia) -cap['ARC'] = np.concatenate((cap['GIS'],cap['CDE'],cap['CBI'],cap['ICL'], - cap['SVB'],cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) -cap['GIC'] = np.concatenate((cap['CDE'],cap['CBI'],cap['ICL'],cap['SVB'], - cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM'],cap['PAT']),axis=0) +cap['ARC'] = np.concatenate( + ( + cap['GIS'], + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + ), + axis=0, +) +cap['GIC'] = np.concatenate( + ( + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + cap['PAT'], + ), + axis=0, +) # Russian Arctic (Franz Josef Land, Svernaya Zemlya and Novaya Zemlya) -cap['RUS'] = np.concatenate((cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) +cap['RUS'] = np.concatenate((cap['FJL'], cap['SZEM'], cap['NZEM']), axis=0) # All caps (Greenland, Glaciers and Ice Caps, Antarctica) -cap['ALL'] = np.concatenate((cap['GIS'],cap['GIC'],cap['AIS']),axis=0) +cap['ALL'] = np.concatenate((cap['GIS'], cap['GIC'], cap['AIS']), axis=0) + # PURPOSE: keep track of threads def info(args): @@ -170,14 +349,19 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') -def combine_HEX_ATM_errors(base_dir, MODEL, LMAX, RAD, + +def combine_HEX_ATM_errors( + base_dir, + MODEL, + LMAX, + RAD, MMAX=None, DESTRIPE=False, REDISTRIBUTE=False, REDISTRIBUTE_MASCONS=False, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -189,10 +373,22 @@ def combine_HEX_ATM_errors(base_dir, MODEL, LMAX, RAD, # RANGE = {'ERA-Interim':(4,210),'ERA5':(4,226),'MERRA-2':(4,227), # 'NCEP-DOE-2':(4,228),'NCEP-CFSR':(4,227),'JRA-55':(4,227)} - dim_flags = {'ERA-Interim':'','ERA5':'','MERRA-2':'', - 'NCEP-DOE-2':'','NCEP-CFSR':'','JRA-55':''} - RANGE = {'ERA-Interim':(4,210),'ERA5':(4,251),'MERRA-2':(4,251), - 'NCEP-DOE-2':(4,251),'NCEP-CFSR':(4,227),'JRA-55':(4,251)} + dim_flags = { + 'ERA-Interim': '', + 'ERA5': '', + 'MERRA-2': '', + 'NCEP-DOE-2': '', + 'NCEP-CFSR': '', + 'JRA-55': '', + } + RANGE = { + 'ERA-Interim': (4, 210), + 'ERA5': (4, 251), + 'MERRA-2': (4, 251), + 'NCEP-DOE-2': (4, 251), + 'NCEP-CFSR': (4, 227), + 'JRA-55': (4, 251), + } # Gaussian smoothing string for radius RAD (if 0: no flag) gw_str = f'_r{RAD:0.0f}km' if (RAD != 0) else '' @@ -222,13 +418,13 @@ def combine_HEX_ATM_errors(base_dir, MODEL, LMAX, RAD, # read reanalysis outputs for m in MODEL: # use 3D geometry with the ERA-Interim and MERRA-2 reanalyses - a2=(m.upper(),dim_flags[m],'',LMAX,RANGE[m][0],RANGE[m][1]) - s2='{0}{1}{2}_SPH_CAP_MSCNS_L{3:d}_{4:03d}-{5:03d}'.format(*a2) + a2 = (m.upper(), dim_flags[m], '', LMAX, RANGE[m][0], RANGE[m][1]) + s2 = '{0}{1}{2}_SPH_CAP_MSCNS_L{3:d}_{4:03d}-{5:03d}'.format(*a2) # calculate RMS based on actual values - a2=(m,RAD_CAP,k,LMAX,gw_str,ocean_str) - f2='{0}_SPH_CAP_RAD{1:0.1f}_{2:d}_L{3:d}{4}{5}.txt'.format(*a2) + a2 = (m, RAD_CAP, k, LMAX, gw_str, ocean_str) + f2 = '{0}_SPH_CAP_RAD{1:0.1f}_{2:d}_L{3:d}{4}{5}.txt'.format(*a2) # read atmospheric pressure anomalies and verify shape - atm_file = base_dir.joinpath('reanalysis',m,s2,f2) + atm_file = base_dir.joinpath('reanalysis', m, s2, f2) ATM[m][k] = np.loadtxt(atm_file, ndmin=2) # date information @@ -236,23 +432,25 @@ def combine_HEX_ATM_errors(base_dir, MODEL, LMAX, RAD, end_mon = -np.inf missing = [] for M in MODEL: - ATM_months = ATM[M][k][:,0].astype(np.int64) - if (np.min(ATM_months) < start_mon): + ATM_months = ATM[M][k][:, 0].astype(np.int64) + if np.min(ATM_months) < start_mon: start_mon = np.min(ATM_months) - if (np.max(ATM_months) > end_mon): + if np.max(ATM_months) > end_mon: end_mon = np.max(ATM_months) # find missing months for any dataset - missing.extend(list(set(np.arange(start_mon,end_mon+1))-set(ATM_months))) + missing.extend( + list(set(np.arange(start_mon, end_mon + 1)) - set(ATM_months)) + ) # GRACE/GRACE-FO months - mon = np.arange(start_mon,end_mon+1) + mon = np.arange(start_mon, end_mon + 1) missing = sorted(set(missing)) n_mon = len(mon) # GRACE/GRACE-FO dates - calendar_year = 2002 + (mon-1)//12 - calendar_month = np.mod(mon-1,12) + 1 - tdec = gravtk.time.convert_calendar_decimal(calendar_year,calendar_month) + calendar_year = 2002 + (mon - 1) // 12 + calendar_month = np.mod(mon - 1, 12) + 1 + tdec = gravtk.time.convert_calendar_decimal(calendar_year, calendar_month) # remove mean of 2003--2014 - m0314, = np.nonzero((mon >= 13) & (mon <= 156)) + (m0314,) = np.nonzero((mon >= 13) & (mon <= 156)) # for each region for i in region: @@ -265,31 +463,40 @@ def combine_HEX_ATM_errors(base_dir, MODEL, LMAX, RAD, for k in cap[i]: for M in MODEL: # sum of reanalysis outputs - ATM_months = ATM[M][k][:,0].astype(np.int64) + ATM_months = ATM[M][k][:, 0].astype(np.int64) ind = np.ravel([np.flatnonzero(mon == m) for m in ATM_months]) - ATM_mass[M][i][ind] += ATM[M][k][:,2] + ATM_mass[M][i][ind] += ATM[M][k][:, 2] # add mascon area to total area (cm^2) - area_reg[i] += 1e10*ATM[M][k][0,3] + area_reg[i] += 1e10 * ATM[M][k][0, 3] # calculate mean of reanalyses ATM_mean = np.zeros((n_mon)) ATM_variance = np.zeros((n_mon)) - for c,M in enumerate(MODEL): + for c, M in enumerate(MODEL): ATM_mean += ATM_mass[M][i] - ATM_mass[M][i][m0314].mean() ATM_mean /= len(MODEL) # calculate variance off of mean - for c,M in enumerate(MODEL): + for c, M in enumerate(MODEL): mm = ATM_mass[M][i] - ATM_mass[M][i][m0314].mean() - ATM_variance += (mm - ATM_mean)**2 + ATM_variance += (mm - ATM_mean) ** 2 # RMS sum of reanalysis residuals components - ATM_reg[i] = np.sqrt(ATM_variance/(len(MODEL)-1.0)) + ATM_reg[i] = np.sqrt(ATM_variance / (len(MODEL) - 1.0)) # replace invalid values with nan ind = np.ravel([np.flatnonzero(mon == m) for m in missing]) ATM_reg[i][ind] = np.nan # output data files # a = ('ATM_Differences',i,'3D_',ocean_str,LMAX,order_str,gw_str,ds_str) - a = ('ATM_Differences',i,'',ocean_str,LMAX,order_str,gw_str,ds_str) + a = ( + 'ATM_Differences', + i, + '', + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + ) FILE1 = '{0}_{1}_{2}SPH_CAP{3}_L{4:d}{5}{6}{7}.txt'.format(*a) FILE2 = '{0}_{1}_{2}SPH_CAP{3}_L{4:d}{5}{6}{7}_mbar.txt'.format(*a) # open files for writing region time-series @@ -301,7 +508,7 @@ def combine_HEX_ATM_errors(base_dir, MODEL, LMAX, RAD, # output GRACE regional time-series for t in range(n_mon): mass_error = ATM_reg[i][t] - mbar_error = 1e14*g_wmo*ATM_reg[i][t]/area_reg[i] + mbar_error = 1e14 * g_wmo * ATM_reg[i][t] / area_reg[i] print(f'{mon[t]:03d} {tdec[t]:12.4f} {mass_error:14.6f}', file=fid1) print(f'{mon[t]:03d} {tdec[t]:12.4f} {mbar_error:14.6f}', file=fid2) # close the output file @@ -311,68 +518,118 @@ def combine_HEX_ATM_errors(base_dir, MODEL, LMAX, RAD, mass_file.chmod(mode=MODE) mbar_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""calculates the time-series of atmospheric mass leakage for spherical cap mascons """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - choices = ['ERA-Interim','ERA5','MERRA-2','NCEP-DOE-2','NCEP-CFSR','JRA-55'] - parser.add_argument('model', - metavar='MODEL', type=str, nargs='+', - default=['ERA5','MERRA-2','NCEP-DOE-2','JRA-55'], choices=choices, - help='Reanalysis Models') + choices = [ + 'ERA-Interim', + 'ERA5', + 'MERRA-2', + 'NCEP-DOE-2', + 'NCEP-CFSR', + 'JRA-55', + ] + parser.add_argument( + 'model', + metavar='MODEL', + type=str, + nargs='+', + default=['ERA5', 'MERRA-2', 'NCEP-DOE-2', 'JRA-55'], + choices=choices, + help='Reanalysis Models', + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # uniformly redistribute pressure values over the ocean - parser.add_argument('--redistribute-mass', - default=False, action='store_true', - help='Redistribute pressure values over the ocean') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--redistribute-mass', + default=False, + action='store_true', + help='Redistribute pressure values over the ocean', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -392,7 +649,8 @@ def main(): REDISTRIBUTE=args.redistribute_mass, REDISTRIBUTE_MASCONS=args.redistribute_mascons, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -400,6 +658,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/combine_HEX_Caron_errors.py b/scripts/combine_HEX_Caron_errors.py index 3104f015..50733f8e 100644 --- a/scripts/combine_HEX_Caron_errors.py +++ b/scripts/combine_HEX_Caron_errors.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_HEX_Caron_errors.py (04/2024) calculates the estimated GIA uncertainty for spherical cap mascon regions @@ -27,6 +27,7 @@ Updated 10/2020: use argparse to set command line parameters Written 10/2019 """ + from __future__ import print_function import sys @@ -43,102 +44,280 @@ # Regions region = [] -region.extend(['AAp', 'ApB', 'BC', 'CCp', 'CpD', 'DDp', 'DpE', 'EEp', 'EpFp', - 'FpG', 'GH', 'HHp', 'HpI', 'IIpp', 'IppJ', 'JJpp', 'JppK','KKp', 'KpA', - 'INTERIOR', 'AIS', 'EAIS', 'WAIS', 'APIS', 'QML', 'NoQML', 'CpDc','CpDi', - 'DDpc','DDpi','GH2','HHp2','JJpp2','GH3','TM','PIG','THSPK']) -region.extend(['NW','NN','NE','SW','SE','GIS','CDE','CBI','ICL','SVB','ALK', - 'DEN','PNW','PAT','FJL','SZEM','NZEM','RUS','ARC','GIC']) +region.extend( + [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', + 'INTERIOR', + 'AIS', + 'EAIS', + 'WAIS', + 'APIS', + 'QML', + 'NoQML', + 'CpDc', + 'CpDi', + 'DDpc', + 'DDpi', + 'GH2', + 'HHp2', + 'JJpp2', + 'GH3', + 'TM', + 'PIG', + 'THSPK', + ] +) +region.extend( + [ + 'NW', + 'NN', + 'NE', + 'SW', + 'SE', + 'GIS', + 'CDE', + 'CBI', + 'ICL', + 'SVB', + 'ALK', + 'DEN', + 'PNW', + 'PAT', + 'FJL', + 'SZEM', + 'NZEM', + 'RUS', + 'ARC', + 'GIC', + ] +) # Cap numbers for each basin cap = {} # the contents of the cap variable will be referenced to the basin name # the cap numbers are the global numbers for the spherical cap RADius -cap['AAp'] = np.array([25,26,39,40,41,53,54]) -cap['ApB'] = np.array([55,56,67,68,69,70,83,97]) -cap['BC'] = np.array([80,81,82,93,94,95,96,107,108,109,121,122]) -cap['CCp'] = np.array([110,123,124,136,137,138,150,151]) -cap['CpD'] = np.array([135,148,149,162,163,164,175,176,177,189,190]) -cap['DDp'] = np.array([174,186,187,188,200,201,202]) -cap['DpE'] = np.array([158,159,160,161,172,173,185,199]) -cap['EEp'] = np.array([106,117,118,119,120,131,132,133,134,145,146,147]) -cap['EpFp'] = np.array([102,103,115,116,128,129,130,142,143]) -cap['FpG'] = np.array([127,141]) -cap['GH'] = np.array([86,100,101,113,114]) -cap['HHp'] = np.array([72,85,99]) -cap['HpI'] = np.array([45,59]) -cap['IIpp'] = np.array([4,17,18,31,32]) +cap['AAp'] = np.array([25, 26, 39, 40, 41, 53, 54]) +cap['ApB'] = np.array([55, 56, 67, 68, 69, 70, 83, 97]) +cap['BC'] = np.array([80, 81, 82, 93, 94, 95, 96, 107, 108, 109, 121, 122]) +cap['CCp'] = np.array([110, 123, 124, 136, 137, 138, 150, 151]) +cap['CpD'] = np.array([135, 148, 149, 162, 163, 164, 175, 176, 177, 189, 190]) +cap['DDp'] = np.array([174, 186, 187, 188, 200, 201, 202]) +cap['DpE'] = np.array([158, 159, 160, 161, 172, 173, 185, 199]) +cap['EEp'] = np.array( + [106, 117, 118, 119, 120, 131, 132, 133, 134, 145, 146, 147] +) +cap['EpFp'] = np.array([102, 103, 115, 116, 128, 129, 130, 142, 143]) +cap['FpG'] = np.array([127, 141]) +cap['GH'] = np.array([86, 100, 101, 113, 114]) +cap['HHp'] = np.array([72, 85, 99]) +cap['HpI'] = np.array([45, 59]) +cap['IIpp'] = np.array([4, 17, 18, 31, 32]) cap['IppJ'] = np.array([46]) -cap['JJpp'] = np.array([60,73,74,75,87,88,89]) -cap['JppK'] = np.array([49,50,51,52,62,63,64,65,66,76,77,78,79,90,91,92,104,105]) -cap['KKp'] = np.array([23,36,37]) -cap['KpA'] = np.array([24,38]) -cap['SHELVES'] = np.array([61,144,157]) +cap['JJpp'] = np.array([60, 73, 74, 75, 87, 88, 89]) +cap['JppK'] = np.array( + [49, 50, 51, 52, 62, 63, 64, 65, 66, 76, 77, 78, 79, 90, 91, 92, 104, 105] +) +cap['KKp'] = np.array([23, 36, 37]) +cap['KpA'] = np.array([24, 38]) +cap['SHELVES'] = np.array([61, 144, 157]) # Grouping Basins into larger regions (AIS, EAIS, WAIS and APEN) -cap['AIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['EpFp'], cap['FpG'], \ - cap['GH'], cap['HHp'], cap['HpI'], cap['IIpp'], cap['IppJ'], cap['JJpp'], \ - cap['JppK'], cap['KKp'], cap['KpA']),axis=0) #, cap['SHELVES'] -cap['EAIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp'], \ - cap['KpA']),axis=0) -cap['WAIS'] = np.concatenate((cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']),axis=0) -cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']),axis=0) -cap['INTERIOR'] = np.array([38,51,52,53,65,66,67,68,78,79,80,81,91,92,93,94,\ - 104,105,106,107,108,118,119,120,121,122,123,133,134,135,136,147,148,149,\ - 160,161,162,174,175]) -cap['QML'] = np.concatenate((cap['KpA'],cap['AAp'],cap['ApB']),axis=0) -cap['NoQML'] = np.concatenate((cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp']),axis=0) +cap['AIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['EpFp'], + cap['FpG'], + cap['GH'], + cap['HHp'], + cap['HpI'], + cap['IIpp'], + cap['IppJ'], + cap['JJpp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) # , cap['SHELVES'] +cap['EAIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) +cap['WAIS'] = np.concatenate( + (cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']), axis=0 +) +cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']), axis=0) +cap['INTERIOR'] = np.array( + [ + 38, + 51, + 52, + 53, + 65, + 66, + 67, + 68, + 78, + 79, + 80, + 81, + 91, + 92, + 93, + 94, + 104, + 105, + 106, + 107, + 108, + 118, + 119, + 120, + 121, + 122, + 123, + 133, + 134, + 135, + 136, + 147, + 148, + 149, + 160, + 161, + 162, + 174, + 175, + ] +) +cap['QML'] = np.concatenate((cap['KpA'], cap['AAp'], cap['ApB']), axis=0) +cap['NoQML'] = np.concatenate( + ( + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + ), + axis=0, +) cap['ISLAND'] = np.array([62]) -cap['CpDc'] = np.array([163,164,176,177,189,190]) -#cap['CpDc'] = np.array([163,164,177]) -cap['CpDi'] = np.array([135,148,149,162,175]) -cap['TM'] = np.array([135,148,149,162,163,176,177,190]) -cap['DDpc'] = np.array([186,187,188,200,201,202]) -#cap['DDpi'] = np.array([174]) -cap['DDpi'] = np.concatenate((cap['DDp'],cap['DpE']),axis=0) +cap['CpDc'] = np.array([163, 164, 176, 177, 189, 190]) +# cap['CpDc'] = np.array([163,164,177]) +cap['CpDi'] = np.array([135, 148, 149, 162, 175]) +cap['TM'] = np.array([135, 148, 149, 162, 163, 176, 177, 190]) +cap['DDpc'] = np.array([186, 187, 188, 200, 201, 202]) +# cap['DDpi'] = np.array([174]) +cap['DDpi'] = np.concatenate((cap['DDp'], cap['DpE']), axis=0) # different version of west ant -cap['GH2'] = np.array([86,87,99,100,101,113,114]) -cap['HHp2'] = np.array([72,85]) -cap['JJpp2'] = np.array([60,73,74,75,88,89]) -cap['GH3'] = np.array([86,99,100,101,113,114]) +cap['GH2'] = np.array([86, 87, 99, 100, 101, 113, 114]) +cap['HHp2'] = np.array([72, 85]) +cap['JJpp2'] = np.array([60, 73, 74, 75, 88, 89]) +cap['GH3'] = np.array([86, 99, 100, 101, 113, 114]) # Amundsen Sea Embayment regions -cap['PIG'] = np.array([86,87,99]) -cap['THSPK'] = np.array([100,101,113,114]) +cap['PIG'] = np.array([86, 87, 99]) +cap['THSPK'] = np.array([100, 101, 113, 114]) # Greenland Mascons -cap['NW'] = np.array([308,309,312,313,316,317]) -cap['NN'] = np.array([301,302,303,304,305,306]) -cap['NE'] = np.array([307,310,311,314,315]) -cap['SE'] = np.array([318,319,322,323,325,327]) -cap['SW'] = np.array([320,321,324,326]) -cap['GIS'] = np.concatenate((cap['NW'],cap['NN'],cap['NE'],cap['SW'],cap['SE']),axis=0) +cap['NW'] = np.array([308, 309, 312, 313, 316, 317]) +cap['NN'] = np.array([301, 302, 303, 304, 305, 306]) +cap['NE'] = np.array([307, 310, 311, 314, 315]) +cap['SE'] = np.array([318, 319, 322, 323, 325, 327]) +cap['SW'] = np.array([320, 321, 324, 326]) +cap['GIS'] = np.concatenate( + (cap['NW'], cap['NN'], cap['NE'], cap['SW'], cap['SE']), axis=0 +) # Canadian Archipelago -cap['CDE'] = np.array([328,329,330,331,332,333]) -cap['CBI'] = np.array([334,335,336,337,338]) +cap['CDE'] = np.array([328, 329, 330, 331, 332, 333]) +cap['CBI'] = np.array([334, 335, 336, 337, 338]) # Iceland, Svalbard, Franz Josef Land, Svernaya Zemlya and Novaya Zemlya cap['ICL'] = np.array([339]) cap['SVB'] = np.array([340]) cap['FJL'] = np.array([341]) cap['SZEM'] = np.array([342]) -cap['NZEM'] = np.array([343,344]) +cap['NZEM'] = np.array([343, 344]) # Alaska (Denali and Pacific Northwest) -cap['ALK'] = np.arange(345,368) -cap['DEN'] = np.array([345,346,348,349,350,351,352,353,354,355,356,367]) -cap['PNW'] = np.array([347,357,358,359,360,361,362,363,364,365,366]) +cap['ALK'] = np.arange(345, 368) +cap['DEN'] = np.array( + [345, 346, 348, 349, 350, 351, 352, 353, 354, 355, 356, 367] +) +cap['PNW'] = np.array([347, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366]) # Patagonia -cap['PAT'] = np.arange(400,407) +cap['PAT'] = np.arange(400, 407) # All Arctic and all GIC (with patagonia) -cap['ARC'] = np.concatenate((cap['GIS'],cap['CDE'],cap['CBI'],cap['ICL'], - cap['SVB'],cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) -cap['GIC'] = np.concatenate((cap['CDE'],cap['CBI'],cap['ICL'],cap['SVB'], - cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM'],cap['PAT']),axis=0) +cap['ARC'] = np.concatenate( + ( + cap['GIS'], + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + ), + axis=0, +) +cap['GIC'] = np.concatenate( + ( + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + cap['PAT'], + ), + axis=0, +) # Russian Arctic (Franz Josef Land, Svernaya Zemlya and Novaya Zemlya) -cap['RUS'] = np.concatenate((cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) +cap['RUS'] = np.concatenate((cap['FJL'], cap['SZEM'], cap['NZEM']), axis=0) # All caps (Greenland, Glaciers and Ice Caps, Antarctica) -cap['ALL'] = np.concatenate((cap['GIS'],cap['GIC'],cap['AIS']),axis=0) +cap['ALL'] = np.concatenate((cap['GIS'], cap['GIC'], cap['AIS']), axis=0) + # PURPOSE: keep track of threads def info(args): @@ -148,13 +327,16 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') -def combine_HEX_Caron_errors(LMAX, RAD, + +def combine_HEX_Caron_errors( + LMAX, + RAD, MMAX=None, DESTRIPE=False, REDISTRIBUTE_MASCONS=False, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -178,7 +360,7 @@ def combine_HEX_Caron_errors(LMAX, RAD, # Read each spherical cap for k in cap['ALL']: # GIA files - args = (RAD_CAP,k,'CARON_ERROR_',ocean_str,LMAX,order_str,gw_str) + args = (RAD_CAP, k, 'CARON_ERROR_', ocean_str, LMAX, order_str, gw_str) f1 = 'SPH_CAP_RAD{0:0.1f}_{1:d}_{2}{3}L{4:d}{5}{6}.txt'.format(*args) # read cap for GIA covariances GIA_data[k] = np.loadtxt(OUTPUT_DIRECTORY.joinpath(f1)) @@ -192,12 +374,12 @@ def combine_HEX_Caron_errors(LMAX, RAD, # sum mascons in region for k in cap[i]: # sum of GIA uncertainty data - GIA_reg[i] += GIA_data[k][0]**2 + GIA_reg[i] += GIA_data[k][0] ** 2 # add mascon area to total area (cm^2) - area_reg[i] += 1e10*GIA_data[k][1] + area_reg[i] += 1e10 * GIA_data[k][1] # output data files - args = ('Caron_Error_',i,ocean_str,LMAX,order_str,gw_str) + args = ('Caron_Error_', i, ocean_str, LMAX, order_str, gw_str) FILE1 = '{0}{1}_SPH_CAP_{2}L{3:d}{4}{5}.txt'.format(*args) FILE2 = '{0}{1}_SPH_CAP_{2}L{3:d}{4}{5}_cmwe.txt'.format(*args) # open files for writing region error @@ -208,7 +390,7 @@ def combine_HEX_Caron_errors(LMAX, RAD, fid2 = cmwe_file.open(mode='w', encoding='utf8') # output GIA regional error total_error = np.sqrt(GIA_reg[i]) - thick_error = 1e15*total_error/area_reg[i] + thick_error = 1e15 * total_error / area_reg[i] print(f'{total_error:14.6f}', file=fid1) print(f'{thick_error:14.6f}', file=fid2) # close the output file @@ -218,54 +400,86 @@ def combine_HEX_Caron_errors(LMAX, RAD, mass_file.chmod(mode=MODE) cmwe_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the estimated GIA uncertainty for spherical cap mascon regions """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -282,7 +496,8 @@ def main(): DESTRIPE=args.destripe, REDISTRIBUTE_MASCONS=args.redistribute_mascons, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -290,6 +505,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/combine_HEX_OBP_errors.py b/scripts/combine_HEX_OBP_errors.py index c6d02041..3794a2ad 100644 --- a/scripts/combine_HEX_OBP_errors.py +++ b/scripts/combine_HEX_OBP_errors.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_HEX_OBP_errors.py (04/2024) calculates the time-series of ocean mass leakage for spherical cap mascons @@ -55,6 +55,7 @@ output average time series in equivalent surface pressure difference Written 01/2018 """ + from __future__ import print_function import sys @@ -71,102 +72,280 @@ # Regions region = [] -region.extend(['AAp', 'ApB', 'BC', 'CCp', 'CpD', 'DDp', 'DpE', 'EEp', 'EpFp', - 'FpG', 'GH', 'HHp', 'HpI', 'IIpp', 'IppJ', 'JJpp', 'JppK','KKp', 'KpA', - 'INTERIOR', 'AIS', 'EAIS', 'WAIS', 'APIS', 'QML', 'NoQML', 'CpDc','CpDi', - 'DDpc','DDpi','GH2','HHp2','JJpp2','GH3','TM','PIG','THSPK']) -region.extend(['NW','NN','NE','SW','SE','GIS','CDE','CBI','ICL','SVB','ALK', - 'DEN','PNW','PAT','FJL','SZEM','NZEM','RUS','ARC','GIC']) +region.extend( + [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', + 'INTERIOR', + 'AIS', + 'EAIS', + 'WAIS', + 'APIS', + 'QML', + 'NoQML', + 'CpDc', + 'CpDi', + 'DDpc', + 'DDpi', + 'GH2', + 'HHp2', + 'JJpp2', + 'GH3', + 'TM', + 'PIG', + 'THSPK', + ] +) +region.extend( + [ + 'NW', + 'NN', + 'NE', + 'SW', + 'SE', + 'GIS', + 'CDE', + 'CBI', + 'ICL', + 'SVB', + 'ALK', + 'DEN', + 'PNW', + 'PAT', + 'FJL', + 'SZEM', + 'NZEM', + 'RUS', + 'ARC', + 'GIC', + ] +) # Cap numbers for each basin cap = {} # the contents of the cap variable will be referenced to the basin name # the cap numbers are the global numbers for the spherical cap RADius -cap['AAp'] = np.array([25,26,39,40,41,53,54]) -cap['ApB'] = np.array([55,56,67,68,69,70,83,97]) -cap['BC'] = np.array([80,81,82,93,94,95,96,107,108,109,121,122]) -cap['CCp'] = np.array([110,123,124,136,137,138,150,151]) -cap['CpD'] = np.array([135,148,149,162,163,164,175,176,177,189,190]) -cap['DDp'] = np.array([174,186,187,188,200,201,202]) -cap['DpE'] = np.array([158,159,160,161,172,173,185,199]) -cap['EEp'] = np.array([106,117,118,119,120,131,132,133,134,145,146,147]) -cap['EpFp'] = np.array([102,103,115,116,128,129,130,142,143]) -cap['FpG'] = np.array([127,141]) -cap['GH'] = np.array([86,100,101,113,114]) -cap['HHp'] = np.array([72,85,99]) -cap['HpI'] = np.array([45,59]) -cap['IIpp'] = np.array([4,17,18,31,32]) +cap['AAp'] = np.array([25, 26, 39, 40, 41, 53, 54]) +cap['ApB'] = np.array([55, 56, 67, 68, 69, 70, 83, 97]) +cap['BC'] = np.array([80, 81, 82, 93, 94, 95, 96, 107, 108, 109, 121, 122]) +cap['CCp'] = np.array([110, 123, 124, 136, 137, 138, 150, 151]) +cap['CpD'] = np.array([135, 148, 149, 162, 163, 164, 175, 176, 177, 189, 190]) +cap['DDp'] = np.array([174, 186, 187, 188, 200, 201, 202]) +cap['DpE'] = np.array([158, 159, 160, 161, 172, 173, 185, 199]) +cap['EEp'] = np.array( + [106, 117, 118, 119, 120, 131, 132, 133, 134, 145, 146, 147] +) +cap['EpFp'] = np.array([102, 103, 115, 116, 128, 129, 130, 142, 143]) +cap['FpG'] = np.array([127, 141]) +cap['GH'] = np.array([86, 100, 101, 113, 114]) +cap['HHp'] = np.array([72, 85, 99]) +cap['HpI'] = np.array([45, 59]) +cap['IIpp'] = np.array([4, 17, 18, 31, 32]) cap['IppJ'] = np.array([46]) -cap['JJpp'] = np.array([60,73,74,75,87,88,89]) -cap['JppK'] = np.array([49,50,51,52,62,63,64,65,66,76,77,78,79,90,91,92,104,105]) -cap['KKp'] = np.array([23,36,37]) -cap['KpA'] = np.array([24,38]) -cap['SHELVES'] = np.array([61,144,157]) +cap['JJpp'] = np.array([60, 73, 74, 75, 87, 88, 89]) +cap['JppK'] = np.array( + [49, 50, 51, 52, 62, 63, 64, 65, 66, 76, 77, 78, 79, 90, 91, 92, 104, 105] +) +cap['KKp'] = np.array([23, 36, 37]) +cap['KpA'] = np.array([24, 38]) +cap['SHELVES'] = np.array([61, 144, 157]) # Grouping Basins into larger regions (AIS, EAIS, WAIS and APEN) -cap['AIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['EpFp'], cap['FpG'], \ - cap['GH'], cap['HHp'], cap['HpI'], cap['IIpp'], cap['IppJ'], cap['JJpp'], \ - cap['JppK'], cap['KKp'], cap['KpA']),axis=0) #, cap['SHELVES'] -cap['EAIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp'], \ - cap['KpA']),axis=0) -cap['WAIS'] = np.concatenate((cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']),axis=0) -cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']),axis=0) -cap['INTERIOR'] = np.array([38,51,52,53,65,66,67,68,78,79,80,81,91,92,93,94,\ - 104,105,106,107,108,118,119,120,121,122,123,133,134,135,136,147,148,149,\ - 160,161,162,174,175]) -cap['QML'] = np.concatenate((cap['KpA'],cap['AAp'],cap['ApB']),axis=0) -cap['NoQML'] = np.concatenate((cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp']),axis=0) +cap['AIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['EpFp'], + cap['FpG'], + cap['GH'], + cap['HHp'], + cap['HpI'], + cap['IIpp'], + cap['IppJ'], + cap['JJpp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) # , cap['SHELVES'] +cap['EAIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) +cap['WAIS'] = np.concatenate( + (cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']), axis=0 +) +cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']), axis=0) +cap['INTERIOR'] = np.array( + [ + 38, + 51, + 52, + 53, + 65, + 66, + 67, + 68, + 78, + 79, + 80, + 81, + 91, + 92, + 93, + 94, + 104, + 105, + 106, + 107, + 108, + 118, + 119, + 120, + 121, + 122, + 123, + 133, + 134, + 135, + 136, + 147, + 148, + 149, + 160, + 161, + 162, + 174, + 175, + ] +) +cap['QML'] = np.concatenate((cap['KpA'], cap['AAp'], cap['ApB']), axis=0) +cap['NoQML'] = np.concatenate( + ( + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + ), + axis=0, +) cap['ISLAND'] = np.array([62]) -cap['CpDc'] = np.array([163,164,176,177,189,190]) -#cap['CpDc'] = np.array([163,164,177]) -cap['CpDi'] = np.array([135,148,149,162,175]) -cap['TM'] = np.array([135,148,149,162,163,176,177,190]) -cap['DDpc'] = np.array([186,187,188,200,201,202]) -#cap['DDpi'] = np.array([174]) -cap['DDpi'] = np.concatenate((cap['DDp'],cap['DpE']),axis=0) +cap['CpDc'] = np.array([163, 164, 176, 177, 189, 190]) +# cap['CpDc'] = np.array([163,164,177]) +cap['CpDi'] = np.array([135, 148, 149, 162, 175]) +cap['TM'] = np.array([135, 148, 149, 162, 163, 176, 177, 190]) +cap['DDpc'] = np.array([186, 187, 188, 200, 201, 202]) +# cap['DDpi'] = np.array([174]) +cap['DDpi'] = np.concatenate((cap['DDp'], cap['DpE']), axis=0) # different version of west ant -cap['GH2'] = np.array([86,87,99,100,101,113,114]) -cap['HHp2'] = np.array([72,85]) -cap['JJpp2'] = np.array([60,73,74,75,88,89]) -cap['GH3'] = np.array([86,99,100,101,113,114]) +cap['GH2'] = np.array([86, 87, 99, 100, 101, 113, 114]) +cap['HHp2'] = np.array([72, 85]) +cap['JJpp2'] = np.array([60, 73, 74, 75, 88, 89]) +cap['GH3'] = np.array([86, 99, 100, 101, 113, 114]) # Amundsen Sea Embayment regions -cap['PIG'] = np.array([86,87,99]) -cap['THSPK'] = np.array([100,101,113,114]) +cap['PIG'] = np.array([86, 87, 99]) +cap['THSPK'] = np.array([100, 101, 113, 114]) # Greenland Mascons -cap['NW'] = np.array([308,309,312,313,316,317]) -cap['NN'] = np.array([301,302,303,304,305,306]) -cap['NE'] = np.array([307,310,311,314,315]) -cap['SE'] = np.array([318,319,322,323,325,327]) -cap['SW'] = np.array([320,321,324,326]) -cap['GIS'] = np.concatenate((cap['NW'],cap['NN'],cap['NE'],cap['SW'],cap['SE']),axis=0) +cap['NW'] = np.array([308, 309, 312, 313, 316, 317]) +cap['NN'] = np.array([301, 302, 303, 304, 305, 306]) +cap['NE'] = np.array([307, 310, 311, 314, 315]) +cap['SE'] = np.array([318, 319, 322, 323, 325, 327]) +cap['SW'] = np.array([320, 321, 324, 326]) +cap['GIS'] = np.concatenate( + (cap['NW'], cap['NN'], cap['NE'], cap['SW'], cap['SE']), axis=0 +) # Canadian Archipelago -cap['CDE'] = np.array([328,329,330,331,332,333]) -cap['CBI'] = np.array([334,335,336,337,338]) +cap['CDE'] = np.array([328, 329, 330, 331, 332, 333]) +cap['CBI'] = np.array([334, 335, 336, 337, 338]) # Iceland, Svalbard, Franz Josef Land, Svernaya Zemlya and Novaya Zemlya cap['ICL'] = np.array([339]) cap['SVB'] = np.array([340]) cap['FJL'] = np.array([341]) cap['SZEM'] = np.array([342]) -cap['NZEM'] = np.array([343,344]) +cap['NZEM'] = np.array([343, 344]) # Alaska (Denali and Pacific Northwest) -cap['ALK'] = np.arange(345,368) -cap['DEN'] = np.array([345,346,348,349,350,351,352,353,354,355,356,367]) -cap['PNW'] = np.array([347,357,358,359,360,361,362,363,364,365,366]) +cap['ALK'] = np.arange(345, 368) +cap['DEN'] = np.array( + [345, 346, 348, 349, 350, 351, 352, 353, 354, 355, 356, 367] +) +cap['PNW'] = np.array([347, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366]) # Patagonia -cap['PAT'] = np.arange(400,407) +cap['PAT'] = np.arange(400, 407) # All Arctic and all GIC (with patagonia) -cap['ARC'] = np.concatenate((cap['GIS'],cap['CDE'],cap['CBI'],cap['ICL'], - cap['SVB'],cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) -cap['GIC'] = np.concatenate((cap['CDE'],cap['CBI'],cap['ICL'],cap['SVB'], - cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM'],cap['PAT']),axis=0) +cap['ARC'] = np.concatenate( + ( + cap['GIS'], + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + ), + axis=0, +) +cap['GIC'] = np.concatenate( + ( + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + cap['PAT'], + ), + axis=0, +) # Russian Arctic (Franz Josef Land, Svernaya Zemlya and Novaya Zemlya) -cap['RUS'] = np.concatenate((cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) +cap['RUS'] = np.concatenate((cap['FJL'], cap['SZEM'], cap['NZEM']), axis=0) # All caps (Greenland, Glaciers and Ice Caps, Antarctica) -cap['ALL'] = np.concatenate((cap['GIS'],cap['GIC'],cap['AIS']),axis=0) +cap['ALL'] = np.concatenate((cap['GIS'], cap['GIC'], cap['AIS']), axis=0) + # PURPOSE: keep track of threads def info(args): @@ -176,13 +355,19 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') -def combine_HEX_OBP_errors(base_dir, MODEL, DSET, LMAX, RAD, + +def combine_HEX_OBP_errors( + base_dir, + MODEL, + DSET, + LMAX, + RAD, MMAX=None, DESTRIPE=False, REDISTRIBUTE_MASCONS=False, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -216,8 +401,14 @@ def combine_HEX_OBP_errors(base_dir, MODEL, DSET, LMAX, RAD, PREFIX['V4r4'] = 'ECCO_V4r4' PREFIX['V5alpha'] = 'ECCO_V5alpha' PREFIX['Cube92'] = 'ECCO_Cube92' - RANGE = {'kf080i':(4,237),'dr080i':(4,237),'V4r3':(4,168),'V4r4':(4,192), - 'V5alpha':(4,192),'Cube92':(4,216)} + RANGE = { + 'kf080i': (4, 237), + 'dr080i': (4, 237), + 'V4r3': (4, 168), + 'V4r4': (4, 192), + 'V5alpha': (4, 192), + 'Cube92': (4, 216), + } # read each mascon file and store in dictionary with k of the cap number grace_data = {} @@ -235,45 +426,47 @@ def combine_HEX_OBP_errors(base_dir, MODEL, DSET, LMAX, RAD, # Read each spherical cap for k in cap['ALL']: # GRACE files - a=(RAD_CAP,k,DSET,ocean_str,LMAX,order_str,gw_str,ds_str) - f1='SPH_CAP_RAD{0:0.1f}_{1:d}_{2}{3}_L{4:d}{5}{6}{7}.txt'.format(*a) + a = (RAD_CAP, k, DSET, ocean_str, LMAX, order_str, gw_str, ds_str) + f1 = 'SPH_CAP_RAD{0:0.1f}_{1:d}_{2}{3}_L{4:d}{5}{6}{7}.txt'.format(*a) # read cap time-series for GRACE GAD grace_data[k] = np.loadtxt(OUTPUT_DIRECTORY.joinpath(f1)) # ECCO obp anomalies (kf080i and dr080i models) # ECCO2 Cube92 obp anomalies for M in MODEL: # directories and file for ocean bottom pressure anomalies - a = (PREFIX[M],'AveRmvd','OBP',LMAX,RANGE[M][0],RANGE[M][1]) - s2='{0}_{1}_{2}_SPH_CAP_MSCNS_L{3:d}_{4:03d}-{5:03d}'.format(*a) - a = (PREFIX[M],'OBP',RAD_CAP,k,LMAX,gw_str,ocean_str) - f2='{0}_{1}_SPH_CAP_RAD{2:0.1f}_{3:d}_L{4:d}{5}{6}.txt'.format(*a) + a = (PREFIX[M], 'AveRmvd', 'OBP', LMAX, RANGE[M][0], RANGE[M][1]) + s2 = '{0}_{1}_{2}_SPH_CAP_MSCNS_L{3:d}_{4:03d}-{5:03d}'.format(*a) + a = (PREFIX[M], 'OBP', RAD_CAP, k, LMAX, gw_str, ocean_str) + f2 = '{0}_{1}_SPH_CAP_RAD{2:0.1f}_{3:d}_L{4:d}{5}{6}.txt'.format(*a) # read ocean bottom pressure anomalies and verify shape - obp_file = base_dir.joinpath(*OBP_DIRECTORY[M],s2,f2) + obp_file = base_dir.joinpath(*OBP_DIRECTORY[M], s2, f2) OBP[M][k] = np.loadtxt(obp_file, ndmin=2) # date information - months = grace_data[k][:,0].astype(np.int64) + months = grace_data[k][:, 0].astype(np.int64) start_mon = np.min(months) end_mon = np.max(months) - missing = list(set(np.arange(start_mon,end_mon+1)) - set(months)) + missing = list(set(np.arange(start_mon, end_mon + 1)) - set(months)) for M in MODEL: - OBP_months = OBP[M][k][:,0].astype(np.int64) - if (np.min(OBP_months) < start_mon): + OBP_months = OBP[M][k][:, 0].astype(np.int64) + if np.min(OBP_months) < start_mon: start_mon = np.min(OBP_months) - if (np.max(OBP_months) > end_mon): + if np.max(OBP_months) > end_mon: end_mon = np.max(OBP_months) # find missing months for any dataset - missing.extend(list(set(np.arange(start_mon,end_mon+1))-set(OBP_months))) + missing.extend( + list(set(np.arange(start_mon, end_mon + 1)) - set(OBP_months)) + ) # GRACE/GRACE-FO months - mon = np.arange(start_mon,end_mon+1) + mon = np.arange(start_mon, end_mon + 1) missing = sorted(set(missing)) n_mon = len(mon) # GRACE/GRACE-FO dates - calendar_year = 2002 + (mon-1)//12 - calendar_month = np.mod(mon-1,12) + 1 - tdec = gravtk.time.convert_calendar_decimal(calendar_year,calendar_month) + calendar_year = 2002 + (mon - 1) // 12 + calendar_month = np.mod(mon - 1, 12) + 1 + tdec = gravtk.time.convert_calendar_decimal(calendar_year, calendar_month) # remove mean of 2003--2007 - m0307, = np.nonzero((mon >= 13) & (mon <= 72)) + (m0307,) = np.nonzero((mon >= 13) & (mon <= 72)) # for each region for i in region: @@ -286,38 +479,46 @@ def combine_HEX_OBP_errors(base_dir, MODEL, DSET, LMAX, RAD, # sum mascons in region for k in cap[i]: # sum of GRACE GAD data - for j,m in enumerate(mon): + for j, m in enumerate(mon): if m not in months: continue - ind, = np.nonzero(months == m) - grace_reg[i][j] += grace_data[k][ind,2] + (ind,) = np.nonzero(months == m) + grace_reg[i][j] += grace_data[k][ind, 2] # RMS sum of ECCO-GAD residuals for M in MODEL: - OBP_months = OBP[M][k][:,0].astype(np.int64) - for j,m in enumerate(mon): + OBP_months = OBP[M][k][:, 0].astype(np.int64) + for j, m in enumerate(mon): if m not in OBP_months: continue - ind, = np.nonzero(OBP_months == m) - OBP_mass[M][i][j] += OBP[M][k][ind,2] + (ind,) = np.nonzero(OBP_months == m) + OBP_mass[M][i][j] += OBP[M][k][ind, 2] # add mascon area to total area (cm^2) - area_reg[i] += 1e10*grace_data[k][0,3] + area_reg[i] += 1e10 * grace_data[k][0, 3] # RMS sum of ECCO OBP residuals components mass1 = grace_reg[i] - grace_reg[i][m0307].mean() # calculate variance off of GAD model OBP_variance = np.zeros((n_mon)) - for c,M in enumerate(MODEL): + for c, M in enumerate(MODEL): mass2 = OBP_mass[M][i] - OBP_mass[M][i][m0307].mean() - OBP_variance += (mass1 - mass2)**2 + OBP_variance += (mass1 - mass2) ** 2 # RMS sum of reanalysis residuals components - OBP_reg[i] = np.sqrt(OBP_variance/(len(MODEL)-1.0)) + OBP_reg[i] = np.sqrt(OBP_variance / (len(MODEL) - 1.0)) # replace invalid values with nan for m in missing: - ind, = np.nonzero(mon == m) + (ind,) = np.nonzero(mon == m) OBP_reg[i][ind] = np.nan # output data files - args=('ECCO-GAD_OBP_Residuals',i,ocean_str,LMAX,order_str,gw_str,ds_str) + args = ( + 'ECCO-GAD_OBP_Residuals', + i, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + ) FILE1 = '{0}_{1}_SPH_CAP{2}_L{3:d}{4}{5}{6}.txt'.format(*args) FILE2 = '{0}_{1}_SPH_CAP{2}_L{3:d}{4}{5}{6}_mbar.txt'.format(*args) # open files for writing region time-series @@ -329,7 +530,7 @@ def combine_HEX_OBP_errors(base_dir, MODEL, DSET, LMAX, RAD, # output GRACE regional time-series for t in range(n_mon): mass_error = OBP_reg[i][t] - mbar_error = 1e14*g_wmo*OBP_reg[i][t]/area_reg[i] + mbar_error = 1e14 * g_wmo * OBP_reg[i][t] / area_reg[i] print(f'{mon[t]:03d} {tdec[t]:12.4f} {mass_error:14.6f}', file=fid1) print(f'{mon[t]:03d} {tdec[t]:12.4f} {mbar_error:14.6f}', file=fid2) # close the output file @@ -339,69 +540,114 @@ def combine_HEX_OBP_errors(base_dir, MODEL, DSET, LMAX, RAD, mass_file.chmod(mode=MODE) mbar_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""calculates the time-series of ocean mass leakage for spherical cap mascons """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - choices = ['kf080i','dr080i','V4r3','V4r4','V5alpha','Cube92'] - parser.add_argument('model', - metavar='MODEL', type=str, nargs='+', - default=['kf080i','dr080i'], choices=choices, - help='ECCO Models') + choices = ['kf080i', 'dr080i', 'V4r3', 'V4r4', 'V5alpha', 'Cube92'] + parser.add_argument( + 'model', + metavar='MODEL', + type=str, + nargs='+', + default=['kf080i', 'dr080i'], + choices=choices, + help='ECCO Models', + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GAD', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GAD', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # uniformly redistribute mascon mass over the ocean - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -421,7 +667,8 @@ def main(): DESTRIPE=args.destripe, REDISTRIBUTE_MASCONS=args.redistribute_mascons, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -429,6 +676,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/combine_HEX_SLF_errors.py b/scripts/combine_HEX_SLF_errors.py index 0c964332..0767a6ca 100644 --- a/scripts/combine_HEX_SLF_errors.py +++ b/scripts/combine_HEX_SLF_errors.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_HEX_SLF_errors.py (04/2024) calculates the estimated SLF uncertainty for spherical cap mascon regions @@ -39,6 +39,7 @@ adjusted output filename to fit a more common format Written 04/2018 """ + from __future__ import print_function import sys @@ -55,102 +56,280 @@ # Regions region = [] -region.extend(['AAp', 'ApB', 'BC', 'CCp', 'CpD', 'DDp', 'DpE', 'EEp', 'EpFp', - 'FpG', 'GH', 'HHp', 'HpI', 'IIpp', 'IppJ', 'JJpp', 'JppK','KKp', 'KpA', - 'INTERIOR', 'AIS', 'EAIS', 'WAIS', 'APIS', 'QML', 'NoQML', 'CpDc','CpDi', - 'DDpc','DDpi','GH2','HHp2','JJpp2','GH3','TM','PIG','THSPK']) -region.extend(['NW','NN','NE','SW','SE','GIS','CDE','CBI','ICL','SVB','ALK', - 'DEN','PNW','PAT','FJL','SZEM','NZEM','RUS','ARC','GIC']) +region.extend( + [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', + 'INTERIOR', + 'AIS', + 'EAIS', + 'WAIS', + 'APIS', + 'QML', + 'NoQML', + 'CpDc', + 'CpDi', + 'DDpc', + 'DDpi', + 'GH2', + 'HHp2', + 'JJpp2', + 'GH3', + 'TM', + 'PIG', + 'THSPK', + ] +) +region.extend( + [ + 'NW', + 'NN', + 'NE', + 'SW', + 'SE', + 'GIS', + 'CDE', + 'CBI', + 'ICL', + 'SVB', + 'ALK', + 'DEN', + 'PNW', + 'PAT', + 'FJL', + 'SZEM', + 'NZEM', + 'RUS', + 'ARC', + 'GIC', + ] +) # Cap numbers for each basin cap = {} # the contents of the cap variable will be referenced to the basin name # the cap numbers are the global numbers for the spherical cap RADius -cap['AAp'] = np.array([25,26,39,40,41,53,54]) -cap['ApB'] = np.array([55,56,67,68,69,70,83,97]) -cap['BC'] = np.array([80,81,82,93,94,95,96,107,108,109,121,122]) -cap['CCp'] = np.array([110,123,124,136,137,138,150,151]) -cap['CpD'] = np.array([135,148,149,162,163,164,175,176,177,189,190]) -cap['DDp'] = np.array([174,186,187,188,200,201,202]) -cap['DpE'] = np.array([158,159,160,161,172,173,185,199]) -cap['EEp'] = np.array([106,117,118,119,120,131,132,133,134,145,146,147]) -cap['EpFp'] = np.array([102,103,115,116,128,129,130,142,143]) -cap['FpG'] = np.array([127,141]) -cap['GH'] = np.array([86,100,101,113,114]) -cap['HHp'] = np.array([72,85,99]) -cap['HpI'] = np.array([45,59]) -cap['IIpp'] = np.array([4,17,18,31,32]) +cap['AAp'] = np.array([25, 26, 39, 40, 41, 53, 54]) +cap['ApB'] = np.array([55, 56, 67, 68, 69, 70, 83, 97]) +cap['BC'] = np.array([80, 81, 82, 93, 94, 95, 96, 107, 108, 109, 121, 122]) +cap['CCp'] = np.array([110, 123, 124, 136, 137, 138, 150, 151]) +cap['CpD'] = np.array([135, 148, 149, 162, 163, 164, 175, 176, 177, 189, 190]) +cap['DDp'] = np.array([174, 186, 187, 188, 200, 201, 202]) +cap['DpE'] = np.array([158, 159, 160, 161, 172, 173, 185, 199]) +cap['EEp'] = np.array( + [106, 117, 118, 119, 120, 131, 132, 133, 134, 145, 146, 147] +) +cap['EpFp'] = np.array([102, 103, 115, 116, 128, 129, 130, 142, 143]) +cap['FpG'] = np.array([127, 141]) +cap['GH'] = np.array([86, 100, 101, 113, 114]) +cap['HHp'] = np.array([72, 85, 99]) +cap['HpI'] = np.array([45, 59]) +cap['IIpp'] = np.array([4, 17, 18, 31, 32]) cap['IppJ'] = np.array([46]) -cap['JJpp'] = np.array([60,73,74,75,87,88,89]) -cap['JppK'] = np.array([49,50,51,52,62,63,64,65,66,76,77,78,79,90,91,92,104,105]) -cap['KKp'] = np.array([23,36,37]) -cap['KpA'] = np.array([24,38]) -cap['SHELVES'] = np.array([61,144,157]) +cap['JJpp'] = np.array([60, 73, 74, 75, 87, 88, 89]) +cap['JppK'] = np.array( + [49, 50, 51, 52, 62, 63, 64, 65, 66, 76, 77, 78, 79, 90, 91, 92, 104, 105] +) +cap['KKp'] = np.array([23, 36, 37]) +cap['KpA'] = np.array([24, 38]) +cap['SHELVES'] = np.array([61, 144, 157]) # Grouping Basins into larger regions (AIS, EAIS, WAIS and APEN) -cap['AIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['EpFp'], cap['FpG'], \ - cap['GH'], cap['HHp'], cap['HpI'], cap['IIpp'], cap['IppJ'], cap['JJpp'], \ - cap['JppK'], cap['KKp'], cap['KpA']),axis=0) #, cap['SHELVES'] -cap['EAIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp'], \ - cap['KpA']),axis=0) -cap['WAIS'] = np.concatenate((cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']),axis=0) -cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']),axis=0) -cap['INTERIOR'] = np.array([38,51,52,53,65,66,67,68,78,79,80,81,91,92,93,94,\ - 104,105,106,107,108,118,119,120,121,122,123,133,134,135,136,147,148,149,\ - 160,161,162,174,175]) -cap['QML'] = np.concatenate((cap['KpA'],cap['AAp'],cap['ApB']),axis=0) -cap['NoQML'] = np.concatenate((cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp']),axis=0) +cap['AIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['EpFp'], + cap['FpG'], + cap['GH'], + cap['HHp'], + cap['HpI'], + cap['IIpp'], + cap['IppJ'], + cap['JJpp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) # , cap['SHELVES'] +cap['EAIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) +cap['WAIS'] = np.concatenate( + (cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']), axis=0 +) +cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']), axis=0) +cap['INTERIOR'] = np.array( + [ + 38, + 51, + 52, + 53, + 65, + 66, + 67, + 68, + 78, + 79, + 80, + 81, + 91, + 92, + 93, + 94, + 104, + 105, + 106, + 107, + 108, + 118, + 119, + 120, + 121, + 122, + 123, + 133, + 134, + 135, + 136, + 147, + 148, + 149, + 160, + 161, + 162, + 174, + 175, + ] +) +cap['QML'] = np.concatenate((cap['KpA'], cap['AAp'], cap['ApB']), axis=0) +cap['NoQML'] = np.concatenate( + ( + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + ), + axis=0, +) cap['ISLAND'] = np.array([62]) -cap['CpDc'] = np.array([163,164,176,177,189,190]) -#cap['CpDc'] = np.array([163,164,177]) -cap['CpDi'] = np.array([135,148,149,162,175]) -cap['TM'] = np.array([135,148,149,162,163,176,177,190]) -cap['DDpc'] = np.array([186,187,188,200,201,202]) -#cap['DDpi'] = np.array([174]) -cap['DDpi'] = np.concatenate((cap['DDp'],cap['DpE']),axis=0) +cap['CpDc'] = np.array([163, 164, 176, 177, 189, 190]) +# cap['CpDc'] = np.array([163,164,177]) +cap['CpDi'] = np.array([135, 148, 149, 162, 175]) +cap['TM'] = np.array([135, 148, 149, 162, 163, 176, 177, 190]) +cap['DDpc'] = np.array([186, 187, 188, 200, 201, 202]) +# cap['DDpi'] = np.array([174]) +cap['DDpi'] = np.concatenate((cap['DDp'], cap['DpE']), axis=0) # different version of west ant -cap['GH2'] = np.array([86,87,99,100,101,113,114]) -cap['HHp2'] = np.array([72,85]) -cap['JJpp2'] = np.array([60,73,74,75,88,89]) -cap['GH3'] = np.array([86,99,100,101,113,114]) +cap['GH2'] = np.array([86, 87, 99, 100, 101, 113, 114]) +cap['HHp2'] = np.array([72, 85]) +cap['JJpp2'] = np.array([60, 73, 74, 75, 88, 89]) +cap['GH3'] = np.array([86, 99, 100, 101, 113, 114]) # Amundsen Sea Embayment regions -cap['PIG'] = np.array([86,87,99]) -cap['THSPK'] = np.array([100,101,113,114]) +cap['PIG'] = np.array([86, 87, 99]) +cap['THSPK'] = np.array([100, 101, 113, 114]) # Greenland Mascons -cap['NW'] = np.array([308,309,312,313,316,317]) -cap['NN'] = np.array([301,302,303,304,305,306]) -cap['NE'] = np.array([307,310,311,314,315]) -cap['SE'] = np.array([318,319,322,323,325,327]) -cap['SW'] = np.array([320,321,324,326]) -cap['GIS'] = np.concatenate((cap['NW'],cap['NN'],cap['NE'],cap['SW'],cap['SE']),axis=0) +cap['NW'] = np.array([308, 309, 312, 313, 316, 317]) +cap['NN'] = np.array([301, 302, 303, 304, 305, 306]) +cap['NE'] = np.array([307, 310, 311, 314, 315]) +cap['SE'] = np.array([318, 319, 322, 323, 325, 327]) +cap['SW'] = np.array([320, 321, 324, 326]) +cap['GIS'] = np.concatenate( + (cap['NW'], cap['NN'], cap['NE'], cap['SW'], cap['SE']), axis=0 +) # Canadian Archipelago -cap['CDE'] = np.array([328,329,330,331,332,333]) -cap['CBI'] = np.array([334,335,336,337,338]) +cap['CDE'] = np.array([328, 329, 330, 331, 332, 333]) +cap['CBI'] = np.array([334, 335, 336, 337, 338]) # Iceland, Svalbard, Franz Josef Land, Svernaya Zemlya and Novaya Zemlya cap['ICL'] = np.array([339]) cap['SVB'] = np.array([340]) cap['FJL'] = np.array([341]) cap['SZEM'] = np.array([342]) -cap['NZEM'] = np.array([343,344]) +cap['NZEM'] = np.array([343, 344]) # Alaska (Denali and Pacific Northwest) -cap['ALK'] = np.arange(345,368) -cap['DEN'] = np.array([345,346,348,349,350,351,352,353,354,355,356,367]) -cap['PNW'] = np.array([347,357,358,359,360,361,362,363,364,365,366]) +cap['ALK'] = np.arange(345, 368) +cap['DEN'] = np.array( + [345, 346, 348, 349, 350, 351, 352, 353, 354, 355, 356, 367] +) +cap['PNW'] = np.array([347, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366]) # Patagonia -cap['PAT'] = np.arange(400,407) +cap['PAT'] = np.arange(400, 407) # All Arctic and all GIC (with patagonia) -cap['ARC'] = np.concatenate((cap['GIS'],cap['CDE'],cap['CBI'],cap['ICL'], - cap['SVB'],cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) -cap['GIC'] = np.concatenate((cap['CDE'],cap['CBI'],cap['ICL'],cap['SVB'], - cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM'],cap['PAT']),axis=0) +cap['ARC'] = np.concatenate( + ( + cap['GIS'], + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + ), + axis=0, +) +cap['GIC'] = np.concatenate( + ( + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + cap['PAT'], + ), + axis=0, +) # Russian Arctic (Franz Josef Land, Svernaya Zemlya and Novaya Zemlya) -cap['RUS'] = np.concatenate((cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) +cap['RUS'] = np.concatenate((cap['FJL'], cap['SZEM'], cap['NZEM']), axis=0) # All caps (Greenland, Glaciers and Ice Caps, Antarctica) -cap['ALL'] = np.concatenate((cap['GIS'],cap['GIC'],cap['AIS']),axis=0) +cap['ALL'] = np.concatenate((cap['GIS'], cap['GIC'], cap['AIS']), axis=0) + # PURPOSE: keep track of threads def info(args): @@ -160,14 +339,17 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') -def combine_HEX_SLF_errors(LMAX, RAD, + +def combine_HEX_SLF_errors( + LMAX, + RAD, MMAX=None, DESTRIPE=False, REDISTRIBUTE_MASCONS=False, REDISTRIBUTE_REMOVED=False, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -191,13 +373,13 @@ def combine_HEX_SLF_errors(LMAX, RAD, # Read each spherical cap for k in cap['ALL']: # GRACE files - a1=(RAD_CAP,k,LMAX,order_str,gw_str,ds_str,ocean_str) - f1='MC_SPH_CAP_RAD{0:0.1f}_{1:d}_L{2:d}{3}{4}{5}{6}.txt'.format(*a1) + a1 = (RAD_CAP, k, LMAX, order_str, gw_str, ds_str, ocean_str) + f1 = 'MC_SPH_CAP_RAD{0:0.1f}_{1:d}_L{2:d}{3}{4}{5}{6}.txt'.format(*a1) # read cap time-series and verify shape SLF_data[k] = np.loadtxt(OUTPUT_DIRECTORY.joinpath(f1), ndmin=2) # monte carlo information - RUNS = len(SLF_data[k][:,0]) + RUNS = len(SLF_data[k][:, 0]) # for each region for i in region: @@ -208,14 +390,18 @@ def combine_HEX_SLF_errors(LMAX, RAD, # sum mascons in region for k in cap[i]: # sum of SLF uncertainty data - SLF_reg[i] += SLF_data[k][:,0]**2 + SLF_reg[i] += SLF_data[k][:, 0] ** 2 # add mascon area to total area (cm^2) - area_reg[i] += 1e10*SLF_data[k][0,1] + area_reg[i] += 1e10 * SLF_data[k][0, 1] # output data files - args = (i,RAD_CAP,ocean_str,LMAX,order_str,gw_str,ds_str) - FILE1='MC_{0}_SPH_CAP_RAD{1:0.1f}{2}_L{3:d}{4}{5}{6}.txt'.format(*args) - FILE2='MC_{0}_SPH_CAP_RAD{1:0.1f}{2}_L{3:d}{4}{5}{6}_cmwe.txt'.format(*args) + args = (i, RAD_CAP, ocean_str, LMAX, order_str, gw_str, ds_str) + FILE1 = 'MC_{0}_SPH_CAP_RAD{1:0.1f}{2}_L{3:d}{4}{5}{6}.txt'.format( + *args + ) + FILE2 = 'MC_{0}_SPH_CAP_RAD{1:0.1f}{2}_L{3:d}{4}{5}{6}_cmwe.txt'.format( + *args + ) # open files for writing region time-series mass_file = OUTPUT_DIRECTORY.joinpath(FILE1) cmwe_file = OUTPUT_DIRECTORY.joinpath(FILE2) @@ -224,7 +410,7 @@ def combine_HEX_SLF_errors(LMAX, RAD, fid2 = cmwe_file.open(mode='w', encoding='utf8') # calculate the RMS of the SLF for the region in mass and cm thickness SLF_RMS_reg = np.sqrt(SLF_reg[i]) - SLF_RMS_thick = 1e15*np.sqrt(SLF_reg[i])/area_reg[i] + SLF_RMS_thick = 1e15 * np.sqrt(SLF_reg[i]) / area_reg[i] for n in range(RUNS): print(f'{n:05d} {SLF_RMS_reg[n]:14.6f}', file=fid1) print(f'{n:05d} {SLF_RMS_thick[n]:14.6f}', file=fid2) @@ -235,57 +421,92 @@ def combine_HEX_SLF_errors(LMAX, RAD, mass_file.chmod(mode=MODE) cmwe_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the estimated SLF uncertainty for spherical cap mascon regions """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -303,7 +524,8 @@ def main(): REDISTRIBUTE_MASCONS=args.redistribute_mascons, REDISTRIBUTE_REMOVED=args.redistribute_removed, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -311,6 +533,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/combine_HEX_TWC_errors.py b/scripts/combine_HEX_TWC_errors.py index 9ece0745..881fe03f 100644 --- a/scripts/combine_HEX_TWC_errors.py +++ b/scripts/combine_HEX_TWC_errors.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_HEX_TWC_errors.py (04/2024) calculates the time-series of terrestrial mass leakage for spherical cap mascons using outputs from least_squares_mascon_timeseries.py @@ -54,6 +54,7 @@ Updated 03/2018: simplified flag setting algorithms to single lines Written 03/2018 """ + from __future__ import print_function import sys @@ -70,80 +71,229 @@ # Regions region = [] -region.extend(['AAp', 'ApB', 'BC', 'CCp', 'CpD', 'DDp', 'DpE', 'EEp', 'EpFp', - 'FpG', 'GH', 'HHp', 'HpI', 'IIpp', 'IppJ', 'JJpp', 'JppK','KKp', 'KpA', - 'INTERIOR', 'AIS', 'EAIS', 'WAIS', 'APIS', 'QML', 'NoQML', 'CpDc','CpDi', - 'DDpc','DDpi','GH2','HHp2','JJpp2','GH3','TM']) -region.extend(['NW','NN','NE','SW','SE','GIS','CDE','CBI','ICL','SVB','ALK', - 'DEN','PNW','PAT','FJL','SZEM','NZEM','RUS','ARC','GIC']) +region.extend( + [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', + 'INTERIOR', + 'AIS', + 'EAIS', + 'WAIS', + 'APIS', + 'QML', + 'NoQML', + 'CpDc', + 'CpDi', + 'DDpc', + 'DDpi', + 'GH2', + 'HHp2', + 'JJpp2', + 'GH3', + 'TM', + ] +) +region.extend( + [ + 'NW', + 'NN', + 'NE', + 'SW', + 'SE', + 'GIS', + 'CDE', + 'CBI', + 'ICL', + 'SVB', + 'ALK', + 'DEN', + 'PNW', + 'PAT', + 'FJL', + 'SZEM', + 'NZEM', + 'RUS', + 'ARC', + 'GIC', + ] +) # Cap numbers for each basin cap = {} # the contents of the cap variable will be referenced to the basin name # the cap numbers are the global numbers for the spherical cap radius -cap['AAp'] = np.array([25,26,39,40,41,53,54]) -cap['ApB'] = np.array([55,56,67,68,69,70,83,97]) -cap['BC'] = np.array([80,81,82,93,94,95,96,107,108,109,121,122]) -cap['CCp'] = np.array([110,123,124,136,137,138,150,151]) -cap['CpD'] = np.array([135,148,149,162,163,164,175,176,177,189,190]) -cap['DDp'] = np.array([174,186,187,188,200,201,202]) -cap['DpE'] = np.array([158,159,160,161,172,173,185,199]) -cap['EEp'] = np.array([106,117,118,119,120,131,132,133,134,145,146,147]) -cap['EpFp'] = np.array([102,103,115,116,128,129,130,142,143]) -cap['FpG'] = np.array([127,141]) -cap['GH'] = np.array([86,100,101,113,114]) -cap['HHp'] = np.array([72,85,99]) -cap['HpI'] = np.array([45,59]) -cap['IIpp'] = np.array([4,17,18,31,32]) +cap['AAp'] = np.array([25, 26, 39, 40, 41, 53, 54]) +cap['ApB'] = np.array([55, 56, 67, 68, 69, 70, 83, 97]) +cap['BC'] = np.array([80, 81, 82, 93, 94, 95, 96, 107, 108, 109, 121, 122]) +cap['CCp'] = np.array([110, 123, 124, 136, 137, 138, 150, 151]) +cap['CpD'] = np.array([135, 148, 149, 162, 163, 164, 175, 176, 177, 189, 190]) +cap['DDp'] = np.array([174, 186, 187, 188, 200, 201, 202]) +cap['DpE'] = np.array([158, 159, 160, 161, 172, 173, 185, 199]) +cap['EEp'] = np.array( + [106, 117, 118, 119, 120, 131, 132, 133, 134, 145, 146, 147] +) +cap['EpFp'] = np.array([102, 103, 115, 116, 128, 129, 130, 142, 143]) +cap['FpG'] = np.array([127, 141]) +cap['GH'] = np.array([86, 100, 101, 113, 114]) +cap['HHp'] = np.array([72, 85, 99]) +cap['HpI'] = np.array([45, 59]) +cap['IIpp'] = np.array([4, 17, 18, 31, 32]) cap['IppJ'] = np.array([46]) -cap['JJpp'] = np.array([60,73,74,75,87,88,89]) -cap['JppK'] = np.array([49,50,51,52,62,63,64,65,66,76,77,78,79,90,91,92,104,105]) -cap['KKp'] = np.array([23,36,37]) -cap['KpA'] = np.array([24,38]) -cap['SHELVES'] = np.array([61,144,157]) +cap['JJpp'] = np.array([60, 73, 74, 75, 87, 88, 89]) +cap['JppK'] = np.array( + [49, 50, 51, 52, 62, 63, 64, 65, 66, 76, 77, 78, 79, 90, 91, 92, 104, 105] +) +cap['KKp'] = np.array([23, 36, 37]) +cap['KpA'] = np.array([24, 38]) +cap['SHELVES'] = np.array([61, 144, 157]) # Grouping Basins into larger regions (AIS, EAIS, WAIS and APEN) -cap['AIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['EpFp'], cap['FpG'], \ - cap['GH'], cap['HHp'], cap['HpI'], cap['IIpp'], cap['IppJ'], cap['JJpp'], \ - cap['JppK'], cap['KKp'], cap['KpA']),axis=0) #, cap['SHELVES'] -cap['EAIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp'], \ - cap['KpA']),axis=0) -cap['WAIS'] = np.concatenate((cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']),axis=0) -cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']),axis=0) -cap['INTERIOR'] = np.array([38,51,52,53,65,66,67,68,78,79,80,81,91,92,93,94,\ - 104,105,106,107,108,118,119,120,121,122,123,133,134,135,136,147,148,149,\ - 160,161,162,174,175]) -cap['QML'] = np.concatenate((cap['KpA'],cap['AAp'],cap['ApB']),axis=0) -cap['NoQML'] = np.concatenate((cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp']),axis=0) +cap['AIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['EpFp'], + cap['FpG'], + cap['GH'], + cap['HHp'], + cap['HpI'], + cap['IIpp'], + cap['IppJ'], + cap['JJpp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) # , cap['SHELVES'] +cap['EAIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) +cap['WAIS'] = np.concatenate( + (cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']), axis=0 +) +cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']), axis=0) +cap['INTERIOR'] = np.array( + [ + 38, + 51, + 52, + 53, + 65, + 66, + 67, + 68, + 78, + 79, + 80, + 81, + 91, + 92, + 93, + 94, + 104, + 105, + 106, + 107, + 108, + 118, + 119, + 120, + 121, + 122, + 123, + 133, + 134, + 135, + 136, + 147, + 148, + 149, + 160, + 161, + 162, + 174, + 175, + ] +) +cap['QML'] = np.concatenate((cap['KpA'], cap['AAp'], cap['ApB']), axis=0) +cap['NoQML'] = np.concatenate( + ( + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + ), + axis=0, +) cap['ISLAND'] = np.array([62]) -cap['CpDc'] = np.array([163,164,176,177,189,190]) -#cap['CpDc'] = np.array([163,164,177]) -cap['CpDi'] = np.array([135,148,149,162,175]) -cap['TM'] = np.array([135,148,149,162,163,176,177,190]) -cap['DDpc'] = np.array([186,187,188,200,201,202]) -#cap['DDpi'] = np.array([174]) -cap['DDpi'] = np.concatenate((cap['DDp'],cap['DpE']),axis=0) +cap['CpDc'] = np.array([163, 164, 176, 177, 189, 190]) +# cap['CpDc'] = np.array([163,164,177]) +cap['CpDi'] = np.array([135, 148, 149, 162, 175]) +cap['TM'] = np.array([135, 148, 149, 162, 163, 176, 177, 190]) +cap['DDpc'] = np.array([186, 187, 188, 200, 201, 202]) +# cap['DDpi'] = np.array([174]) +cap['DDpi'] = np.concatenate((cap['DDp'], cap['DpE']), axis=0) # different version of west ant -cap['GH2'] = np.array([86,87,99,100,101,113,114]) -cap['HHp2'] = np.array([72,85]) -cap['JJpp2'] = np.array([60,73,74,75,88,89]) -cap['GH3'] = np.array([86,99,100,101,113,114]) +cap['GH2'] = np.array([86, 87, 99, 100, 101, 113, 114]) +cap['HHp2'] = np.array([72, 85]) +cap['JJpp2'] = np.array([60, 73, 74, 75, 88, 89]) +cap['GH3'] = np.array([86, 99, 100, 101, 113, 114]) # Amundsen Sea Embayment regions -cap['PIG'] = np.array([86,87,99]) -cap['THSPK'] = np.array([100,101,113,114]) +cap['PIG'] = np.array([86, 87, 99]) +cap['THSPK'] = np.array([100, 101, 113, 114]) # Greenland Mascons -cap['NW'] = np.array([308,309,312,313,316,317]) -cap['NN'] = np.array([301,302,303,304,305,306]) -cap['NE'] = np.array([307,310,311,314,315]) -cap['SE'] = np.array([318,319,322,323,325,327]) -cap['SW'] = np.array([320,321,324,326]) -cap['GIS'] = np.concatenate((cap['NW'],cap['NN'],cap['NE'],cap['SW'],cap['SE']),axis=0) +cap['NW'] = np.array([308, 309, 312, 313, 316, 317]) +cap['NN'] = np.array([301, 302, 303, 304, 305, 306]) +cap['NE'] = np.array([307, 310, 311, 314, 315]) +cap['SE'] = np.array([318, 319, 322, 323, 325, 327]) +cap['SW'] = np.array([320, 321, 324, 326]) +cap['GIS'] = np.concatenate( + (cap['NW'], cap['NN'], cap['NE'], cap['SW'], cap['SE']), axis=0 +) # Canadian Archipelago -cap['CDE'] = np.array([328,329,330,331,332,333]) -cap['CBI'] = np.array([334,335,336,337,338]) +cap['CDE'] = np.array([328, 329, 330, 331, 332, 333]) +cap['CBI'] = np.array([334, 335, 336, 337, 338]) # Iceland, Svalbard, Franz Josef Land, Svernaya Zemlya and Novaya Zemlya cap['ICL'] = np.array([339]) cap['SVB'] = np.array([340]) @@ -151,21 +301,46 @@ cap['SZEM'] = np.array([342]) cap['NZEM'] = np.array([343]) # Alaska (Denali and Pacific Northwest) -cap['ALK'] = np.arange(344,364) -cap['DEN'] = np.array([344,347,348,349,350,351,352,353,354,355]) -cap['PNW'] = np.array([345,346,356,357,358,359,360,361,362,363]) +cap['ALK'] = np.arange(344, 364) +cap['DEN'] = np.array([344, 347, 348, 349, 350, 351, 352, 353, 354, 355]) +cap['PNW'] = np.array([345, 346, 356, 357, 358, 359, 360, 361, 362, 363]) # Patagonia -cap['PAT'] = np.arange(400,407) +cap['PAT'] = np.arange(400, 407) # All Arctic and all GIC (with patagonia) -cap['ARC'] = np.concatenate((cap['GIS'],cap['CDE'],cap['CBI'],cap['ICL'], - cap['SVB'],cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) -cap['GIC'] = np.concatenate((cap['CDE'],cap['CBI'],cap['ICL'],cap['SVB'], - cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM'],cap['PAT']),axis=0) +cap['ARC'] = np.concatenate( + ( + cap['GIS'], + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + ), + axis=0, +) +cap['GIC'] = np.concatenate( + ( + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + cap['PAT'], + ), + axis=0, +) # Russian Arctic (Franz Josef Land, Svernaya Zemlya and Novaya Zemlya) -cap['RUS'] = np.concatenate((cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) +cap['RUS'] = np.concatenate((cap['FJL'], cap['SZEM'], cap['NZEM']), axis=0) # All caps (Greenland, Glaciers and Ice Caps, Antarctica) -cap['ALL'] = np.concatenate((cap['GIS'],cap['GIC'],cap['AIS']),axis=0) +cap['ALL'] = np.concatenate((cap['GIS'], cap['GIC'], cap['AIS']), axis=0) + # PURPOSE: keep track of threads def info(args): @@ -175,7 +350,12 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') -def combine_HEX_TWC_errors(base_dir, MODEL, LMAX, RAD, + +def combine_HEX_TWC_errors( + base_dir, + MODEL, + LMAX, + RAD, START=None, END=None, VERSION=None, @@ -184,8 +364,8 @@ def combine_HEX_TWC_errors(base_dir, MODEL, LMAX, RAD, DESTRIPE=False, REDISTRIBUTE_MASCONS=False, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -201,15 +381,15 @@ def combine_HEX_TWC_errors(base_dir, MODEL, LMAX, RAD, # using coefficients that were redistributed over the ocean ocean_str = '_OCN' if REDISTRIBUTE_MASCONS else '' # models to run - if (VERSION == '1'): + if VERSION == '1': # GLDAS Version 1 - V1,V2 = ('_V1','') + V1, V2 = ('_V1', '') else: # GLDAS Version 2.1 - V1,V2 = ('','V{0}_'.format(VERSION)) + V1, V2 = ('', 'V{0}_'.format(VERSION)) # subdirectory with output RMS data - a = ('HEX_GLDAS_TWC_',V2,LMAX,START,END) + a = ('HEX_GLDAS_TWC_', V2, LMAX, START, END) sd = '{0}{1}RMS_SPH_CAP_MSCNS_L{2:d}_{3:03d}-{4:03d}'.format(*a) # read each mascon file and store in dictionary with k of the cap number @@ -224,28 +404,40 @@ def combine_HEX_TWC_errors(base_dir, MODEL, LMAX, RAD, area_reg = {} # input files from least_squares_mascon_timeseries.py - ff='GLDAS_{0}{1}_TWC_SPH_CAP_RAD{2:0.1f}_{3}_L{4:d}{5}{6}{7}{8}.txt' + ff = 'GLDAS_{0}{1}_TWC_SPH_CAP_RAD{2:0.1f}_{3}_L{4:d}{5}{6}{7}{8}.txt' for m in MODEL: # subdirectory with data - a=(m,SPACING,V1,LMAX,START,END) - s='GLDAS_{0}{1}{2}_TWC_SPH_CAP_MSCNS_L{3:d}_{4:03d}-{5:03d}'.format(*a) + a = (m, SPACING, V1, LMAX, START, END) + s = 'GLDAS_{0}{1}{2}_TWC_SPH_CAP_MSCNS_L{3:d}_{4:03d}-{5:03d}'.format( + *a + ) # Read each spherical cap for k in cap['ALL']: # read cap time-series for mascon and verify shape - a=(m,SPACING,RAD_CAP,k,LMAX,order_str,gw_str,ds_str,ocean_str) - TWC[m][k] = np.loadtxt(base_dir.joinpath(s,ff.format(*a)), ndmin=2) + a = ( + m, + SPACING, + RAD_CAP, + k, + LMAX, + order_str, + gw_str, + ds_str, + ocean_str, + ) + TWC[m][k] = np.loadtxt(base_dir.joinpath(s, ff.format(*a)), ndmin=2) # GRACE/GRACE-FO months - mon = np.arange(START,END+1) - TWC_months = TWC[m][k][:,0].astype(np.int64) - missing = sorted(set(np.arange(START,END+1))-set(TWC_months)) + mon = np.arange(START, END + 1) + TWC_months = TWC[m][k][:, 0].astype(np.int64) + missing = sorted(set(np.arange(START, END + 1)) - set(TWC_months)) n_mon = len(mon) # GRACE/GRACE-FO dates - calendar_year = 2002 + (mon-1)//12 - calendar_month = np.mod(mon-1,12) + 1 - tdec = gravtk.time.convert_calendar_decimal(calendar_year,calendar_month) + calendar_year = 2002 + (mon - 1) // 12 + calendar_month = np.mod(mon - 1, 12) + 1 + tdec = gravtk.time.convert_calendar_decimal(calendar_year, calendar_month) # remove mean of 2003--2014 - m0314, = np.nonzero((mon >= 13) & (mon <= 156)) + (m0314,) = np.nonzero((mon >= 13) & (mon <= 156)) # for each region for i in region: @@ -257,11 +449,13 @@ def combine_HEX_TWC_errors(base_dir, MODEL, LMAX, RAD, # sum mascons in region for k in cap[i]: for m in MODEL: - TWC_months = TWC[M][k][:,0].astype(np.int64) + TWC_months = TWC[M][k][:, 0].astype(np.int64) ind = np.ravel([np.flatnonzero(mon == m) for m in TWC_months]) - TWC_mass[m][i][ind] += TWC[m][k][:,2] - TWC[m][k][m0314,2].mean() + TWC_mass[m][i][ind] += ( + TWC[m][k][:, 2] - TWC[m][k][m0314, 2].mean() + ) # add mascon area to total area (cm^2) - area_reg[i] += 1e10*TWC[m][k][0,3] + area_reg[i] += 1e10 * TWC[m][k][0, 3] # calculate mean between all hydrological models TWC_mean = np.zeros((n_mon)) @@ -272,15 +466,24 @@ def combine_HEX_TWC_errors(base_dir, MODEL, LMAX, RAD, # calculate variance off mean TWC_variance = np.zeros((n_mon)) for m in MODEL: - TWC_variance += (TWC_mass[m][i]-TWC_mean)**2 + TWC_variance += (TWC_mass[m][i] - TWC_mean) ** 2 # calculate RMS of mean differences - TWC_reg[i] = np.sqrt(TWC_variance/(len(MODEL)-1.0)) + TWC_reg[i] = np.sqrt(TWC_variance / (len(MODEL) - 1.0)) # replace invalid values with nan ind = np.ravel([np.flatnonzero(mon == m) for m in missing]) TWC_reg[i][ind] = np.nan # output data files - a = ('GLDAS_TWC_RMS_',i,RAD_CAP,ocean_str,LMAX,order_str,gw_str,ds_str) + a = ( + 'GLDAS_TWC_RMS_', + i, + RAD_CAP, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + ) FILE1 = '{0}{1}_SPH_CAP_RAD{2:0.1f}{3}_L{4:d}{5}{6}.txt'.format(*a) FILE2 = '{0}{1}_SPH_CAP_RAD{2:0.1f}{3}_L{4:d}{5}{6}_cmwe.txt'.format(*a) # open files for writing region time-series @@ -292,9 +495,11 @@ def combine_HEX_TWC_errors(base_dir, MODEL, LMAX, RAD, # output regional mascon time-series for t in range(n_mon): mass_error = TWC_reg[i][t] - thick_error = 1e15*TWC_reg[i][t]/area_reg[i] + thick_error = 1e15 * TWC_reg[i][t] / area_reg[i] print(f'{mon[t]:03d} {tdec[t]:12.4f} {mass_error:14.6f}', file=fid1) - print(f'{mon[t]:03d} {tdec[t]:12.4f} {thick_error:14.6f}', file=fid2) + print( + f'{mon[t]:03d} {tdec[t]:12.4f} {thick_error:14.6f}', file=fid2 + ) # close the output file fid1.close() fid2.close() @@ -302,81 +507,129 @@ def combine_HEX_TWC_errors(base_dir, MODEL, LMAX, RAD, mass_file.chmod(mode=MODE) cmwe_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""calculates the time-series of terrestrial water storage mass leakage for spherical cap mascons """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - choices = ['CLM','CLSM','MOS','NOAH','VIC'] - parser.add_argument('model', - metavar='MODEL', type=str, nargs='+', - default=['CLSM','NOAH','VIC'], choices=choices, - help='GLDAS land surface model') + choices = ['CLM', 'CLSM', 'MOS', 'NOAH', 'VIC'] + parser.add_argument( + 'model', + metavar='MODEL', + type=str, + nargs='+', + default=['CLSM', 'NOAH', 'VIC'], + choices=choices, + help='GLDAS land surface model', + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) # GLDAS model version - parser.add_argument('--version', - type=str, default='2.1', - help='GLDAS model version') + parser.add_argument( + '--version', type=str, default='2.1', help='GLDAS model version' + ) # model spatial resolution # 10: 1.0 degrees latitude/longitude # 025: 0.25 degrees latitude/longitude - parser.add_argument('--spacing', - type=str, default='10', choices=['10','025'], - help='Spatial resolution of GLDAS model') + parser.add_argument( + '--spacing', + type=str, + default='10', + choices=['10', '025'], + help='Spatial resolution of GLDAS model', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -399,7 +652,8 @@ def main(): DESTRIPE=args.destripe, REDISTRIBUTE_MASCONS=args.redistribute_mascons, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -407,6 +661,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/combine_HEX_leakage.py b/scripts/combine_HEX_leakage.py index 221e975d..d3ff751c 100644 --- a/scripts/combine_HEX_leakage.py +++ b/scripts/combine_HEX_leakage.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_HEX_leakage.py (02/2024) calculates the leakage component of ice mass change for spherical cap mascons @@ -40,6 +40,7 @@ Updated 03/2018: simplified flag setting algorithms to single lines Written 08/2017 """ + from __future__ import print_function import sys @@ -58,103 +59,268 @@ cap = {} # the contents of the cap variable will be referenced to the basin name # the cap numbers are the global numbers for the spherical cap radius -cap['AAp'] = np.array([25,26,39,40,41,53,54]) -cap['ApB'] = np.array([55,56,67,68,69,70,83,97]) -cap['BC'] = np.array([80,81,82,93,94,95,96,107,108,109,121,122]) -cap['CCp'] = np.array([110,123,124,136,137,138,150,151]) -cap['CpD'] = np.array([135,148,149,162,163,164,175,176,177,189,190]) -cap['DDp'] = np.array([174,186,187,188,200,201,202]) -cap['DpE'] = np.array([158,159,160,161,172,173,185,199]) -cap['EEp'] = np.array([106,117,118,119,120,131,132,133,134,145,146,147]) -cap['EpFp'] = np.array([102,103,115,116,128,129,130,142,143]) -cap['FpG'] = np.array([127,141]) -cap['GH'] = np.array([86,100,101,113,114]) -cap['HHp'] = np.array([72,85,99]) -cap['HpI'] = np.array([45,59]) -cap['IIpp'] = np.array([4,17,18,31,32]) +cap['AAp'] = np.array([25, 26, 39, 40, 41, 53, 54]) +cap['ApB'] = np.array([55, 56, 67, 68, 69, 70, 83, 97]) +cap['BC'] = np.array([80, 81, 82, 93, 94, 95, 96, 107, 108, 109, 121, 122]) +cap['CCp'] = np.array([110, 123, 124, 136, 137, 138, 150, 151]) +cap['CpD'] = np.array([135, 148, 149, 162, 163, 164, 175, 176, 177, 189, 190]) +cap['DDp'] = np.array([174, 186, 187, 188, 200, 201, 202]) +cap['DpE'] = np.array([158, 159, 160, 161, 172, 173, 185, 199]) +cap['EEp'] = np.array( + [106, 117, 118, 119, 120, 131, 132, 133, 134, 145, 146, 147] +) +cap['EpFp'] = np.array([102, 103, 115, 116, 128, 129, 130, 142, 143]) +cap['FpG'] = np.array([127, 141]) +cap['GH'] = np.array([86, 100, 101, 113, 114]) +cap['HHp'] = np.array([72, 85, 99]) +cap['HpI'] = np.array([45, 59]) +cap['IIpp'] = np.array([4, 17, 18, 31, 32]) cap['IppJ'] = np.array([46]) -cap['JJpp'] = np.array([60,73,74,75,87,88,89]) -cap['JppK'] = np.array([49,50,51,52,62,63,64,65,66,76,77,78,79,90,91,92,104,105]) -cap['KKp'] = np.array([23,36,37]) -cap['KpA'] = np.array([24,38]) -cap['SHELVES'] = np.array([61,144,157]) +cap['JJpp'] = np.array([60, 73, 74, 75, 87, 88, 89]) +cap['JppK'] = np.array( + [49, 50, 51, 52, 62, 63, 64, 65, 66, 76, 77, 78, 79, 90, 91, 92, 104, 105] +) +cap['KKp'] = np.array([23, 36, 37]) +cap['KpA'] = np.array([24, 38]) +cap['SHELVES'] = np.array([61, 144, 157]) # Grouping Basins into larger regions (AIS, EAIS, WAIS and APEN) -cap['AIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['EpFp'], cap['FpG'], \ - cap['GH'], cap['HHp'], cap['HpI'], cap['IIpp'], cap['IppJ'], cap['JJpp'], \ - cap['JppK'], cap['KKp'], cap['KpA']),axis=0) #, cap['SHELVES'] -cap['EAIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp'], \ - cap['KpA']),axis=0) -cap['WAIS'] = np.concatenate((cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']),axis=0) -cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']),axis=0) -cap['INTERIOR'] = np.array([38,51,52,53,65,66,67,68,78,79,80,81,91,92,93,94,\ - 104,105,106,107,108,118,119,120,121,122,123,133,134,135,136,147,148,149,\ - 160,161,162,174,175]) -cap['QML'] = np.concatenate((cap['KpA'],cap['AAp'],cap['ApB']),axis=0) -cap['NoQML'] = np.concatenate((cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp']),axis=0) +cap['AIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['EpFp'], + cap['FpG'], + cap['GH'], + cap['HHp'], + cap['HpI'], + cap['IIpp'], + cap['IppJ'], + cap['JJpp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) # , cap['SHELVES'] +cap['EAIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) +cap['WAIS'] = np.concatenate( + (cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']), axis=0 +) +cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']), axis=0) +cap['INTERIOR'] = np.array( + [ + 38, + 51, + 52, + 53, + 65, + 66, + 67, + 68, + 78, + 79, + 80, + 81, + 91, + 92, + 93, + 94, + 104, + 105, + 106, + 107, + 108, + 118, + 119, + 120, + 121, + 122, + 123, + 133, + 134, + 135, + 136, + 147, + 148, + 149, + 160, + 161, + 162, + 174, + 175, + ] +) +cap['QML'] = np.concatenate((cap['KpA'], cap['AAp'], cap['ApB']), axis=0) +cap['NoQML'] = np.concatenate( + ( + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + ), + axis=0, +) cap['ISLAND'] = np.array([62]) -cap['CpDc'] = np.array([163,164,176,177,189,190]) -#cap['CpDc'] = np.array([163,164,177]) -cap['CpDi'] = np.array([135,148,149,162,175]) -cap['TM'] = np.array([135,148,149,162,163,176,177,190]) -cap['DDpc'] = np.array([186,187,188,200,201,202]) -#cap['DDpi'] = np.array([174]) -cap['DDpi'] = np.concatenate((cap['DDp'],cap['DpE']),axis=0) +cap['CpDc'] = np.array([163, 164, 176, 177, 189, 190]) +# cap['CpDc'] = np.array([163,164,177]) +cap['CpDi'] = np.array([135, 148, 149, 162, 175]) +cap['TM'] = np.array([135, 148, 149, 162, 163, 176, 177, 190]) +cap['DDpc'] = np.array([186, 187, 188, 200, 201, 202]) +# cap['DDpi'] = np.array([174]) +cap['DDpi'] = np.concatenate((cap['DDp'], cap['DpE']), axis=0) # different version of west ant -cap['GH2'] = np.array([86,87,99,100,101,113,114]) -cap['HHp2'] = np.array([72,85]) -cap['JJpp2'] = np.array([60,73,74,75,88,89]) -cap['GH3'] = np.array([86,99,100,101,113,114]) +cap['GH2'] = np.array([86, 87, 99, 100, 101, 113, 114]) +cap['HHp2'] = np.array([72, 85]) +cap['JJpp2'] = np.array([60, 73, 74, 75, 88, 89]) +cap['GH3'] = np.array([86, 99, 100, 101, 113, 114]) # Amundsen Sea Embayment regions -cap['PIG'] = np.array([86,87,99]) -cap['THSPK'] = np.array([100,101,113,114]) +cap['PIG'] = np.array([86, 87, 99]) +cap['THSPK'] = np.array([100, 101, 113, 114]) # Greenland Mascons -cap['NW'] = np.array([308,309,312,313,316,317]) -cap['NN'] = np.array([301,302,303,304,305,306]) -cap['NE'] = np.array([307,310,311,314,315]) -cap['SE'] = np.array([318,319,322,323,325,327]) -cap['SW'] = np.array([320,321,324,326]) -cap['GIS'] = np.concatenate((cap['NW'],cap['NN'],cap['NE'],cap['SW'],cap['SE']),axis=0) +cap['NW'] = np.array([308, 309, 312, 313, 316, 317]) +cap['NN'] = np.array([301, 302, 303, 304, 305, 306]) +cap['NE'] = np.array([307, 310, 311, 314, 315]) +cap['SE'] = np.array([318, 319, 322, 323, 325, 327]) +cap['SW'] = np.array([320, 321, 324, 326]) +cap['GIS'] = np.concatenate( + (cap['NW'], cap['NN'], cap['NE'], cap['SW'], cap['SE']), axis=0 +) # Canadian Archipelago -cap['CDE'] = np.array([328,329,330,331,332,333]) -cap['CBI'] = np.array([334,335,336,337,338]) +cap['CDE'] = np.array([328, 329, 330, 331, 332, 333]) +cap['CBI'] = np.array([334, 335, 336, 337, 338]) # Iceland, Svalbard, Franz Josef Land, Svernaya Zemlya and Novaya Zemlya cap['ICL'] = np.array([339]) cap['SVB'] = np.array([340]) cap['FJL'] = np.array([341]) cap['SZEM'] = np.array([342]) -cap['NZEM'] = np.array([343,344]) +cap['NZEM'] = np.array([343, 344]) # Alaska (Denali and Pacific Northwest) -cap['ALK'] = np.arange(345,368) -cap['DEN'] = np.array([345,346,348,349,350,351,352,353,354,355,356,367]) -cap['PNW'] = np.array([347,357,358,359,360,361,362,363,364,365,366]) +cap['ALK'] = np.arange(345, 368) +cap['DEN'] = np.array( + [345, 346, 348, 349, 350, 351, 352, 353, 354, 355, 356, 367] +) +cap['PNW'] = np.array([347, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366]) # Patagonia -cap['PAT'] = np.arange(400,407) +cap['PAT'] = np.arange(400, 407) # All Arctic and all GIC (with patagonia) -cap['ARC'] = np.concatenate((cap['GIS'],cap['CDE'],cap['CBI'],cap['ICL'], - cap['SVB'],cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) -cap['GIC'] = np.concatenate((cap['CDE'],cap['CBI'],cap['ICL'],cap['SVB'], - cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM'],cap['PAT']),axis=0) +cap['ARC'] = np.concatenate( + ( + cap['GIS'], + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + ), + axis=0, +) +cap['GIC'] = np.concatenate( + ( + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + cap['PAT'], + ), + axis=0, +) # Russian Arctic (Franz Josef Land, Svernaya Zemlya and Novaya Zemlya) -cap['RUS'] = np.concatenate((cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) +cap['RUS'] = np.concatenate((cap['FJL'], cap['SZEM'], cap['NZEM']), axis=0) # All caps (Greenland, Glaciers and Ice Caps, Antarctica) -cap['ALL'] = np.concatenate((cap['GIS'],cap['GIC'],cap['AIS']),axis=0) +cap['ALL'] = np.concatenate((cap['GIS'], cap['GIC'], cap['AIS']), axis=0) # groupings of major regions major_regions = {} -major_regions['ANT'] = ['AAp','ApB','BC','CCp','CpD','DDp', - 'DpE','EEp','EpFp','FpG','GH','HHp','HpI','IIpp','IppJ','JJpp', - 'JppK','KKp','KpA','INTERIOR','AIS','EAIS','WAIS','APIS','QML', - 'NoQML','CpDc','CpDi','DDpc','DDpi','GH2','HHp2','JJpp2','GH3', - 'TM','PIG','THSPK'] -major_regions['GRN'] = ['NW','NN','NE','SW','SE','GIS'] -major_regions['GIC'] = ['CDE','CBI','ICL','SVB','FJL','SZEM','NZEM', - 'RUS','ALK','DEN','PNW','PAT'] -major_regions['ALL'] = ['ALL','ARC','GIC'] +major_regions['ANT'] = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', + 'INTERIOR', + 'AIS', + 'EAIS', + 'WAIS', + 'APIS', + 'QML', + 'NoQML', + 'CpDc', + 'CpDi', + 'DDpc', + 'DDpi', + 'GH2', + 'HHp2', + 'JJpp2', + 'GH3', + 'TM', + 'PIG', + 'THSPK', +] +major_regions['GRN'] = ['NW', 'NN', 'NE', 'SW', 'SE', 'GIS'] +major_regions['GIC'] = [ + 'CDE', + 'CBI', + 'ICL', + 'SVB', + 'FJL', + 'SZEM', + 'NZEM', + 'RUS', + 'ALK', + 'DEN', + 'PNW', + 'PAT', +] +major_regions['ALL'] = ['ALL', 'ARC', 'GIC'] + # PURPOSE: keep track of threads def info(args): @@ -164,7 +330,10 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') -def combine_HEX_leakage(LMAX, RAD, + +def combine_HEX_leakage( + LMAX, + RAD, MMAX=None, DESTRIPE=False, COORDINATE_FILE=None, @@ -174,8 +343,8 @@ def combine_HEX_leakage(LMAX, RAD, TYPE=None, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -192,10 +361,10 @@ def combine_HEX_leakage(LMAX, RAD, ocean_str = '_OCN' if REDISTRIBUTE_MASCONS else '' # offset and scale for each mascon configuration (Hole or Island) - if (TYPE == 'Hole'): + if TYPE == 'Hole': offset = 0.0 scale = 1.0 - elif (TYPE == 'Island'): + elif TYPE == 'Island': offset = 1.0 scale = -1.0 @@ -208,7 +377,7 @@ def combine_HEX_leakage(LMAX, RAD, # read coordinate file for lat/lon of spherical cap centers coord = np.loadtxt(COORDINATE_FILE, skiprows=HEADER) # column 1: cap number - num = coord[:,0].astype(int) + num = coord[:, 0].astype(int) # read each mascon file and store in dictionary with k of the cap number leakage_data = {} @@ -219,7 +388,16 @@ def combine_HEX_leakage(LMAX, RAD, # Read each spherical cap for k in cap['ALL']: # input files - args = (FILE_PREFIX,RAD_CAP,k,LMAX,order_str,gw_str,ds_str,ocean_str) + args = ( + FILE_PREFIX, + RAD_CAP, + k, + LMAX, + order_str, + gw_str, + ds_str, + ocean_str, + ) f1 = '{0}SPH_CAP_RAD{1:0.1f}_{2:d}_L{3:d}{4}{5}{6}{7}.txt'.format(*args) # read cap for mascon and verify shape leakage_data[k] = np.loadtxt(OUTPUT_DIRECTORY.joinpath(f1), ndmin=2) @@ -228,7 +406,7 @@ def combine_HEX_leakage(LMAX, RAD, region = [region for reg in REGION for region in major_regions[reg]] # output data file with all regions - args = (FILE_PREFIX,RAD_CAP,LMAX,order_str,gw_str,ds_str,ocean_str) + args = (FILE_PREFIX, RAD_CAP, LMAX, order_str, gw_str, ds_str, ocean_str) f2 = '{0}SPH_CAP_RAD{1:0.1f}_L{2:d}{3}{4}{5}{6}.txt'.format(*args) # open files for writing region leakage mass mass_file = OUTPUT_DIRECTORY.joinpath(f2) @@ -243,92 +421,143 @@ def combine_HEX_leakage(LMAX, RAD, # sum mascons in region for k in cap[i]: # find cap order - ii, = np.nonzero(num == k) - if (leakage_data[k][ii,2] != 0): - leakage[i] += (leakage_data[k][ii,0]/leakage_data[k][ii,2])**2 - leakage_reg[i] += leakage_data[k][ii,0]**2 - input_reg[i] += leakage_data[k][ii,2]**2 + (ii,) = np.nonzero(num == k) + if leakage_data[k][ii, 2] != 0: + leakage[i] += ( + leakage_data[k][ii, 0] / leakage_data[k][ii, 2] + ) ** 2 + leakage_reg[i] += leakage_data[k][ii, 0] ** 2 + input_reg[i] += leakage_data[k][ii, 2] ** 2 # print to file - leakage_percent, = offset + scale*np.sqrt(leakage[i]/len(cap[i])) - leakage_fraction, = offset + scale*np.sqrt(leakage_reg[i]/input_reg[i]) + (leakage_percent,) = offset + scale * np.sqrt(leakage[i] / len(cap[i])) + (leakage_fraction,) = offset + scale * np.sqrt( + leakage_reg[i] / input_reg[i] + ) print(f'{i} {leakage_percent:12.8f} {leakage_fraction:12.8f}', file=fid) # close the output file fid.close() mass_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""calculates the leakage component of ice mass change errors for spherical cap mascons """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') - parser.add_argument('--file-prefix','-P', - type=str, - help='Prefix string for mascon files') + help='Output directory for mascon files', + ) + parser.add_argument( + '--file-prefix', '-P', type=str, help='Prefix string for mascon files' + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # mascon coordinate file and parameters - parser.add_argument('--coordinate-file', + parser.add_argument( + '--coordinate-file', type=pathlib.Path, required=True, - help='File with spatial coordinates of mascon centers') + help='File with spatial coordinates of mascon centers', + ) # number of header lines to skip in coordinate file - parser.add_argument('--header','-H', - type=int, default=0, - help='Number of header lines to skip in coordinate file') + parser.add_argument( + '--header', + '-H', + type=int, + default=0, + help='Number of header lines to skip in coordinate file', + ) # major regions to run - choices = ['ANT','GRN','GIC','ALL'] - parser.add_argument('--region','-r', + choices = ['ANT', 'GRN', 'GIC', 'ALL'] + parser.add_argument( + '--region', + '-r', type=lambda x: str(x).upper(), - metavar='REGION', nargs='+', - default=['ANT','GRN'], choices=choices, - help='Major regions to run') + metavar='REGION', + nargs='+', + default=['ANT', 'GRN'], + choices=choices, + help='Major regions to run', + ) # leakage mascon type - parser.add_argument('--type','-t', + parser.add_argument( + '--type', + '-t', type=lambda x: str(x).title(), - metavar='TYPE', required=True, - choices=['Hole','Island'], - help='Leakage mascon type') + metavar='TYPE', + required=True, + choices=['Hole', 'Island'], + help='Leakage mascon type', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -350,7 +579,8 @@ def main(): TYPE=args.type, OUTPUT_DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -358,6 +588,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/scripts/combine_HEX_mascon_timeseries.py b/scripts/combine_HEX_mascon_timeseries.py index 5b1e09b3..56fe8240 100644 --- a/scripts/combine_HEX_mascon_timeseries.py +++ b/scripts/combine_HEX_mascon_timeseries.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_HEX_mascon_timeseries.py (04/2024) Calculates the time-series of mass change for spherical cap mascons from synthetic data @@ -37,6 +37,7 @@ Updated 03/2018: additional output in equivalent surface pressure (mbar) Written 03/2018 """ + from __future__ import print_function import sys @@ -53,102 +54,280 @@ # Regions region = [] -region.extend(['AAp', 'ApB', 'BC', 'CCp', 'CpD', 'DDp', 'DpE', 'EEp', 'EpFp', - 'FpG', 'GH', 'HHp', 'HpI', 'IIpp', 'IppJ', 'JJpp', 'JppK','KKp', 'KpA', - 'INTERIOR', 'AIS', 'EAIS', 'WAIS', 'APIS', 'QML', 'NoQML', 'CpDc','CpDi', - 'DDpc','DDpi','GH2','HHp2','JJpp2','GH3','TM','PIG','THSPK']) -region.extend(['NW','NN','NE','SW','SE','GIS','CDE','CBI','ICL','SVB','ALK', - 'DEN','PNW','PAT','FJL','SZEM','NZEM','RUS','ARC','GIC']) +region.extend( + [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', + 'INTERIOR', + 'AIS', + 'EAIS', + 'WAIS', + 'APIS', + 'QML', + 'NoQML', + 'CpDc', + 'CpDi', + 'DDpc', + 'DDpi', + 'GH2', + 'HHp2', + 'JJpp2', + 'GH3', + 'TM', + 'PIG', + 'THSPK', + ] +) +region.extend( + [ + 'NW', + 'NN', + 'NE', + 'SW', + 'SE', + 'GIS', + 'CDE', + 'CBI', + 'ICL', + 'SVB', + 'ALK', + 'DEN', + 'PNW', + 'PAT', + 'FJL', + 'SZEM', + 'NZEM', + 'RUS', + 'ARC', + 'GIC', + ] +) # Cap numbers for each basin cap = {} # the contents of the cap variable will be referenced to the basin name # the cap numbers are the global numbers for the spherical cap radius -cap['AAp'] = np.array([25,26,39,40,41,53,54]) -cap['ApB'] = np.array([55,56,67,68,69,70,83,97]) -cap['BC'] = np.array([80,81,82,93,94,95,96,107,108,109,121,122]) -cap['CCp'] = np.array([110,123,124,136,137,138,150,151]) -cap['CpD'] = np.array([135,148,149,162,163,164,175,176,177,189,190]) -cap['DDp'] = np.array([174,186,187,188,200,201,202]) -cap['DpE'] = np.array([158,159,160,161,172,173,185,199]) -cap['EEp'] = np.array([106,117,118,119,120,131,132,133,134,145,146,147]) -cap['EpFp'] = np.array([102,103,115,116,128,129,130,142,143]) -cap['FpG'] = np.array([127,141]) -cap['GH'] = np.array([86,100,101,113,114]) -cap['HHp'] = np.array([72,85,99]) -cap['HpI'] = np.array([45,59]) -cap['IIpp'] = np.array([4,17,18,31,32]) +cap['AAp'] = np.array([25, 26, 39, 40, 41, 53, 54]) +cap['ApB'] = np.array([55, 56, 67, 68, 69, 70, 83, 97]) +cap['BC'] = np.array([80, 81, 82, 93, 94, 95, 96, 107, 108, 109, 121, 122]) +cap['CCp'] = np.array([110, 123, 124, 136, 137, 138, 150, 151]) +cap['CpD'] = np.array([135, 148, 149, 162, 163, 164, 175, 176, 177, 189, 190]) +cap['DDp'] = np.array([174, 186, 187, 188, 200, 201, 202]) +cap['DpE'] = np.array([158, 159, 160, 161, 172, 173, 185, 199]) +cap['EEp'] = np.array( + [106, 117, 118, 119, 120, 131, 132, 133, 134, 145, 146, 147] +) +cap['EpFp'] = np.array([102, 103, 115, 116, 128, 129, 130, 142, 143]) +cap['FpG'] = np.array([127, 141]) +cap['GH'] = np.array([86, 100, 101, 113, 114]) +cap['HHp'] = np.array([72, 85, 99]) +cap['HpI'] = np.array([45, 59]) +cap['IIpp'] = np.array([4, 17, 18, 31, 32]) cap['IppJ'] = np.array([46]) -cap['JJpp'] = np.array([60,73,74,75,87,88,89]) -cap['JppK'] = np.array([49,50,51,52,62,63,64,65,66,76,77,78,79,90,91,92,104,105]) -cap['KKp'] = np.array([23,36,37]) -cap['KpA'] = np.array([24,38]) -cap['SHELVES'] = np.array([61,144,157]) +cap['JJpp'] = np.array([60, 73, 74, 75, 87, 88, 89]) +cap['JppK'] = np.array( + [49, 50, 51, 52, 62, 63, 64, 65, 66, 76, 77, 78, 79, 90, 91, 92, 104, 105] +) +cap['KKp'] = np.array([23, 36, 37]) +cap['KpA'] = np.array([24, 38]) +cap['SHELVES'] = np.array([61, 144, 157]) # Grouping Basins into larger regions (AIS, EAIS, WAIS and APEN) -cap['AIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['EpFp'], cap['FpG'], \ - cap['GH'], cap['HHp'], cap['HpI'], cap['IIpp'], cap['IppJ'], cap['JJpp'], \ - cap['JppK'], cap['KKp'], cap['KpA']),axis=0) #, cap['SHELVES'] -cap['EAIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp'], \ - cap['KpA']),axis=0) -cap['WAIS'] = np.concatenate((cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']),axis=0) -cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']),axis=0) -cap['INTERIOR'] = np.array([38,51,52,53,65,66,67,68,78,79,80,81,91,92,93,94,\ - 104,105,106,107,108,118,119,120,121,122,123,133,134,135,136,147,148,149,\ - 160,161,162,174,175]) -cap['QML'] = np.concatenate((cap['KpA'],cap['AAp'],cap['ApB']),axis=0) -cap['NoQML'] = np.concatenate((cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp']),axis=0) +cap['AIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['EpFp'], + cap['FpG'], + cap['GH'], + cap['HHp'], + cap['HpI'], + cap['IIpp'], + cap['IppJ'], + cap['JJpp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) # , cap['SHELVES'] +cap['EAIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) +cap['WAIS'] = np.concatenate( + (cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']), axis=0 +) +cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']), axis=0) +cap['INTERIOR'] = np.array( + [ + 38, + 51, + 52, + 53, + 65, + 66, + 67, + 68, + 78, + 79, + 80, + 81, + 91, + 92, + 93, + 94, + 104, + 105, + 106, + 107, + 108, + 118, + 119, + 120, + 121, + 122, + 123, + 133, + 134, + 135, + 136, + 147, + 148, + 149, + 160, + 161, + 162, + 174, + 175, + ] +) +cap['QML'] = np.concatenate((cap['KpA'], cap['AAp'], cap['ApB']), axis=0) +cap['NoQML'] = np.concatenate( + ( + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + ), + axis=0, +) cap['ISLAND'] = np.array([62]) -cap['CpDc'] = np.array([163,164,176,177,189,190]) -#cap['CpDc'] = np.array([163,164,177]) -cap['CpDi'] = np.array([135,148,149,162,175]) -cap['TM'] = np.array([135,148,149,162,163,176,177,190]) -cap['DDpc'] = np.array([186,187,188,200,201,202]) -#cap['DDpi'] = np.array([174]) -cap['DDpi'] = np.concatenate((cap['DDp'],cap['DpE']),axis=0) +cap['CpDc'] = np.array([163, 164, 176, 177, 189, 190]) +# cap['CpDc'] = np.array([163,164,177]) +cap['CpDi'] = np.array([135, 148, 149, 162, 175]) +cap['TM'] = np.array([135, 148, 149, 162, 163, 176, 177, 190]) +cap['DDpc'] = np.array([186, 187, 188, 200, 201, 202]) +# cap['DDpi'] = np.array([174]) +cap['DDpi'] = np.concatenate((cap['DDp'], cap['DpE']), axis=0) # different version of west ant -cap['GH2'] = np.array([86,87,99,100,101,113,114]) -cap['HHp2'] = np.array([72,85]) -cap['JJpp2'] = np.array([60,73,74,75,88,89]) -cap['GH3'] = np.array([86,99,100,101,113,114]) +cap['GH2'] = np.array([86, 87, 99, 100, 101, 113, 114]) +cap['HHp2'] = np.array([72, 85]) +cap['JJpp2'] = np.array([60, 73, 74, 75, 88, 89]) +cap['GH3'] = np.array([86, 99, 100, 101, 113, 114]) # Amundsen Sea Embayment regions -cap['PIG'] = np.array([86,87,99]) -cap['THSPK'] = np.array([100,101,113,114]) +cap['PIG'] = np.array([86, 87, 99]) +cap['THSPK'] = np.array([100, 101, 113, 114]) # Greenland Mascons -cap['NW'] = np.array([308,309,312,313,316,317]) -cap['NN'] = np.array([301,302,303,304,305,306]) -cap['NE'] = np.array([307,310,311,314,315]) -cap['SE'] = np.array([318,319,322,323,325,327]) -cap['SW'] = np.array([320,321,324,326]) -cap['GIS'] = np.concatenate((cap['NW'],cap['NN'],cap['NE'],cap['SW'],cap['SE']),axis=0) +cap['NW'] = np.array([308, 309, 312, 313, 316, 317]) +cap['NN'] = np.array([301, 302, 303, 304, 305, 306]) +cap['NE'] = np.array([307, 310, 311, 314, 315]) +cap['SE'] = np.array([318, 319, 322, 323, 325, 327]) +cap['SW'] = np.array([320, 321, 324, 326]) +cap['GIS'] = np.concatenate( + (cap['NW'], cap['NN'], cap['NE'], cap['SW'], cap['SE']), axis=0 +) # Canadian Archipelago -cap['CDE'] = np.array([328,329,330,331,332,333]) -cap['CBI'] = np.array([334,335,336,337,338]) +cap['CDE'] = np.array([328, 329, 330, 331, 332, 333]) +cap['CBI'] = np.array([334, 335, 336, 337, 338]) # Iceland, Svalbard, Franz Josef Land, Svernaya Zemlya and Novaya Zemlya cap['ICL'] = np.array([339]) cap['SVB'] = np.array([340]) cap['FJL'] = np.array([341]) cap['SZEM'] = np.array([342]) -cap['NZEM'] = np.array([343,344]) +cap['NZEM'] = np.array([343, 344]) # Alaska (Denali and Pacific Northwest) -cap['ALK'] = np.arange(345,368) -cap['DEN'] = np.array([345,346,348,349,350,351,352,353,354,355,356,367]) -cap['PNW'] = np.array([347,357,358,359,360,361,362,363,364,365,366]) +cap['ALK'] = np.arange(345, 368) +cap['DEN'] = np.array( + [345, 346, 348, 349, 350, 351, 352, 353, 354, 355, 356, 367] +) +cap['PNW'] = np.array([347, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366]) # Patagonia -cap['PAT'] = np.arange(400,407) +cap['PAT'] = np.arange(400, 407) # All Arctic and all GIC (with patagonia) -cap['ARC'] = np.concatenate((cap['GIS'],cap['CDE'],cap['CBI'],cap['ICL'], - cap['SVB'],cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) -cap['GIC'] = np.concatenate((cap['CDE'],cap['CBI'],cap['ICL'],cap['SVB'], - cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM'],cap['PAT']),axis=0) +cap['ARC'] = np.concatenate( + ( + cap['GIS'], + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + ), + axis=0, +) +cap['GIC'] = np.concatenate( + ( + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + cap['PAT'], + ), + axis=0, +) # Russian Arctic (Franz Josef Land, Svernaya Zemlya and Novaya Zemlya) -cap['RUS'] = np.concatenate((cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) +cap['RUS'] = np.concatenate((cap['FJL'], cap['SZEM'], cap['NZEM']), axis=0) # All caps (Greenland, Glaciers and Ice Caps, Antarctica) -cap['ALL'] = np.concatenate((cap['GIS'],cap['GIC'],cap['AIS']),axis=0) +cap['ALL'] = np.concatenate((cap['GIS'], cap['GIC'], cap['AIS']), axis=0) + # PURPOSE: keep track of threads def info(args): @@ -158,14 +337,17 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') -def combine_HEX_mascon_timeseries(LMAX, RAD, + +def combine_HEX_mascon_timeseries( + LMAX, + RAD, MMAX=None, DESTRIPE=False, REDISTRIBUTE_MASCONS=False, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -193,14 +375,23 @@ def combine_HEX_mascon_timeseries(LMAX, RAD, # Read each spherical cap for k in cap['ALL']: # input files from least_squares_mascon_timeseries.py - a=(FILE_PREFIX,RAD_CAP,k,LMAX,order_str,gw_str,ds_str,ocean_str) + a = ( + FILE_PREFIX, + RAD_CAP, + k, + LMAX, + order_str, + gw_str, + ds_str, + ocean_str, + ) FILE = '{0}SPH_CAP_RAD{1:0.1f}_{2}_L{3:d}{4}{5}{6}{7}.txt'.format(*a) # read cap time-series for mascon and verify shape mass_data[k] = np.loadtxt(OUTPUT_DIRECTORY.joinpath(FILE), ndmin=2) # date information - mon = mass_data[k][:,0].astype(np.int64) - tdec = mass_data[k][:,1] + mon = mass_data[k][:, 0].astype(np.int64) + tdec = mass_data[k][:, 1] n_mon = len(mon) # for each region @@ -211,12 +402,21 @@ def combine_HEX_mascon_timeseries(LMAX, RAD, area_reg[i] = 0.0 # sum mascons in region for k in cap[i]: - mass_reg[i] += mass_data[k][:,2] + mass_reg[i] += mass_data[k][:, 2] # add mascon area to total area (cm^2) - area_reg[i] += 1e10*mass_data[k][0,3] + area_reg[i] += 1e10 * mass_data[k][0, 3] # output data files - a = (FILE_PREFIX,i,RAD_CAP,ocean_str,LMAX,order_str,gw_str,ds_str) + a = ( + FILE_PREFIX, + i, + RAD_CAP, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + ) FILE1 = '{0}{1}_SPH_CAP_RAD{2:0.1f}{3}_L{4:d}{5}{6}.txt'.format(*a) FILE2 = '{0}{1}_SPH_CAP_RAD{2:0.1f}{3}_L{4:d}{5}{6}_cmwe.txt'.format(*a) FILE3 = '{0}{1}_SPH_CAP_RAD{2:0.1f}{3}_L{4:d}{5}{6}_mbar.txt'.format(*a) @@ -230,15 +430,21 @@ def combine_HEX_mascon_timeseries(LMAX, RAD, fid3 = mbar_file.open(mode='w', encoding='utf8') # output regional mascon time-series total_mass = mass_reg[i] - total_thick = 1e15*total_mass/area_reg[i] - total_mbar = 1e14*g_wmo*total_mass/area_reg[i] + total_thick = 1e15 * total_mass / area_reg[i] + total_mbar = 1e14 * g_wmo * total_mass / area_reg[i] for t in range(n_mon): mass_anomaly = total_mass[t] - total_mass.mean() thick_anomaly = total_thick[t] - total_thick.mean() mbar_anomaly = total_mbar[t] - total_mbar.mean() - print(f'{mon[t]:03d} {tdec[t]:12.4f} {mass_anomaly:14.6f}', file=fid1) - print(f'{mon[t]:03d} {tdec[t]:12.4f} {thick_anomaly:14.6f}', file=fid2) - print(f'{mon[t]:03d} {tdec[t]:12.4f} {mbar_anomaly:14.6f}', file=fid3) + print( + f'{mon[t]:03d} {tdec[t]:12.4f} {mass_anomaly:14.6f}', file=fid1 + ) + print( + f'{mon[t]:03d} {tdec[t]:12.4f} {thick_anomaly:14.6f}', file=fid2 + ) + print( + f'{mon[t]:03d} {tdec[t]:12.4f} {mbar_anomaly:14.6f}', file=fid3 + ) # close the output file fid1.close() fid2.close() @@ -248,57 +454,89 @@ def combine_HEX_mascon_timeseries(LMAX, RAD, cmwe_file.chmod(mode=MODE) mbar_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the time-series of mass change for spherical cap mascons """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') - parser.add_argument('--file-prefix','-P', - type=str, - help='Prefix string for mascon files') + help='Output directory for mascon files', + ) + parser.add_argument( + '--file-prefix', '-P', type=str, help='Prefix string for mascon files' + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -316,7 +554,8 @@ def main(): REDISTRIBUTE_MASCONS=args.redistribute_mascons, OUTPUT_DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -324,6 +563,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/combine_HEX_mascons.py b/scripts/combine_HEX_mascons.py index 53623405..4628fadf 100644 --- a/scripts/combine_HEX_mascons.py +++ b/scripts/combine_HEX_mascons.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_HEX_mascons.py (04/2024) Calculates the mass change for spherical cap mascons from synthetic data @@ -38,6 +38,7 @@ Updated 03/2018: additional output in equivalent surface pressure (mbar) Written 03/2018 """ + from __future__ import print_function import sys @@ -54,102 +55,280 @@ # Regions region = [] -region.extend(['AAp', 'ApB', 'BC', 'CCp', 'CpD', 'DDp', 'DpE', 'EEp', 'EpFp', - 'FpG', 'GH', 'HHp', 'HpI', 'IIpp', 'IppJ', 'JJpp', 'JppK','KKp', 'KpA', - 'INTERIOR', 'AIS', 'EAIS', 'WAIS', 'APIS', 'QML', 'NoQML', 'CpDc','CpDi', - 'DDpc','DDpi','GH2','HHp2','JJpp2','GH3','TM','PIG','THSPK']) -region.extend(['NW','NN','NE','SW','SE','GIS','CDE','CBI','ICL','SVB','ALK', - 'DEN','PNW','PAT','FJL','SZEM','NZEM','RUS','ARC','GIC']) +region.extend( + [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', + 'INTERIOR', + 'AIS', + 'EAIS', + 'WAIS', + 'APIS', + 'QML', + 'NoQML', + 'CpDc', + 'CpDi', + 'DDpc', + 'DDpi', + 'GH2', + 'HHp2', + 'JJpp2', + 'GH3', + 'TM', + 'PIG', + 'THSPK', + ] +) +region.extend( + [ + 'NW', + 'NN', + 'NE', + 'SW', + 'SE', + 'GIS', + 'CDE', + 'CBI', + 'ICL', + 'SVB', + 'ALK', + 'DEN', + 'PNW', + 'PAT', + 'FJL', + 'SZEM', + 'NZEM', + 'RUS', + 'ARC', + 'GIC', + ] +) # Cap numbers for each basin cap = {} # the contents of the cap variable will be referenced to the basin name # the cap numbers are the global numbers for the spherical cap radius -cap['AAp'] = np.array([25,26,39,40,41,53,54]) -cap['ApB'] = np.array([55,56,67,68,69,70,83,97]) -cap['BC'] = np.array([80,81,82,93,94,95,96,107,108,109,121,122]) -cap['CCp'] = np.array([110,123,124,136,137,138,150,151]) -cap['CpD'] = np.array([135,148,149,162,163,164,175,176,177,189,190]) -cap['DDp'] = np.array([174,186,187,188,200,201,202]) -cap['DpE'] = np.array([158,159,160,161,172,173,185,199]) -cap['EEp'] = np.array([106,117,118,119,120,131,132,133,134,145,146,147]) -cap['EpFp'] = np.array([102,103,115,116,128,129,130,142,143]) -cap['FpG'] = np.array([127,141]) -cap['GH'] = np.array([86,100,101,113,114]) -cap['HHp'] = np.array([72,85,99]) -cap['HpI'] = np.array([45,59]) -cap['IIpp'] = np.array([4,17,18,31,32]) +cap['AAp'] = np.array([25, 26, 39, 40, 41, 53, 54]) +cap['ApB'] = np.array([55, 56, 67, 68, 69, 70, 83, 97]) +cap['BC'] = np.array([80, 81, 82, 93, 94, 95, 96, 107, 108, 109, 121, 122]) +cap['CCp'] = np.array([110, 123, 124, 136, 137, 138, 150, 151]) +cap['CpD'] = np.array([135, 148, 149, 162, 163, 164, 175, 176, 177, 189, 190]) +cap['DDp'] = np.array([174, 186, 187, 188, 200, 201, 202]) +cap['DpE'] = np.array([158, 159, 160, 161, 172, 173, 185, 199]) +cap['EEp'] = np.array( + [106, 117, 118, 119, 120, 131, 132, 133, 134, 145, 146, 147] +) +cap['EpFp'] = np.array([102, 103, 115, 116, 128, 129, 130, 142, 143]) +cap['FpG'] = np.array([127, 141]) +cap['GH'] = np.array([86, 100, 101, 113, 114]) +cap['HHp'] = np.array([72, 85, 99]) +cap['HpI'] = np.array([45, 59]) +cap['IIpp'] = np.array([4, 17, 18, 31, 32]) cap['IppJ'] = np.array([46]) -cap['JJpp'] = np.array([60,73,74,75,87,88,89]) -cap['JppK'] = np.array([49,50,51,52,62,63,64,65,66,76,77,78,79,90,91,92,104,105]) -cap['KKp'] = np.array([23,36,37]) -cap['KpA'] = np.array([24,38]) -cap['SHELVES'] = np.array([61,144,157]) +cap['JJpp'] = np.array([60, 73, 74, 75, 87, 88, 89]) +cap['JppK'] = np.array( + [49, 50, 51, 52, 62, 63, 64, 65, 66, 76, 77, 78, 79, 90, 91, 92, 104, 105] +) +cap['KKp'] = np.array([23, 36, 37]) +cap['KpA'] = np.array([24, 38]) +cap['SHELVES'] = np.array([61, 144, 157]) # Grouping Basins into larger regions (AIS, EAIS, WAIS and APEN) -cap['AIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['EpFp'], cap['FpG'], \ - cap['GH'], cap['HHp'], cap['HpI'], cap['IIpp'], cap['IppJ'], cap['JJpp'], \ - cap['JppK'], cap['KKp'], cap['KpA']),axis=0) #, cap['SHELVES'] -cap['EAIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp'], \ - cap['KpA']),axis=0) -cap['WAIS'] = np.concatenate((cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']),axis=0) -cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']),axis=0) -cap['INTERIOR'] = np.array([38,51,52,53,65,66,67,68,78,79,80,81,91,92,93,94,\ - 104,105,106,107,108,118,119,120,121,122,123,133,134,135,136,147,148,149,\ - 160,161,162,174,175]) -cap['QML'] = np.concatenate((cap['KpA'],cap['AAp'],cap['ApB']),axis=0) -cap['NoQML'] = np.concatenate((cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp']),axis=0) +cap['AIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['EpFp'], + cap['FpG'], + cap['GH'], + cap['HHp'], + cap['HpI'], + cap['IIpp'], + cap['IppJ'], + cap['JJpp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) # , cap['SHELVES'] +cap['EAIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) +cap['WAIS'] = np.concatenate( + (cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']), axis=0 +) +cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']), axis=0) +cap['INTERIOR'] = np.array( + [ + 38, + 51, + 52, + 53, + 65, + 66, + 67, + 68, + 78, + 79, + 80, + 81, + 91, + 92, + 93, + 94, + 104, + 105, + 106, + 107, + 108, + 118, + 119, + 120, + 121, + 122, + 123, + 133, + 134, + 135, + 136, + 147, + 148, + 149, + 160, + 161, + 162, + 174, + 175, + ] +) +cap['QML'] = np.concatenate((cap['KpA'], cap['AAp'], cap['ApB']), axis=0) +cap['NoQML'] = np.concatenate( + ( + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + ), + axis=0, +) cap['ISLAND'] = np.array([62]) -cap['CpDc'] = np.array([163,164,176,177,189,190]) -#cap['CpDc'] = np.array([163,164,177]) -cap['CpDi'] = np.array([135,148,149,162,175]) -cap['TM'] = np.array([135,148,149,162,163,176,177,190]) -cap['DDpc'] = np.array([186,187,188,200,201,202]) -#cap['DDpi'] = np.array([174]) -cap['DDpi'] = np.concatenate((cap['DDp'],cap['DpE']),axis=0) +cap['CpDc'] = np.array([163, 164, 176, 177, 189, 190]) +# cap['CpDc'] = np.array([163,164,177]) +cap['CpDi'] = np.array([135, 148, 149, 162, 175]) +cap['TM'] = np.array([135, 148, 149, 162, 163, 176, 177, 190]) +cap['DDpc'] = np.array([186, 187, 188, 200, 201, 202]) +# cap['DDpi'] = np.array([174]) +cap['DDpi'] = np.concatenate((cap['DDp'], cap['DpE']), axis=0) # different version of west ant -cap['GH2'] = np.array([86,87,99,100,101,113,114]) -cap['HHp2'] = np.array([72,85]) -cap['JJpp2'] = np.array([60,73,74,75,88,89]) -cap['GH3'] = np.array([86,99,100,101,113,114]) +cap['GH2'] = np.array([86, 87, 99, 100, 101, 113, 114]) +cap['HHp2'] = np.array([72, 85]) +cap['JJpp2'] = np.array([60, 73, 74, 75, 88, 89]) +cap['GH3'] = np.array([86, 99, 100, 101, 113, 114]) # Amundsen Sea Embayment regions -cap['PIG'] = np.array([86,87,99]) -cap['THSPK'] = np.array([100,101,113,114]) +cap['PIG'] = np.array([86, 87, 99]) +cap['THSPK'] = np.array([100, 101, 113, 114]) # Greenland Mascons -cap['NW'] = np.array([308,309,312,313,316,317]) -cap['NN'] = np.array([301,302,303,304,305,306]) -cap['NE'] = np.array([307,310,311,314,315]) -cap['SE'] = np.array([318,319,322,323,325,327]) -cap['SW'] = np.array([320,321,324,326]) -cap['GIS'] = np.concatenate((cap['NW'],cap['NN'],cap['NE'],cap['SW'],cap['SE']),axis=0) +cap['NW'] = np.array([308, 309, 312, 313, 316, 317]) +cap['NN'] = np.array([301, 302, 303, 304, 305, 306]) +cap['NE'] = np.array([307, 310, 311, 314, 315]) +cap['SE'] = np.array([318, 319, 322, 323, 325, 327]) +cap['SW'] = np.array([320, 321, 324, 326]) +cap['GIS'] = np.concatenate( + (cap['NW'], cap['NN'], cap['NE'], cap['SW'], cap['SE']), axis=0 +) # Canadian Archipelago -cap['CDE'] = np.array([328,329,330,331,332,333]) -cap['CBI'] = np.array([334,335,336,337,338]) +cap['CDE'] = np.array([328, 329, 330, 331, 332, 333]) +cap['CBI'] = np.array([334, 335, 336, 337, 338]) # Iceland, Svalbard, Franz Josef Land, Svernaya Zemlya and Novaya Zemlya cap['ICL'] = np.array([339]) cap['SVB'] = np.array([340]) cap['FJL'] = np.array([341]) cap['SZEM'] = np.array([342]) -cap['NZEM'] = np.array([343,344]) +cap['NZEM'] = np.array([343, 344]) # Alaska (Denali and Pacific Northwest) -cap['ALK'] = np.arange(345,368) -cap['DEN'] = np.array([345,346,348,349,350,351,352,353,354,355,356,367]) -cap['PNW'] = np.array([347,357,358,359,360,361,362,363,364,365,366]) +cap['ALK'] = np.arange(345, 368) +cap['DEN'] = np.array( + [345, 346, 348, 349, 350, 351, 352, 353, 354, 355, 356, 367] +) +cap['PNW'] = np.array([347, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366]) # Patagonia -cap['PAT'] = np.arange(400,407) +cap['PAT'] = np.arange(400, 407) # All Arctic and all GIC (with patagonia) -cap['ARC'] = np.concatenate((cap['GIS'],cap['CDE'],cap['CBI'],cap['ICL'], - cap['SVB'],cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) -cap['GIC'] = np.concatenate((cap['CDE'],cap['CBI'],cap['ICL'],cap['SVB'], - cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM'],cap['PAT']),axis=0) +cap['ARC'] = np.concatenate( + ( + cap['GIS'], + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + ), + axis=0, +) +cap['GIC'] = np.concatenate( + ( + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + cap['PAT'], + ), + axis=0, +) # Russian Arctic (Franz Josef Land, Svernaya Zemlya and Novaya Zemlya) -cap['RUS'] = np.concatenate((cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) +cap['RUS'] = np.concatenate((cap['FJL'], cap['SZEM'], cap['NZEM']), axis=0) # All caps (Greenland, Glaciers and Ice Caps, Antarctica) -cap['ALL'] = np.concatenate((cap['GIS'],cap['GIC'],cap['AIS']),axis=0) +cap['ALL'] = np.concatenate((cap['GIS'], cap['GIC'], cap['AIS']), axis=0) + # PURPOSE: keep track of threads def info(args): @@ -159,15 +338,18 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') -def combine_HEX_mascons(LMAX, RAD, + +def combine_HEX_mascons( + LMAX, + RAD, MMAX=None, DESTRIPE=False, REDISTRIBUTE_MASCONS=False, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, DATE=False, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -195,17 +377,26 @@ def combine_HEX_mascons(LMAX, RAD, # Read each spherical cap for k in cap['ALL']: # input files from least_squares_mascon.py - a=(FILE_PREFIX,RAD_CAP,k,LMAX,order_str,gw_str,ds_str,ocean_str) + a = ( + FILE_PREFIX, + RAD_CAP, + k, + LMAX, + order_str, + gw_str, + ds_str, + ocean_str, + ) FILE = '{0}SPH_CAP_RAD{1:0.1f}_{2}_L{3:d}{4}{5}{6}{7}.txt'.format(*a) # read cap for mascon and verify shape mass_data[k] = np.loadtxt(OUTPUT_DIRECTORY.joinpath(FILE), ndmin=2) # shape of input data - n_rows,n_cols = mass_data[k].shape + n_rows, n_cols = mass_data[k].shape if DATE: # date information - mon = mass_data[k][:,0].astype(np.int64) - tdec = mass_data[k][:,1] + mon = mass_data[k][:, 0].astype(np.int64) + tdec = mass_data[k][:, 1] # for each region for i in region: @@ -216,16 +407,25 @@ def combine_HEX_mascons(LMAX, RAD, # sum mascons in region for k in cap[i]: if DATE: - mass_reg[i] += mass_data[k][:,2] + mass_reg[i] += mass_data[k][:, 2] # add mascon area to total area (cm^2) - area_reg[i] += 1e10*mass_data[k][0,3] + area_reg[i] += 1e10 * mass_data[k][0, 3] else: - mass_reg[i] += mass_data[k][:,0] + mass_reg[i] += mass_data[k][:, 0] # add mascon area to total area (cm^2) - area_reg[i] += 1e10*mass_data[k][0,1] + area_reg[i] += 1e10 * mass_data[k][0, 1] # output data files - a = (FILE_PREFIX,i,RAD_CAP,ocean_str,LMAX,order_str,gw_str,ds_str) + a = ( + FILE_PREFIX, + i, + RAD_CAP, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + ) FILE1 = '{0}{1}_SPH_CAP_RAD{2:0.1f}{3}_L{4:d}{5}{6}.txt'.format(*a) FILE2 = '{0}{1}_SPH_CAP_RAD{2:0.1f}{3}_L{4:d}{5}{6}_cmwe.txt'.format(*a) FILE3 = '{0}{1}_SPH_CAP_RAD{2:0.1f}{3}_L{4:d}{5}{6}_mbar.txt'.format(*a) @@ -239,13 +439,22 @@ def combine_HEX_mascons(LMAX, RAD, fid3 = mbar_file.open(mode='w', encoding='utf8') # output regional mascon averages total_mass = mass_reg[i] - total_thick = 1e15*total_mass/area_reg[i] - total_mbar = 1e14*g_wmo*total_mass/area_reg[i] + total_thick = 1e15 * total_mass / area_reg[i] + total_mbar = 1e14 * g_wmo * total_mass / area_reg[i] for t in range(n_rows): if DATE: - print(f'{mon[t]:03d} {tdec[t]:12.4f} {total_mass[t]:14.6f}', file=fid1) - print(f'{mon[t]:03d} {tdec[t]:12.4f} {total_thick[t]:14.6f}', file=fid2) - print(f'{mon[t]:03d} {tdec[t]:12.4f} {total_mbar[t]:14.6f}', file=fid3) + print( + f'{mon[t]:03d} {tdec[t]:12.4f} {total_mass[t]:14.6f}', + file=fid1, + ) + print( + f'{mon[t]:03d} {tdec[t]:12.4f} {total_thick[t]:14.6f}', + file=fid2, + ) + print( + f'{mon[t]:03d} {tdec[t]:12.4f} {total_mbar[t]:14.6f}', + file=fid3, + ) else: print(f'{total_mass[t]:14.6f}', file=fid1) print(f'{total_thick[t]:14.6f}', file=fid2) @@ -259,59 +468,95 @@ def combine_HEX_mascons(LMAX, RAD, cmwe_file.chmod(mode=MODE) mbar_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the mass change for spherical cap mascons """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') - parser.add_argument('--file-prefix','-P', - type=str, - help='Prefix string for mascon files') - parser.add_argument('--date','-D', - default=False, action='store_true', - help='Model harmonics are a time series') + help='Output directory for mascon files', + ) + parser.add_argument( + '--file-prefix', '-P', type=str, help='Prefix string for mascon files' + ) + parser.add_argument( + '--date', + '-D', + default=False, + action='store_true', + help='Model harmonics are a time series', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -330,7 +575,8 @@ def main(): OUTPUT_DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, DATE=args.date, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -338,6 +584,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/combine_HEX_spherical_caps.py b/scripts/combine_HEX_spherical_caps.py index b004c043..247e83b7 100644 --- a/scripts/combine_HEX_spherical_caps.py +++ b/scripts/combine_HEX_spherical_caps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_HEX_spherical_caps.py (04/2024) calculates the time-series of ice mass change for spherical cap mascons @@ -68,6 +68,7 @@ Updated 08/2015: removed LMIN parameter (not used in program) Written 06/2015 """ + from __future__ import print_function import sys @@ -85,102 +86,280 @@ # Regions region = [] -region.extend(['AAp', 'ApB', 'BC', 'CCp', 'CpD', 'DDp', 'DpE', 'EEp', 'EpFp', - 'FpG', 'GH', 'HHp', 'HpI', 'IIpp', 'IppJ', 'JJpp', 'JppK','KKp', 'KpA', - 'INTERIOR', 'AIS', 'EAIS', 'WAIS', 'APIS', 'QML', 'NoQML', 'CpDc','CpDi', - 'DDpc','DDpi','GH2','HHp2','JJpp2','GH3','TM','PIG','THSPK']) -region.extend(['NW','NN','NE','SW','SE','GIS','CDE','CBI','ICL','SVB','ALK', - 'DEN','PNW','PAT','FJL','SZEM','NZEM','RUS','ARC','GIC']) +region.extend( + [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', + 'INTERIOR', + 'AIS', + 'EAIS', + 'WAIS', + 'APIS', + 'QML', + 'NoQML', + 'CpDc', + 'CpDi', + 'DDpc', + 'DDpi', + 'GH2', + 'HHp2', + 'JJpp2', + 'GH3', + 'TM', + 'PIG', + 'THSPK', + ] +) +region.extend( + [ + 'NW', + 'NN', + 'NE', + 'SW', + 'SE', + 'GIS', + 'CDE', + 'CBI', + 'ICL', + 'SVB', + 'ALK', + 'DEN', + 'PNW', + 'PAT', + 'FJL', + 'SZEM', + 'NZEM', + 'RUS', + 'ARC', + 'GIC', + ] +) # Cap numbers for each basin cap = {} # the contents of the cap variable will be referenced to the basin name # the cap numbers are the global numbers for the spherical cap RADius -cap['AAp'] = np.array([25,26,39,40,41,53,54]) -cap['ApB'] = np.array([55,56,67,68,69,70,83,97]) -cap['BC'] = np.array([80,81,82,93,94,95,96,107,108,109,121,122]) -cap['CCp'] = np.array([110,123,124,136,137,138,150,151]) -cap['CpD'] = np.array([135,148,149,162,163,164,175,176,177,189,190]) -cap['DDp'] = np.array([174,186,187,188,200,201,202]) -cap['DpE'] = np.array([158,159,160,161,172,173,185,199]) -cap['EEp'] = np.array([106,117,118,119,120,131,132,133,134,145,146,147]) -cap['EpFp'] = np.array([102,103,115,116,128,129,130,142,143]) -cap['FpG'] = np.array([127,141]) -cap['GH'] = np.array([86,100,101,113,114]) -cap['HHp'] = np.array([72,85,99]) -cap['HpI'] = np.array([45,59]) -cap['IIpp'] = np.array([4,17,18,31,32]) +cap['AAp'] = np.array([25, 26, 39, 40, 41, 53, 54]) +cap['ApB'] = np.array([55, 56, 67, 68, 69, 70, 83, 97]) +cap['BC'] = np.array([80, 81, 82, 93, 94, 95, 96, 107, 108, 109, 121, 122]) +cap['CCp'] = np.array([110, 123, 124, 136, 137, 138, 150, 151]) +cap['CpD'] = np.array([135, 148, 149, 162, 163, 164, 175, 176, 177, 189, 190]) +cap['DDp'] = np.array([174, 186, 187, 188, 200, 201, 202]) +cap['DpE'] = np.array([158, 159, 160, 161, 172, 173, 185, 199]) +cap['EEp'] = np.array( + [106, 117, 118, 119, 120, 131, 132, 133, 134, 145, 146, 147] +) +cap['EpFp'] = np.array([102, 103, 115, 116, 128, 129, 130, 142, 143]) +cap['FpG'] = np.array([127, 141]) +cap['GH'] = np.array([86, 100, 101, 113, 114]) +cap['HHp'] = np.array([72, 85, 99]) +cap['HpI'] = np.array([45, 59]) +cap['IIpp'] = np.array([4, 17, 18, 31, 32]) cap['IppJ'] = np.array([46]) -cap['JJpp'] = np.array([60,73,74,75,87,88,89]) -cap['JppK'] = np.array([49,50,51,52,62,63,64,65,66,76,77,78,79,90,91,92,104,105]) -cap['KKp'] = np.array([23,36,37]) -cap['KpA'] = np.array([24,38]) -cap['SHELVES'] = np.array([61,144,157]) +cap['JJpp'] = np.array([60, 73, 74, 75, 87, 88, 89]) +cap['JppK'] = np.array( + [49, 50, 51, 52, 62, 63, 64, 65, 66, 76, 77, 78, 79, 90, 91, 92, 104, 105] +) +cap['KKp'] = np.array([23, 36, 37]) +cap['KpA'] = np.array([24, 38]) +cap['SHELVES'] = np.array([61, 144, 157]) # Grouping Basins into larger regions (AIS, EAIS, WAIS and APEN) -cap['AIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['EpFp'], cap['FpG'], \ - cap['GH'], cap['HHp'], cap['HpI'], cap['IIpp'], cap['IppJ'], cap['JJpp'], \ - cap['JppK'], cap['KKp'], cap['KpA']),axis=0) #, cap['SHELVES'] -cap['EAIS'] = np.concatenate((cap['AAp'], cap['ApB'], cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp'], \ - cap['KpA']),axis=0) -cap['WAIS'] = np.concatenate((cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']),axis=0) -cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']),axis=0) -cap['INTERIOR'] = np.array([38,51,52,53,65,66,67,68,78,79,80,81,91,92,93,94,\ - 104,105,106,107,108,118,119,120,121,122,123,133,134,135,136,147,148,149,\ - 160,161,162,174,175]) -cap['QML'] = np.concatenate((cap['KpA'],cap['AAp'],cap['ApB']),axis=0) -cap['NoQML'] = np.concatenate((cap['BC'], cap['CCp'], \ - cap['CpD'], cap['DDp'], cap['DpE'], cap['EEp'], cap['JppK'], cap['KKp']),axis=0) +cap['AIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['EpFp'], + cap['FpG'], + cap['GH'], + cap['HHp'], + cap['HpI'], + cap['IIpp'], + cap['IppJ'], + cap['JJpp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) # , cap['SHELVES'] +cap['EAIS'] = np.concatenate( + ( + cap['AAp'], + cap['ApB'], + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + cap['KpA'], + ), + axis=0, +) +cap['WAIS'] = np.concatenate( + (cap['EpFp'], cap['FpG'], cap['GH'], cap['HHp'], cap['JJpp']), axis=0 +) +cap['APIS'] = np.concatenate((cap['HpI'], cap['IIpp'], cap['IppJ']), axis=0) +cap['INTERIOR'] = np.array( + [ + 38, + 51, + 52, + 53, + 65, + 66, + 67, + 68, + 78, + 79, + 80, + 81, + 91, + 92, + 93, + 94, + 104, + 105, + 106, + 107, + 108, + 118, + 119, + 120, + 121, + 122, + 123, + 133, + 134, + 135, + 136, + 147, + 148, + 149, + 160, + 161, + 162, + 174, + 175, + ] +) +cap['QML'] = np.concatenate((cap['KpA'], cap['AAp'], cap['ApB']), axis=0) +cap['NoQML'] = np.concatenate( + ( + cap['BC'], + cap['CCp'], + cap['CpD'], + cap['DDp'], + cap['DpE'], + cap['EEp'], + cap['JppK'], + cap['KKp'], + ), + axis=0, +) cap['ISLAND'] = np.array([62]) -cap['CpDc'] = np.array([163,164,176,177,189,190]) -#cap['CpDc'] = np.array([163,164,177]) -cap['CpDi'] = np.array([135,148,149,162,175]) -cap['TM'] = np.array([135,148,149,162,163,176,177,190]) -cap['DDpc'] = np.array([186,187,188,200,201,202]) -#cap['DDpi'] = np.array([174]) -cap['DDpi'] = np.concatenate((cap['DDp'],cap['DpE']),axis=0) +cap['CpDc'] = np.array([163, 164, 176, 177, 189, 190]) +# cap['CpDc'] = np.array([163,164,177]) +cap['CpDi'] = np.array([135, 148, 149, 162, 175]) +cap['TM'] = np.array([135, 148, 149, 162, 163, 176, 177, 190]) +cap['DDpc'] = np.array([186, 187, 188, 200, 201, 202]) +# cap['DDpi'] = np.array([174]) +cap['DDpi'] = np.concatenate((cap['DDp'], cap['DpE']), axis=0) # different version of west ant -cap['GH2'] = np.array([86,87,99,100,101,113,114]) -cap['HHp2'] = np.array([72,85]) -cap['JJpp2'] = np.array([60,73,74,75,88,89]) -cap['GH3'] = np.array([86,99,100,101,113,114]) +cap['GH2'] = np.array([86, 87, 99, 100, 101, 113, 114]) +cap['HHp2'] = np.array([72, 85]) +cap['JJpp2'] = np.array([60, 73, 74, 75, 88, 89]) +cap['GH3'] = np.array([86, 99, 100, 101, 113, 114]) # Amundsen Sea Embayment regions -cap['PIG'] = np.array([86,87,99]) -cap['THSPK'] = np.array([100,101,113,114]) +cap['PIG'] = np.array([86, 87, 99]) +cap['THSPK'] = np.array([100, 101, 113, 114]) # Greenland Mascons -cap['NW'] = np.array([308,309,312,313,316,317]) -cap['NN'] = np.array([301,302,303,304,305,306]) -cap['NE'] = np.array([307,310,311,314,315]) -cap['SE'] = np.array([318,319,322,323,325,327]) -cap['SW'] = np.array([320,321,324,326]) -cap['GIS'] = np.concatenate((cap['NW'],cap['NN'],cap['NE'],cap['SW'],cap['SE']),axis=0) +cap['NW'] = np.array([308, 309, 312, 313, 316, 317]) +cap['NN'] = np.array([301, 302, 303, 304, 305, 306]) +cap['NE'] = np.array([307, 310, 311, 314, 315]) +cap['SE'] = np.array([318, 319, 322, 323, 325, 327]) +cap['SW'] = np.array([320, 321, 324, 326]) +cap['GIS'] = np.concatenate( + (cap['NW'], cap['NN'], cap['NE'], cap['SW'], cap['SE']), axis=0 +) # Canadian Archipelago -cap['CDE'] = np.array([328,329,330,331,332,333]) -cap['CBI'] = np.array([334,335,336,337,338]) +cap['CDE'] = np.array([328, 329, 330, 331, 332, 333]) +cap['CBI'] = np.array([334, 335, 336, 337, 338]) # Iceland, Svalbard, Franz Josef Land, Svernaya Zemlya and Novaya Zemlya cap['ICL'] = np.array([339]) cap['SVB'] = np.array([340]) cap['FJL'] = np.array([341]) cap['SZEM'] = np.array([342]) -cap['NZEM'] = np.array([343,344]) +cap['NZEM'] = np.array([343, 344]) # Alaska (Denali and Pacific Northwest) -cap['ALK'] = np.arange(345,368) -cap['DEN'] = np.array([345,346,348,349,350,351,352,353,354,355,356,367]) -cap['PNW'] = np.array([347,357,358,359,360,361,362,363,364,365,366]) +cap['ALK'] = np.arange(345, 368) +cap['DEN'] = np.array( + [345, 346, 348, 349, 350, 351, 352, 353, 354, 355, 356, 367] +) +cap['PNW'] = np.array([347, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366]) # Patagonia -cap['PAT'] = np.arange(400,407) +cap['PAT'] = np.arange(400, 407) # All Arctic and all GIC (with patagonia) -cap['ARC'] = np.concatenate((cap['GIS'],cap['CDE'],cap['CBI'],cap['ICL'], - cap['SVB'],cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) -cap['GIC'] = np.concatenate((cap['CDE'],cap['CBI'],cap['ICL'],cap['SVB'], - cap['ALK'],cap['FJL'],cap['SZEM'],cap['NZEM'],cap['PAT']),axis=0) +cap['ARC'] = np.concatenate( + ( + cap['GIS'], + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + ), + axis=0, +) +cap['GIC'] = np.concatenate( + ( + cap['CDE'], + cap['CBI'], + cap['ICL'], + cap['SVB'], + cap['ALK'], + cap['FJL'], + cap['SZEM'], + cap['NZEM'], + cap['PAT'], + ), + axis=0, +) # Russian Arctic (Franz Josef Land, Svernaya Zemlya and Novaya Zemlya) -cap['RUS'] = np.concatenate((cap['FJL'],cap['SZEM'],cap['NZEM']),axis=0) +cap['RUS'] = np.concatenate((cap['FJL'], cap['SZEM'], cap['NZEM']), axis=0) # All caps (Greenland, Glaciers and Ice Caps, Antarctica) -cap['ALL'] = np.concatenate((cap['GIS'],cap['GIC'],cap['AIS']),axis=0) +cap['ALL'] = np.concatenate((cap['GIS'], cap['GIC'], cap['AIS']), axis=0) + # PURPOSE: keep track of threads def info(args): @@ -191,7 +370,13 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') -def combine_HEX_spherical_caps(PROC, DREL, DSET, LMAX, RAD, + +def combine_HEX_spherical_caps( + PROC, + DREL, + DSET, + LMAX, + RAD, MMAX=None, DESTRIPE=False, GIA=None, @@ -200,8 +385,8 @@ def combine_HEX_spherical_caps(PROC, DREL, DSET, LMAX, RAD, REDISTRIBUTE_MASCONS=False, REDISTRIBUTE_REMOVED=False, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -247,11 +432,23 @@ def combine_HEX_spherical_caps(PROC, DREL, DSET, LMAX, RAD, # Read each spherical cap for k in cap['ALL']: # GRACE files - a=(RAD_CAP,k,FILE_TITLE,atm_str,ocean_str,LMAX,order_str,gw_str,ds_str) - f1='SPH_CAP_RAD{0:0.1f}_{1:d}_{2}{3}{4}L{5:d}{6}{7}{8}.txt'.format(*a) + a = ( + RAD_CAP, + k, + FILE_TITLE, + atm_str, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + ) + f1 = 'SPH_CAP_RAD{0:0.1f}_{1:d}_{2}{3}{4}L{5:d}{6}{7}{8}.txt'.format(*a) FILE1 = OUTPUT_DIRECTORY.joinpath(f1) # leakage file - f2='SPH_CAP_RAD{0:0.1f}_{1:d}_{2}{3}{4}L{5:d}{6}{7}{8}_LEAKAGE.txt'.format(*a) + f2 = 'SPH_CAP_RAD{0:0.1f}_{1:d}_{2}{3}{4}L{5:d}{6}{7}{8}_LEAKAGE.txt'.format( + *a + ) FILE2 = OUTPUT_DIRECTORY.joinpath(f2) # read cap time-series for GRACE grace_data[k] = np.loadtxt(FILE1) @@ -259,14 +456,14 @@ def combine_HEX_spherical_caps(PROC, DREL, DSET, LMAX, RAD, if FILE2.exists(): # if on final iteration and leakage file is available leakage_input = np.loadtxt(FILE2, ndmin=2) - statistical_leak[k] = leakage_input[:,2] + statistical_leak[k] = leakage_input[:, 2] else: # set as zero - statistical_leak[k] = np.zeros_like(grace_data[k][:,2]) + statistical_leak[k] = np.zeros_like(grace_data[k][:, 2]) # date information - mon = grace_data[k][:,0].astype(np.int64) - tdec = grace_data[k][:,1] + mon = grace_data[k][:, 0].astype(np.int64) + tdec = grace_data[k][:, 1] n_mon = len(mon) # for each region @@ -280,20 +477,29 @@ def combine_HEX_spherical_caps(PROC, DREL, DSET, LMAX, RAD, area_reg[i] = 0.0 # sum mascons in region for k in cap[i]: - grace_reg[i] += grace_data[k][:,2] + grace_reg[i] += grace_data[k][:, 2] # RMS sum of GRACE error components - grace_error[i] += (grace_data[k][:,3])**2 + grace_error[i] += (grace_data[k][:, 3]) ** 2 # RMS sum of GRACE leakage components - statistical_reg[i] += (statistical_leak[k])**2 + statistical_reg[i] += (statistical_leak[k]) ** 2 # add mascon area to total area (cm^2) - area_reg[i] += 1e10*grace_data[k][0,4] + area_reg[i] += 1e10 * grace_data[k][0, 4] # RMS sum of error and leakage components grace_error[i] = np.sqrt(grace_error[i]) statistical_reg[i] = np.sqrt(statistical_reg[i]) # output data files - a = (FILE_PREFIX,i,atm_str,ocean_str,LMAX,order_str,gw_str,ds_str) + a = ( + FILE_PREFIX, + i, + atm_str, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + ) FILE1 = '{0}{1}_SPH_CAP_{2}{3}L{4:d}{5}{6}{7}.txt'.format(*a) FILE2 = '{0}{1}_SPH_CAP_{2}{3}L{4:d}{5}{6}{7}_cmwe.txt'.format(*a) # open files for writing region time-series @@ -304,18 +510,24 @@ def combine_HEX_spherical_caps(PROC, DREL, DSET, LMAX, RAD, fid2 = cmwe_file.open(mode='w', encoding='utf8') # output GRACE regional time-series total_mass = grace_reg[i] - total_thick = 1e15*total_mass/area_reg[i] + total_thick = 1e15 * total_mass / area_reg[i] total_error = np.hypot(grace_error[i], statistical_reg[i]) - thick_error = 1e15*total_error/area_reg[i] + thick_error = 1e15 * total_error / area_reg[i] # total area in kilometers^2 - area_km = area_reg[i]/1e10 + area_km = area_reg[i] / 1e10 for t in range(n_mon): mass_anomaly = total_mass[t] - total_mass.mean() - thick_anomaly = total_thick[t]-total_thick.mean() - print(f'{mon[t]:03d} {tdec[t]:12.4f} {mass_anomaly:14.6f} ' - f'{total_error[t]:14.6f} {area_km:16.5f}', file=fid1) - print(f'{mon[t]:03d} {tdec[t]:12.4f} {thick_anomaly:14.6f} ' - f'{thick_error[t]:14.6f} {area_km:16.5f}', file=fid2) + thick_anomaly = total_thick[t] - total_thick.mean() + print( + f'{mon[t]:03d} {tdec[t]:12.4f} {mass_anomaly:14.6f} ' + f'{total_error[t]:14.6f} {area_km:16.5f}', + file=fid1, + ) + print( + f'{mon[t]:03d} {tdec[t]:12.4f} {thick_anomaly:14.6f} ' + f'{thick_error[t]:14.6f} {area_km:16.5f}', + file=fid2, + ) # close the output file fid1.close() fid2.close() @@ -323,47 +535,82 @@ def combine_HEX_spherical_caps(PROC, DREL, DSET, LMAX, RAD, mass_file.chmod(mode=MODE) cmwe_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the time-series of ice mass change for spherical cap mascons """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -379,39 +626,62 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -435,7 +705,8 @@ def main(): REDISTRIBUTE_MASCONS=args.redistribute_mascons, REDISTRIBUTE_REMOVED=args.redistribute_removed, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -443,6 +714,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/combine_harmonics.py b/scripts/combine_harmonics.py index 714cff2f..a7269579 100644 --- a/scripts/combine_harmonics.py +++ b/scripts/combine_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_harmonics.py Written by Tyler Sutterley (10/2023) Converts a file from the spherical harmonic domain into the spatial domain @@ -108,6 +108,7 @@ can output a non-global grid by setting bounding box parameters Written 07/2018 """ + from __future__ import print_function import sys @@ -121,6 +122,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -130,8 +132,11 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: converts from the spherical harmonic domain into the spatial domain -def combine_harmonics(INPUT_FILE, OUTPUT_FILE, +def combine_harmonics( + INPUT_FILE, + OUTPUT_FILE, LMAX=None, MMAX=None, LOVE_NUMBERS=0, @@ -147,8 +152,8 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, MEAN_FILE=None, DATAFORM=None, DATE=False, - MODE=0o775): - + MODE=0o775, +): # verify inputs INPUT_FILE = pathlib.Path(INPUT_FILE).expanduser().absolute() OUTPUT_FILE = pathlib.Path(OUTPUT_FILE).expanduser().absolute() @@ -157,7 +162,9 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, # attributes for output files attributes = dict(ROOT={}) attributes['ROOT']['product_type'] = 'gravity_field' - attributes['ROOT']['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['ROOT']['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # upper bound of spherical harmonic orders (default = LMAX) if MMAX is None: @@ -166,14 +173,16 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, # read input spherical harmonic coefficients from file if DATAFORM in ('ascii', 'netCDF4', 'HDF5'): dataform = copy.copy(DATAFORM) - input_Ylms = gravtk.harmonics().from_file(INPUT_FILE, - format=DATAFORM, date=DATE) + input_Ylms = gravtk.harmonics().from_file( + INPUT_FILE, format=DATAFORM, date=DATE + ) attributes['ROOT']['lineage'] = input_Ylms.filename.name elif DATAFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,dataform = DATAFORM.split('-') - input_Ylms = gravtk.harmonics().from_index(INPUT_FILE, - format=dataform, date=DATE) + _, dataform = DATAFORM.split('-') + input_Ylms = gravtk.harmonics().from_index( + INPUT_FILE, format=dataform, date=DATE + ) attributes['ROOT']['lineage'] = [f.name for f in input_Ylms.filename] # reform harmonic dimensions to be l,m,t # truncate to degree and order LMAX, MMAX @@ -181,13 +190,15 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, # remove mean file from input Ylms if MEAN_FILE: - mean_Ylms = gravtk.harmonics().from_file(MEAN_FILE, - format=DATAFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=DATAFORM, date=False + ) input_Ylms.subtract(mean_Ylms) # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth parameters attributes['ROOT']['earth_model'] = LOVE.model attributes['ROOT']['earth_love_numbers'] = LOVE.citation @@ -199,28 +210,27 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, # distribute total mass uniformly over the ocean if REDISTRIBUTE: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) # 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] + 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 + 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] + 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*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) else: - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) # Output spatial data grid = gravtk.spatial() @@ -229,25 +239,25 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, nt = len(input_Ylms.time) # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output Degree Interval - if (INTERVAL == 1): + if INTERVAL == 1: # (0:360,90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - grid.lon = dlon*np.arange(0,nlon) - grid.lat = 90.0 - dlat*np.arange(0,nlat) - elif (INTERVAL == 2): + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((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) + 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): + 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) + 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) # output spatial grid @@ -277,122 +287,203 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(theta)) # converting harmonics to truncated, smoothed coefficients in output units - for t,Ylms in enumerate(input_Ylms): + for t, Ylms in enumerate(input_Ylms): # convolve spherical harmonics with degree dependent factors - Ylms.convolve(dfactor*wt) + Ylms.convolve(dfactor * wt) # convert spherical harmonics to output spatial grid - grid.data[:,:,t] = gravtk.harmonic_summation(Ylms.clm, Ylms.slm, - grid.lon, grid.lat, LMAX=LMAX, PLM=PLM).T + grid.data[:, :, t] = gravtk.harmonic_summation( + Ylms.clm, Ylms.slm, grid.lon, grid.lat, LMAX=LMAX, PLM=PLM + ).T # outputting data to file - grid.squeeze().to_file(filename=OUTPUT_FILE, format=dataform, - units=units_name, longname=units_longname, - attributes=attributes, date=DATE) + grid.squeeze().to_file( + filename=OUTPUT_FILE, + format=dataform, + units=units_name, + longname=units_longname, + attributes=attributes, + date=DATE, + ) # change output permissions level to MODE OUTPUT_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Converts a file from the spherical harmonic domain into the spatial domain """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # input and output file - parser.add_argument('infile', - type=pathlib.Path, nargs='?', - help='Input harmonic file') - parser.add_argument('outfile', - type=pathlib.Path, nargs='?', - help='Output spatial file') + parser.add_argument( + 'infile', type=pathlib.Path, nargs='?', help='Input harmonic file' + ) + parser.add_argument( + 'outfile', type=pathlib.Path, nargs='?', help='Output spatial file' + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Verbose output of run') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Verbose output of run', + ) # output units - parser.add_argument('--units','-U', - type=int, default=1, choices=[0,1,2,3,4,5], - help='Output units') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[0, 1, 2, 3, 4, 5], + help='Output units', + ) # output grid parameters - parser.add_argument('--spacing','-S', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval','-I', - type=int, default=2, choices=[1,2,3], - help=('Output grid interval ' - '(1: global, 2: centered global, 3: non-global)')) - parser.add_argument('--bounds','-B', - type=float, nargs=4, metavar=('lon_min','lon_max','lat_min','lat_max'), - help='Bounding box for non-global grid') + parser.add_argument( + '--spacing', + '-S', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + '-I', + type=int, + default=2, + choices=[1, 2, 3], + help=( + 'Output grid interval ' + '(1: global, 2: centered global, 3: non-global)' + ), + ) + parser.add_argument( + '--bounds', + '-B', + type=float, + nargs=4, + metavar=('lon_min', 'lon_max', 'lat_min', 'lat_max'), + help='Bounding box for non-global grid', + ) # redistribute total mass over the ocean - parser.add_argument('--redistribute-mass', - default=False, action='store_true', - help='Redistribute total mass over the ocean') + parser.add_argument( + '--redistribute-mass', + default=False, + action='store_true', + help='Redistribute total mass over the ocean', + ) # land-sea mask for redistributing over the ocean - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing over the ocean') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing over the ocean', + ) # mean file to remove - parser.add_argument('--mean', + parser.add_argument( + '--mean', type=pathlib.Path, - help='Mean file to remove from the harmonic data') + help='Mean file to remove from the harmonic data', + ) # input and output data format (ascii, netCDF4, HDF5) choices = [] - choices.extend(['ascii','netCDF4','HDF5']) - choices.extend(['index-ascii','index-netCDF4','index-HDF5']) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=choices, - help='Input and output data format') + choices.extend(['ascii', 'netCDF4', 'HDF5']) + choices.extend(['index-ascii', 'index-netCDF4', 'index-HDF5']) + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=choices, + help='Input and output data format', + ) # Input and output files have date information - parser.add_argument('--date','-D', - default=False, action='store_true', - help='Input and output files have date information') + parser.add_argument( + '--date', + '-D', + default=False, + action='store_true', + help='Input and output files have date information', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the output files (octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -401,7 +492,9 @@ def main(): # run program with parameters try: info(args) - combine_harmonics(args.infile, args.outfile, + combine_harmonics( + args.infile, + args.outfile, LMAX=args.lmax, MMAX=args.mmax, LOVE_NUMBERS=args.love, @@ -417,7 +510,8 @@ def main(): MEAN_FILE=args.mean, DATAFORM=args.format, DATE=args.date, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -425,6 +519,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/combine_sea_level_data.py b/scripts/combine_sea_level_data.py index 1d21cd02..fa9dcb41 100755 --- a/scripts/combine_sea_level_data.py +++ b/scripts/combine_sea_level_data.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_sea_level_data.py Written by Tyler Sutterley (05/2023) Combines the sea level fingerprint data with the input load harmonics @@ -64,6 +64,7 @@ Updated 11/2018: can vary the land-sea mask for ascii files Written 09/2018 """ + from __future__ import print_function import sys @@ -75,6 +76,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -84,8 +86,10 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: combine the sea level data with the input dataset -def combine_sea_level_data(index_file, +def combine_sea_level_data( + index_file, LMAX=None, LOVE_NUMBERS=0, REFERENCE=None, @@ -93,8 +97,8 @@ def combine_sea_level_data(index_file, LANDMASK=None, DIRECTORY=None, FILE_PREFIX=None, - MODE=0o775): - + MODE=0o775, +): # output filename suffix suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')[DATAFORM] # input and output file format @@ -103,10 +107,11 @@ def combine_sea_level_data(index_file, # Land-Sea Mask with Antarctica from Rignot (2017) and Greenland from GEUS # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, - varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) # degree spacing and grid dimensions - dlon,dlat = landsea.spacing + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # longitude and colatitude in radians th = np.radians(90.0 - np.squeeze(landsea.lat)) @@ -114,13 +119,14 @@ def combine_sea_level_data(index_file, # Calculating Legendre Polynomials using Holmes and Featherstone relation PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(th)) # read load Love numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE) + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE + ) # input spherical harmonic datafile index data_Ylms = gravtk.harmonics().from_index(index_file, format=DATAFORM) # truncate to degree and order - data_Ylms.truncate(lmax=LMAX,mmax=LMAX) + data_Ylms.truncate(lmax=LMAX, mmax=LMAX) # index file listing all output spherical harmonic files DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() @@ -132,27 +138,38 @@ def combine_sea_level_data(index_file, # for each grace month and input file for Ylms in data_Ylms: # read sea level file - SLF = file_format.format(FILE_PREFIX,'',LMAX,Ylms.month,suffix) + SLF = file_format.format(FILE_PREFIX, '', LMAX, Ylms.month, suffix) input_file = DIRECTORY.joinpath(SLF) - if (DATAFORM == 'ascii'): - dinput = gravtk.spatial(spacing=[dlon,dlat], - nlon=nlon, nlat=nlat).from_ascii(input_file, date=False) - elif (DATAFORM == 'netCDF4'): + if DATAFORM == 'ascii': + dinput = gravtk.spatial( + spacing=[dlon, dlat], nlon=nlon, nlat=nlat + ).from_ascii(input_file, date=False) + elif DATAFORM == 'netCDF4': dinput = gravtk.spatial().from_netCDF4(input_file, date=False) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': dinput = gravtk.spatial().from_HDF5(input_file, date=False) # Converting sea level field into spherical harmonics - slf_Ylms = gravtk.gen_stokes(dinput.data, dinput.lon, dinput.lat, - UNITS=1, LMIN=0, LMAX=LMAX, PLM=PLM, LOVE=LOVE) + slf_Ylms = gravtk.gen_stokes( + dinput.data, + dinput.lon, + dinput.lat, + UNITS=1, + LMIN=0, + LMAX=LMAX, + PLM=PLM, + LOVE=LOVE, + ) # add sea level harmonics to input harmonics Ylms.add(slf_Ylms) # attributes for output files attributes = {} - attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # output combined sea level harmonics - fargs = (FILE_PREFIX,'CLM_',LMAX,Ylms.month,suffix) + fargs = (FILE_PREFIX, 'CLM_', LMAX, Ylms.month, suffix) output_file = DIRECTORY.joinpath(file_format.format(*fargs)) Ylms.to_file(output_file, format=DATAFORM, **attributes) # change output file permissions mode to MODE @@ -164,69 +181,108 @@ def combine_sea_level_data(index_file, # change the permissions mode of the output index file output_index_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates spatial sensitivity kernels through a least-squares mascon procedure """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', + parser.add_argument( + 'infile', type=pathlib.Path, - help='Input index file with spherical harmonic data files') - parser.add_argument('--output-directory','-O', + help='Input index file with spherical harmonic data files', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for files') - parser.add_argument('--file-prefix','-P', + help='Output directory for files', + ) + parser.add_argument( + '--file-prefix', + '-P', type=str, - help='Prefix string for input and output files') + help='Prefix string for input and output files', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input and output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input and output data format', + ) # land-sea mask - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for sea level fingerprints') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for sea level fingerprints', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger for verbosity level loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -236,7 +292,8 @@ def main(): try: info(args) # run calc_sensitivity_kernel algorithm with parameters - combine_sea_level_data(args.infile, + combine_sea_level_data( + args.infile, LMAX=args.lmax, LOVE_NUMBERS=args.love, REFERENCE=args.reference, @@ -244,7 +301,8 @@ def main(): LANDMASK=args.mask, DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -252,6 +310,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/convert_harmonics.py b/scripts/convert_harmonics.py index c6d7eeee..00342b1b 100644 --- a/scripts/convert_harmonics.py +++ b/scripts/convert_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" convert_harmonics.py Written by Tyler Sutterley (10/2023) Converts a file from the spatial domain into the spherical harmonic domain @@ -87,6 +87,7 @@ Updated 04/2020: updates to reading load love numbers Written 10/2019 """ + from __future__ import print_function import sys @@ -100,6 +101,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -109,8 +111,11 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: converts from the spatial domain into the spherical harmonic domain -def convert_harmonics(INPUT_FILE, OUTPUT_FILE, +def convert_harmonics( + INPUT_FILE, + OUTPUT_FILE, LMAX=None, MMAX=None, UNITS=None, @@ -122,8 +127,8 @@ def convert_harmonics(INPUT_FILE, OUTPUT_FILE, HEADER=None, DATAFORM=None, DATE=False, - MODE=0o775): - + MODE=0o775, +): # verify inputs INPUT_FILE = pathlib.Path(INPUT_FILE).expanduser().absolute() OUTPUT_FILE = pathlib.Path(OUTPUT_FILE).expanduser().absolute() @@ -140,38 +145,50 @@ def convert_harmonics(INPUT_FILE, OUTPUT_FILE, MMAX = np.copy(LMAX) # Grid spacing - dlon,dlat = (DDEG,DDEG) if (np.ndim(DDEG) == 0) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG, DDEG) if (np.ndim(DDEG) == 0) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # read spatial file in data format # expand dimensions - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - input_spatial = gravtk.spatial(fill_value=FILL_VALUE).from_ascii( - INPUT_FILE, header=HEADER, spacing=[dlon,dlat], nlat=nlat, - nlon=nlon, date=DATE).expand_dims() - elif (DATAFORM == 'netCDF4'): + input_spatial = ( + gravtk.spatial(fill_value=FILL_VALUE) + .from_ascii( + INPUT_FILE, + header=HEADER, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + date=DATE, + ) + .expand_dims() + ) + elif DATAFORM == 'netCDF4': # netcdf (.nc) - input_spatial = gravtk.spatial().from_netCDF4( - INPUT_FILE, date=DATE).expand_dims() - elif (DATAFORM == 'HDF5'): + input_spatial = ( + gravtk.spatial().from_netCDF4(INPUT_FILE, date=DATE).expand_dims() + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) - input_spatial = gravtk.spatial().from_HDF5( - INPUT_FILE, date=DATE).expand_dims() + input_spatial = ( + gravtk.spatial().from_HDF5(INPUT_FILE, date=DATE).expand_dims() + ) # convert missing values to zero input_spatial.replace_invalid(0.0) # input data shape nlat, nlon, nt = input_spatial.shape # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth parameters attributes['earth_model'] = LOVE.model attributes['earth_love_numbers'] = LOVE.citation @@ -192,11 +209,19 @@ def convert_harmonics(INPUT_FILE, OUTPUT_FILE, # create list of harmonics objects Ylms_list = [] - for i,spatial_data in enumerate(input_spatial): + for i, spatial_data in enumerate(input_spatial): # convert spatial field to spherical harmonics - output_Ylms = gravtk.gen_stokes(spatial_data.data.T, - spatial_data.lon, spatial_data.lat, UNITS=UNITS, - LMIN=0, LMAX=LMAX, MMAX=MMAX, PLM=PLM, LOVE=LOVE) + output_Ylms = gravtk.gen_stokes( + spatial_data.data.T, + spatial_data.lon, + spatial_data.lat, + UNITS=UNITS, + LMIN=0, + LMAX=LMAX, + MMAX=MMAX, + PLM=PLM, + LOVE=LOVE, + ) # calculate date information if DATE: output_Ylms.time = np.copy(spatial_data.time) @@ -213,89 +238,146 @@ def convert_harmonics(INPUT_FILE, OUTPUT_FILE, # change output permissions level to MODE OUTPUT_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Converts a file from the spatial domain into the spherical harmonic domain """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # input and output file - parser.add_argument('infile', - type=pathlib.Path, nargs='?', - help='Input spatial file') - parser.add_argument('outfile', - type=pathlib.Path, nargs='?', - help='Output harmonic file') + parser.add_argument( + 'infile', type=pathlib.Path, nargs='?', help='Input spatial file' + ) + parser.add_argument( + 'outfile', type=pathlib.Path, nargs='?', help='Output harmonic file' + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # input units # 1: cm of water thickness (cmwe) # 2: Gigatonnes (Gt) # 3: mm of water thickness kg/m^2 - parser.add_argument('--units','-U', - type=int, default=1, choices=[1,2,3], - help='Input units of spatial fields') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[1, 2, 3], + help='Input units of spatial fields', + ) # output grid parameters - parser.add_argument('--spacing','-S', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of input data') - parser.add_argument('--interval','-I', - type=int, default=2, choices=[1,2], - help='Input grid interval (1: global, 2: centered global)') + parser.add_argument( + '--spacing', + '-S', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of input data', + ) + parser.add_argument( + '--interval', + '-I', + type=int, + default=2, + choices=[1, 2], + help='Input grid interval (1: global, 2: centered global)', + ) # fill value for ascii - parser.add_argument('--fill-value','-f', + parser.add_argument( + '--fill-value', + '-f', type=float, - help='Set fill_value for input spatial fields') + help='Set fill_value for input spatial fields', + ) # ascii parameters - parser.add_argument('--header', + parser.add_argument( + '--header', type=int, - help='Number of header rows to skip in input ascii files') + help='Number of header rows to skip in input ascii files', + ) # input and output data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input and output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input and output data format', + ) # Input and output files have date information - parser.add_argument('--date','-D', - default=False, action='store_true', - help='Input and output files have date information') + parser.add_argument( + '--date', + '-D', + default=False, + action='store_true', + help='Input and output files have date information', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the output files (octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -304,7 +386,9 @@ def main(): # run program with parameters try: info(args) - convert_harmonics(args.infile, args.outfile, + convert_harmonics( + args.infile, + args.outfile, LMAX=args.lmax, MMAX=args.mmax, LOVE_NUMBERS=args.love, @@ -316,7 +400,8 @@ def main(): HEADER=args.header, DATAFORM=args.format, DATE=args.date, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -324,6 +409,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/copy_parameter_files.py b/scripts/copy_parameter_files.py index 6da8160e..5ca2b2a9 100644 --- a/scripts/copy_parameter_files.py +++ b/scripts/copy_parameter_files.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" copy_parameter_files.py Written by Tyler Sutterley (05/2023) copies a set of parameter files for the latest months with updated missing @@ -80,6 +80,7 @@ Updated 02/2016: added lines for MMAX==LMAX Written 12/2015 """ + from __future__ import print_function import sys @@ -92,11 +93,24 @@ import numpy as np import gravity_toolkit as gravtk -# PURPOSE: read listed parameter file and write a new one for month range -def copy_parameter_files(base_dir, parameter_file, NEW_START, NEW_END, NEW_DREL, - NEW_DSET, LMAX=None, MMAX=None, DEG1=None, SLR_C20=None, SLR_C30=None, - COMMENTS=False, OVERWRITE=False, MODE=0o775): +# PURPOSE: read listed parameter file and write a new one for month range +def copy_parameter_files( + base_dir, + parameter_file, + NEW_START, + NEW_END, + NEW_DREL, + NEW_DSET, + LMAX=None, + MMAX=None, + DEG1=None, + SLR_C20=None, + SLR_C30=None, + COMMENTS=False, + OVERWRITE=False, + MODE=0o775, +): # read prior parameter file # Opening parameter file and assigning file ID object (fid) parameter_file = pathlib.Path(parameter_file).expanduser().absolute() @@ -119,7 +133,7 @@ def copy_parameter_files(base_dir, parameter_file, NEW_START, NEW_END, NEW_DREL, if NEW_DREL is None: NEW_DREL = parameters['--release'] drel_str = '' - elif (NEW_DREL == OLD_DREL): + elif NEW_DREL == OLD_DREL: drel_str = '' else: drel_str = '_{NEW_DREL}' @@ -129,7 +143,7 @@ def copy_parameter_files(base_dir, parameter_file, NEW_START, NEW_END, NEW_DREL, if NEW_DSET is None: NEW_DSET = parameters['--product'] dset_str = '' - elif (NEW_DSET == OLD_DSET): + elif NEW_DSET == OLD_DSET: dset_str = '' else: dset_str = f'_{NEW_DSET}' @@ -158,9 +172,11 @@ def copy_parameter_files(base_dir, parameter_file, NEW_START, NEW_END, NEW_DREL, # previous date range from parameter file OLD_START = np.int64(parameters['--start']) OLD_END = np.int64(parameters['--end']) - OLD_MISSING = np.array(parameters['--missing'].split(','),dtype=np.int64) + OLD_MISSING = np.array(parameters['--missing'].split(','), dtype=np.int64) # get the latest GRACE months for output data set - grace_months = gravtk.grace_find_months(base_dir, PROC, NEW_DREL, DSET=NEW_DSET) + grace_months = gravtk.grace_find_months( + base_dir, PROC, NEW_DREL, DSET=NEW_DSET + ) # default start and end months are the first and latest months if NEW_START is None: NEW_START = grace_months['start'] @@ -173,7 +189,9 @@ def copy_parameter_files(base_dir, parameter_file, NEW_START, NEW_END, NEW_DREL, mapping = [] mapping.append(('parameters', f'parameters{drel_str}{dset_str}')) mapping.append((f'L{LMAX:d}', f'L{LMAX:d}{order_str}')) - mapping.append((f'{OLD_START:03d}-{OLD_END:03d}', f'{NEW_START:03d}-{NEW_END:03d}')) + mapping.append( + (f'{OLD_START:03d}-{OLD_END:03d}', f'{NEW_START:03d}-{NEW_END:03d}') + ) FILE = copy.copy(parameter_file.name) for key, val in mapping: FILE = FILE.replace(key, val) @@ -188,16 +206,19 @@ def copy_parameter_files(base_dir, parameter_file, NEW_START, NEW_END, NEW_DREL, part = fileline.split() comments = ' '.join(p for p in part[2:]) # filling the parameter definition variables - regex = r'(\-\-output\-directory|\-\-reconstruct\-file|\-\-remove\-file)' - if bool(re.match(regex,part[0])): + regex = ( + r'(\-\-output\-directory|\-\-reconstruct\-file|\-\-remove\-file)' + ) + if bool(re.match(regex, part[0])): # add MMAX string for order 30 solutions # replace the old starting month with the new one # replace the old ending month with the new one # replace the old dataset string with the new dataset string mapping = [] mapping.append(('L60', f'L60{order_str}')) - mapping.append((f'{OLD_START:03d}-{OLD_END:03d}' - f'{NEW_START:03d}-{NEW_END:03d}')) + mapping.append( + (f'{OLD_START:03d}-{OLD_END:03d}{NEW_START:03d}-{NEW_END:03d}') + ) mapping.append((OLD_DREL, NEW_DREL)) mapping.append((OLD_DSET, NEW_DSET)) for key, val in mapping: @@ -211,59 +232,63 @@ def copy_parameter_files(base_dir, parameter_file, NEW_START, NEW_END, NEW_DREL, # replace the old dataset string with the new dataset string mapping = [] mapping.append(('L60', f'L60{order_str}')) - mapping.append((f'{OLD_START:03d}-{OLD_END:03d}', - f'{NEW_START:03d}-{NEW_END:03d}')) + mapping.append( + ( + f'{OLD_START:03d}-{OLD_END:03d}', + f'{NEW_START:03d}-{NEW_END:03d}', + ) + ) mapping.append((OLD_DSET, NEW_DSET)) for key, val in mapping: fileline = fileline.replace(key, val) print(fileline, file=fid) - elif (part[0] == '--start'): + elif part[0] == '--start': # replace the old starting month with the new one mapping = [(f'{OLD_START:d}', f'{OLD_START:d}')] for key, val in mapping: fileline = fileline.replace(key, val) print(fileline, file=fid) - elif (part[0] == '--end'): + elif part[0] == '--end': # replace the old ending month with the new one mapping = [(f'{OLD_END:d}', f'{NEW_END:d}')] for key, val in mapping: fileline = fileline.replace(key, val) print(fileline, file=fid) - elif (part[0] == '--missing'): + elif part[0] == '--missing': # update the missing months missing = ' '.join(f'{m:d}' for m in NEW_MISSING) print(f'{part[0]}\t{missing}\t{comments}', file=fid) - elif (part[0] == '--release'): + elif part[0] == '--release': # replace the old release with the new one mapping = [(OLD_DREL, NEW_DREL)] for key, val in mapping: fileline = fileline.replace(key, val) print(fileline, file=fid) - elif (part[0] == '--product'): + elif part[0] == '--product': # replace the old product with the new one mapping = [(OLD_DSET, NEW_DSET)] for key, val in mapping: fileline = fileline.replace(key, val) print(fileline, file=fid) - elif (part[0] == '--geocenter'): + elif part[0] == '--geocenter': # Geocenter correction to DEG1 print(f'{part[0]}\t{DEG1}\t{comments}', file=fid) - elif (part[0] == '--slr-c20'): + elif part[0] == '--slr-c20': # Oblateness correction print(f'{part[0]}\t{SLR_C20}\t{comments}', file=fid) - elif (part[0] == '--slr-c30'): + elif part[0] == '--slr-c30': # C30 correction print(f'{part[0]}\t{SLR_C30}\t{comments}', file=fid) - elif (part[0] == '--lmax'): + elif part[0] == '--lmax': # LMAX to new spherical harmonic degree print(f'{part[0]}\t{LMAX:d}\t{comments}', file=fid) @@ -287,6 +312,7 @@ def copy_parameter_files(base_dir, parameter_file, NEW_START, NEW_END, NEW_DREL, if OVERWRITE: parameter_file.unlink() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -295,70 +321,131 @@ def arguments(): """ ) # command line parameters - parser.add_argument('parameters', - type=pathlib.Path, nargs='+', - help='Parameter files containing specific variables for each analysis') + parser.add_argument( + 'parameters', + type=pathlib.Path, + nargs='+', + help='Parameter files containing specific variables for each analysis', + ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=None, - help='Starting GRACE/GRACE-FO month for output file') - parser.add_argument('--end','-E', - type=int, default=None, - help='Ending GRACE/GRACE-FO month for output file') + parser.add_argument( + '--start', + '-S', + type=int, + default=None, + help='Starting GRACE/GRACE-FO month for output file', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=None, + help='Ending GRACE/GRACE-FO month for output file', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default=None, - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default=None, + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default=None, - help='GRACE/GRACE-FO Data Product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default=None, + help='GRACE/GRACE-FO Data Product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # geocenter, oblateness and low degree zonals - parser.add_argument('--geocenter', - type=str, metavar='DEG1', default=None, - help='Geocenter Product to use in file') - parser.add_argument('--slr-c20', - type=str, metavar='C20', default=None, - help='C20 Product to use in file') - parser.add_argument('--slr-c30', - type=str, metavar='C30', default=None, - help='C30 Product to use in file') + parser.add_argument( + '--geocenter', + type=str, + metavar='DEG1', + default=None, + help='Geocenter Product to use in file', + ) + parser.add_argument( + '--slr-c20', + type=str, + metavar='C20', + default=None, + help='C20 Product to use in file', + ) + parser.add_argument( + '--slr-c30', + type=str, + metavar='C30', + default=None, + help='C30 Product to use in file', + ) # keep commented lines - parser.add_argument('--comments','-C', - default=False, action='store_true', - help='Keep commented lines in parameter file') + parser.add_argument( + '--comments', + '-C', + default=False, + action='store_true', + help='Keep commented lines in parameter file', + ) # keep commented lines - parser.add_argument('--overwrite','-O', - default=False, action='store_true', - help='Remove previous version of parameter file') + parser.add_argument( + '--overwrite', + '-O', + default=False, + action='store_true', + help='Remove previous version of parameter file', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the output files (octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger for verbosity level loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -366,8 +453,13 @@ def main(): # run directly for file for f in args.parameters: - copy_parameter_files(args.directory, f, args.start, args.end, - args.release, args.product, + copy_parameter_files( + args.directory, + f, + args.start, + args.end, + args.release, + args.product, LMAX=args.lmax, MMAX=args.mmax, DEG1=args.geocenter, @@ -375,7 +467,9 @@ def main(): SLR_C30=args.slr_c30, COMMENTS=args.comments, OVERWRITE=args.overwrite, - MODE=args.mode) + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/scripts/create_public_SLF_data.py b/scripts/create_public_SLF_data.py index dc38953c..10a54db1 100644 --- a/scripts/create_public_SLF_data.py +++ b/scripts/create_public_SLF_data.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" create_public_SLF_data.py Written by Tyler Sutterley (10/2023) Creates public sea level fingerprint files @@ -11,6 +11,7 @@ updated inputs to spatial from_ascii function Written 08/2019 """ + from __future__ import print_function import sys @@ -27,6 +28,7 @@ filename = inspect.getframeinfo(inspect.currentframe()).filename filepath = pathlib.Path(filename).absolute().parent + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -36,8 +38,14 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # program module to run with specified parameters -def create_public_data(base_dir, PROC, DREL, DSET, LMAX, +def create_public_data( + base_dir, + PROC, + DREL, + DSET, + LMAX, START_MON=None, END_MON=None, MISSING=None, @@ -49,8 +57,8 @@ def create_public_data(base_dir, PROC, DREL, DSET, LMAX, LANDMASK=None, DATAFORM=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # for datasets not GSM: will add a label for the dataset dset_str = '' if (DSET == 'GSM') else f'_{DSET}' # input GIA stokes coefficients to get titles @@ -60,40 +68,57 @@ def create_public_data(base_dir, PROC, DREL, DSET, LMAX, # mascon distribution over the ocean ocean_str = '_OCN' if REDISTRIBUTE_MASCONS else '' # version flags - VERSION = ['v0',''] + VERSION = ['v0', ''] DATA_VERSION = 1 # Land-Sea Mask with Antarctica from Rignot (2017) and Greenland from GEUS # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF4 file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, - varname='LSMASK') - dlon,dlat = landsea.spacing + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # read load love numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=0, - REFERENCE='CF', FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=0, REFERENCE='CF', FORMAT='class' + ) # input directory - FLAG={1:'',2:'_SLF1',3:'_SLF2',4:'_SLF3',5:'_SLF3',6:'_SLF3',7:'_SLF3'} + FLAG = { + 1: '', + 2: '_SLF1', + 3: '_SLF2', + 4: '_SLF3', + 5: '_SLF3', + 6: '_SLF3', + 7: '_SLF3', + } # subdirectory and input file formats sd = 'HEX_{0}_{1}{2}_SPH_CAP_MSCNS{3}_L{4:d}_{5:03d}-{6:03d}' # input and output format file_format = 'SLF_ITERATION_{0}{1}{2}{3}_{4}L{5:d}{6}_{7:03d}.{8}' # input mascon file for GIA correction - subdir = sd.format(PROC,DREL,VERSION[DATA_VERSION], - FLAG[ITERATION],LMAX,START_MON,END_MON) + subdir = sd.format( + PROC, + DREL, + VERSION[DATA_VERSION], + FLAG[ITERATION], + LMAX, + START_MON, + END_MON, + ) # input directory setup base_dir = pathlib.Path(base_dir).expanduser().absolute() - mascon_dir = base_dir.joinpath('GRACE','mascons',subdir) + mascon_dir = base_dir.joinpath('GRACE', 'mascons', subdir) # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): OUTPUT_DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) # list of all months - months = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + months = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) nmon = len(months) # output filename suffix suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') @@ -102,17 +127,30 @@ def create_public_data(base_dir, PROC, DREL, DSET, LMAX, spatial_list = [] for t in range(0, nmon): # sea level file for month (spatial fields) - SLF = file_format.format(ITERATION, dset_str, gia_str, ocean_str, '', - EXPANSION, '', months[t], suffix[DATAFORM]) + SLF = file_format.format( + ITERATION, + dset_str, + gia_str, + ocean_str, + '', + EXPANSION, + '', + months[t], + suffix[DATAFORM], + ) # read sea level file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(mascon_dir.joinpath(SLF), - spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + mascon_dir.joinpath(SLF), + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM == 'netCDF4': # netcdf (.nc) dinput = gravtk.spatial().from_netCDF4(mascon_dir.joinpath(SLF)) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 (.H5) dinput = gravtk.spatial().from_HDF5(mascon_dir.joinpath(SLF)) # append to spatial list @@ -139,18 +177,32 @@ def create_public_data(base_dir, PROC, DREL, DSET, LMAX, kwargs['attributes']['ROOT']['reference_frame'] = LOVE.reference kwargs['attributes']['ROOT']['earth_body_tide'] = 'Wahr (1981)' kwargs['attributes']['ROOT']['earth_fluid_love'] = 'Han and Wahr (1989)' - kwargs['attributes']['ROOT']['polar_motion_feedback'] = 'Kendall et al. (2005)' - kwargs['attributes']['ROOT']['reference'] = \ + kwargs['attributes']['ROOT']['polar_motion_feedback'] = ( + 'Kendall et al. (2005)' + ) + kwargs['attributes']['ROOT']['reference'] = ( f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # data summary MISSION = 'GRACE/GRACE-FO' SUMMARY = [] - SUMMARY.append(('Sea level variations derived from {0} ' - 'mission measurements').format(MISSION)) - SUMMARY.append(('Glacial Isostatic Adjustment (GIA) estimates from ' - f'{GIA_Ylms_rate.citation} [{GIA_Ylms_rate.title}] have been removed')) - SUMMARY.append(('Terrestrial water storage (TWS) anomalies from {0} ' - 'have been removed (Rodell et al., 2004).').format('GLDAS NOAHv2.1')) + SUMMARY.append( + ('Sea level variations derived from {0} mission measurements').format( + MISSION + ) + ) + SUMMARY.append( + ( + 'Glacial Isostatic Adjustment (GIA) estimates from ' + f'{GIA_Ylms_rate.citation} [{GIA_Ylms_rate.title}] have been removed' + ) + ) + SUMMARY.append( + ( + 'Terrestrial water storage (TWS) anomalies from {0} ' + 'have been removed (Rodell et al., 2004).' + ).format('GLDAS NOAHv2.1') + ) kwargs['attributes']['ROOT']['summary'] = '. '.join(SUMMARY) # data project PROJECT = [] @@ -174,40 +226,56 @@ def create_public_data(base_dir, PROC, DREL, DSET, LMAX, kwargs['attributes']['ROOT']['keywords_vocabulary'] = VOCABULARY # work acknowledgements ACKNOWLEDGEMENT = [] - ACKNOWLEDGEMENT.append(('Work was supported by an appointment to the NASA ' - 'Postdoctoral Program at NASA Goddard Space Flight Center, ' - 'administered by Universities Space Research Association under ' - 'contract with NASA')) - ACKNOWLEDGEMENT.append(('GRACE is a joint mission of NASA (USA) and DLR ' - '(Germany)')) - if (DREL == 'RL06'): - ACKNOWLEDGEMENT.append('GRACE-FO is a joint mission of NASA (USA) and ' - 'GFZ (Germany)') + ACKNOWLEDGEMENT.append( + ( + 'Work was supported by an appointment to the NASA ' + 'Postdoctoral Program at NASA Goddard Space Flight Center, ' + 'administered by Universities Space Research Association under ' + 'contract with NASA' + ) + ) + ACKNOWLEDGEMENT.append( + ('GRACE is a joint mission of NASA (USA) and DLR (Germany)') + ) + if DREL == 'RL06': + ACKNOWLEDGEMENT.append( + 'GRACE-FO is a joint mission of NASA (USA) and GFZ (Germany)' + ) kwargs['attributes']['ROOT']['acknowledgement'] = '. '.join(ACKNOWLEDGEMENT) # data version PRODUCT_VERSION = f'Release-{DREL[2:]}.{DATA_VERSION}' kwargs['attributes']['ROOT']['product_version'] = PRODUCT_VERSION # product reference REFERENCE = [] - REFERENCE.append(('I. Velicogna, Y. Mohajerani, G. A, F. Landerer, ' - 'J. Mouginot, B. Noel, E. Rignot and T. Sutterley, ' - '"Continuity of ice sheet mass loss in Greenland and Antarctica ' - 'from the GRACE and GRACE Follow-On missions", ' - 'Geophysical Research Letters, 47, (2020). ' - 'https://doi.org/10.1029/2020GL087291')) - REFERENCE.append(('T. C. Sutterley, I. Velicogna, and C.-W. Hsu, ' - '"Self-Consistent Ice Mass Balance and Regional Sea Level from ' - 'Time-Variable Gravity", Earth and Space Science, 7(3), (2020). ' - 'https://doi.org/10.1029/2019EA000860')) + REFERENCE.append( + ( + 'I. Velicogna, Y. Mohajerani, G. A, F. Landerer, ' + 'J. Mouginot, B. Noel, E. Rignot and T. Sutterley, ' + '"Continuity of ice sheet mass loss in Greenland and Antarctica ' + 'from the GRACE and GRACE Follow-On missions", ' + 'Geophysical Research Letters, 47, (2020). ' + 'https://doi.org/10.1029/2020GL087291' + ) + ) + REFERENCE.append( + ( + 'T. C. Sutterley, I. Velicogna, and C.-W. Hsu, ' + '"Self-Consistent Ice Mass Balance and Regional Sea Level from ' + 'Time-Variable Gravity", Earth and Space Science, 7(3), (2020). ' + 'https://doi.org/10.1029/2019EA000860' + ) + ) # append GIA reference if GIA_Ylms_rate.reference is not None: REFERENCE.append(GIA_Ylms_rate.reference) - REFERENCE.append('M. Rodell, P. R. Houser, U. Jambor, J. Gottschalck, K. ' + REFERENCE.append( + 'M. Rodell, P. R. Houser, U. Jambor, J. Gottschalck, K. ' 'Mitchell, C.-J. Meng, K. Arsenault, B. Cosgrove, J. Radakovich, M. ' 'Bosilovich, J. K. Entin, J. P. and Walker, D. Lohmann, and D. Toll, ' '"The Global Land Data Assimilation System." Bulletin of the American ' 'Meteorological Society, 85(3), 381-394, (2004). ' - 'https://doi.org/10.1175/BAMS-85-3-381') + 'https://doi.org/10.1175/BAMS-85-3-381' + ) kwargs['attributes']['ROOT']['references'] = '\n'.join(REFERENCE) # product creators and institutions @@ -224,70 +292,146 @@ def create_public_data(base_dir, PROC, DREL, DSET, LMAX, kwargs['attributes']['ROOT']['creator_institution'] = ', '.join(INSTITUTION) # output to file - FILE = 'SLF{0}{1}{2}_L{3:d}_{4:03d}-{5:03d}.{6}'.format(dset_str, gia_str, - ocean_str, EXPANSION, months[0], months[-1], suffix[DATAFORM]) + FILE = 'SLF{0}{1}{2}_L{3:d}_{4:03d}-{5:03d}.{6}'.format( + dset_str, + gia_str, + ocean_str, + EXPANSION, + months[0], + months[-1], + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(FILE) # save as output DATAFORM - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) # only print ocean points output.fill_value = 0 output.update_mask() output.to_ascii(OUTPUT_FILE, date=True) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netCDF4 (.nc) output.to_netCDF4(OUTPUT_FILE, date=True, **kwargs) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 (.H5) output.to_HDF5(OUTPUT_FILE, date=True, **kwargs) # set the permissions mode of the output file OUTPUT_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates public data for a sea level fingerprints """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=filepath, - help='Output directory for public data files') + help='Output directory for public data files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -303,52 +447,88 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # sea level fingerprint parameters - parser.add_argument('--iteration','-I', - type=int, default=1, - help='Sea level fingerprint iteration') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--iteration', + '-I', + type=int, + default=1, + help='Sea level fingerprint iteration', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # land-sea mask for redistributing mascon mass and land water flux - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing mascon mass and land water flux') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing mascon mass and land water flux', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -375,7 +555,8 @@ def main(): LANDMASK=args.mask, DATAFORM=args.format, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -383,6 +564,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/create_public_timeseries.py b/scripts/create_public_timeseries.py index 61a46509..a0e554b9 100644 --- a/scripts/create_public_timeseries.py +++ b/scripts/create_public_timeseries.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" create_public_timeseries.py Written by Tyler Sutterley (10/2023) Creates public time series files @@ -10,6 +10,7 @@ Updated 03/2023: updated with public repository functions Written 08/2019 """ + from __future__ import print_function import sys @@ -29,6 +30,7 @@ filename = inspect.getframeinfo(inspect.currentframe()).filename filepath = pathlib.Path(filename).absolute().parent + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -38,8 +40,15 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # program module to run with specified parameters -def create_public_timeseries(base_dir, PROC, DREL, DSET, LMAX, RAD, +def create_public_timeseries( + base_dir, + PROC, + DREL, + DSET, + LMAX, + RAD, START_MON=None, END_MON=None, MISSING=None, @@ -51,11 +60,11 @@ def create_public_timeseries(base_dir, PROC, DREL, DSET, LMAX, RAD, REDISTRIBUTE_MASCONS=False, ITERATION=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # input directory setup base_dir = pathlib.Path(base_dir).expanduser().absolute() - mascon_dir = base_dir.joinpath('GRACE','mascons') + mascon_dir = base_dir.joinpath('GRACE', 'mascons') # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -79,20 +88,28 @@ def create_public_timeseries(base_dir, PROC, DREL, DSET, LMAX, RAD, # mascon distribution over the ocean ocean_str = 'OCN_' if REDISTRIBUTE_MASCONS else '' # version flags - VERSION = ['v0',''] + VERSION = ['v0', ''] DATA_VERSION = 1 # list of all months - months = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + months = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) nmon = len(months) # start and end GRACE months for correction data - OBP_START,OBP_END = (4,254) - ATM_START,ATM_END = (4,251) - GLDAS_START,GLDAS_END = (4,254) + OBP_START, OBP_END = (4, 254) + ATM_START, ATM_END = (4, 251) + GLDAS_START, GLDAS_END = (4, 254) # input directory - FLAG={1:'_SLF1',2:'_SLF2',3:'_SLF3',4:'_SLF4',5:'_SLF5',6:'_SLF6',7:'_SLF7'} + FLAG = { + 1: '_SLF1', + 2: '_SLF2', + 3: '_SLF3', + 4: '_SLF4', + 5: '_SLF5', + 6: '_SLF6', + 7: '_SLF7', + } # subdirectory and input file formats sd = 'HEX_{0}_{1}{2}_SPH_CAP_MSCNS{3}_L{4:d}_{5:03d}-{6:03d}' ff = '{0}_{1}{2}_SPH_CAP_{3}{4}L{5:d}{6}{7}.txt' @@ -100,12 +117,24 @@ def create_public_timeseries(base_dir, PROC, DREL, DSET, LMAX, RAD, # data summary MISSION = 'GRACE/GRACE-FO' SUMMARY = [] - SUMMARY.append(('Regional ice mass balance time series derived from {0} ' - 'mission measurements').format(MISSION)) - SUMMARY.append(('Glacial Isostatic Adjustment (GIA) estimates from ' - f'{GIA_Ylms_rate.citation} [{GIA_Ylms_rate.title}] have been removed')) - SUMMARY.append(('Terrestrial water storage (TWS) anomalies from {0} ' - 'have been removed (Rodell et al., 2004).').format('GLDAS NOAHv2.1')) + SUMMARY.append( + ( + 'Regional ice mass balance time series derived from {0} ' + 'mission measurements' + ).format(MISSION) + ) + SUMMARY.append( + ( + 'Glacial Isostatic Adjustment (GIA) estimates from ' + f'{GIA_Ylms_rate.citation} [{GIA_Ylms_rate.title}] have been removed' + ) + ) + SUMMARY.append( + ( + 'Terrestrial water storage (TWS) anomalies from {0} ' + 'have been removed (Rodell et al., 2004).' + ).format('GLDAS NOAHv2.1') + ) # data project PROJECT = [] PROJECT.append('NASA Gravity Recovery And Climate Experiment (GRACE)') @@ -125,38 +154,54 @@ def create_public_timeseries(base_dir, PROC, DREL, DSET, LMAX, RAD, VOCABULARY = 'NASA Global Change Master Directory (GCMD) Science Keywords' # work acknowledgements ACKNOWLEDGEMENT = [] - ACKNOWLEDGEMENT.append(('Work was supported by an appointment to the NASA ' - 'Postdoctoral Program at NASA Goddard Space Flight Center, ' - 'administered by Universities Space Research Association under ' - 'contract with NASA')) - ACKNOWLEDGEMENT.append(('GRACE is a joint mission of NASA (USA) and DLR ' - '(Germany)')) - if (DREL == 'RL06'): - ACKNOWLEDGEMENT.append('GRACE-FO is a joint mission of NASA (USA) and ' - 'GFZ (Germany)') + ACKNOWLEDGEMENT.append( + ( + 'Work was supported by an appointment to the NASA ' + 'Postdoctoral Program at NASA Goddard Space Flight Center, ' + 'administered by Universities Space Research Association under ' + 'contract with NASA' + ) + ) + ACKNOWLEDGEMENT.append( + ('GRACE is a joint mission of NASA (USA) and DLR (Germany)') + ) + if DREL == 'RL06': + ACKNOWLEDGEMENT.append( + 'GRACE-FO is a joint mission of NASA (USA) and GFZ (Germany)' + ) # data version PRODUCT_VERSION = f'Release-{DREL[2:]}.{DATA_VERSION}' # product reference REFERENCE = [] - REFERENCE.append(('I. Velicogna, Y. Mohajerani, G. A, F. Landerer, ' - 'J. Mouginot, B. Noel, E. Rignot and T. Sutterley, ' - '"Continuity of ice sheet mass loss in Greenland and Antarctica ' - 'from the GRACE and GRACE Follow-On missions", ' - 'Geophysical Research Letters, 47, (2020). ' - 'https://doi.org/10.1029/2020GL087291')) - REFERENCE.append(('T. C. Sutterley, I. Velicogna, and C.-W. Hsu, ' - '"Self-Consistent Ice Mass Balance and Regional Sea Level from ' - 'Time-Variable Gravity", Earth and Space Science, 7(3), (2020). ' - 'https://doi.org/10.1029/2019EA000860')) + REFERENCE.append( + ( + 'I. Velicogna, Y. Mohajerani, G. A, F. Landerer, ' + 'J. Mouginot, B. Noel, E. Rignot and T. Sutterley, ' + '"Continuity of ice sheet mass loss in Greenland and Antarctica ' + 'from the GRACE and GRACE Follow-On missions", ' + 'Geophysical Research Letters, 47, (2020). ' + 'https://doi.org/10.1029/2020GL087291' + ) + ) + REFERENCE.append( + ( + 'T. C. Sutterley, I. Velicogna, and C.-W. Hsu, ' + '"Self-Consistent Ice Mass Balance and Regional Sea Level from ' + 'Time-Variable Gravity", Earth and Space Science, 7(3), (2020). ' + 'https://doi.org/10.1029/2019EA000860' + ) + ) # append GIA reference if GIA_Ylms_rate.reference is not None: REFERENCE.append(GIA_Ylms_rate.reference) - REFERENCE.append('M. Rodell, P. R. Houser, U. Jambor, J. Gottschalck, K. ' + REFERENCE.append( + 'M. Rodell, P. R. Houser, U. Jambor, J. Gottschalck, K. ' 'Mitchell, C.-J. Meng, K. Arsenault, B. Cosgrove, J. Radakovich, M. ' 'Bosilovich, J. K. Entin, J. P. and Walker, D. Lohmann, and D. Toll, ' '"The Global Land Data Assimilation System." Bulletin of the American ' 'Meteorological Society, 85(3), 381-394, (2004). ' - 'https://doi.org/10.1175/BAMS-85-3-381') + 'https://doi.org/10.1175/BAMS-85-3-381' + ) # product creators and institutions CREATORS = 'Tyler C. Sutterley, Isabella Velicogna, and Chia-Wei Hsu' EMAILS = 'tsutterl@uw.edu, isabella@uci.edu, and chiaweih@email.arizona.edu' @@ -166,44 +211,72 @@ def create_public_timeseries(base_dir, PROC, DREL, DSET, LMAX, RAD, INSTITUTION.append('University of California, Irvine') # open output zipfile containing regional files - ZFILE = '{0}_SPH_CAP_{1}_L{2:d}{3}.zip'.format(gia_str,'RAD1.5',LMAX,gw_str) + ZFILE = '{0}_SPH_CAP_{1}_L{2:d}{3}.zip'.format( + gia_str, 'RAD1.5', LMAX, gw_str + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(ZFILE) zp = zipfile.ZipFile(OUTPUT_FILE, mode='w') # run for specific regions regions = [] - AIS_regions = ['AIS','WAIS','EAIS','APIS'] - GIS_regions = ['GIS','NW','NN','NE','SW','SE'] + AIS_regions = ['AIS', 'WAIS', 'EAIS', 'APIS'] + GIS_regions = ['GIS', 'NW', 'NN', 'NE', 'SW', 'SE'] # GIC_regions = ['CBI','CDE','ICL','SVB','FJL','SZEM','NZEM','ALK'] - HEM = ['S']*len(AIS_regions) + ['N']*len(GIS_regions) #+ ['N']*len(GIC_regions) - remove = ['AIS']*len(AIS_regions) + ['ARC']*len(GIS_regions) #+ ['ARC']*len(GIC_regions) + HEM = ['S'] * len(AIS_regions) + ['N'] * len( + GIS_regions + ) # + ['N']*len(GIC_regions) + remove = ['AIS'] * len(AIS_regions) + ['ARC'] * len( + GIS_regions + ) # + ['ARC']*len(GIC_regions) regions.extend(AIS_regions) regions.extend(GIS_regions) # regions.extend(GIC_regions) # for each region - for h,reg,rem in zip(HEM,regions,remove): + for h, reg, rem in zip(HEM, regions, remove): # read ocean bottom pressure leakage file - subdir = sd.format('AOD1B',DREL,'','',LMAX,OBP_START,OBP_END) - OBP_file = ff.format('ECCO-GAD_OBP_Residuals',reg,'','',ocean_str,LMAX,gw_str,ds_str) - OBP_input = np.loadtxt(mascon_dir.joinpath(subdir,OBP_file))[:nmon,:] + subdir = sd.format('AOD1B', DREL, '', '', LMAX, OBP_START, OBP_END) + OBP_file = ff.format( + 'ECCO-GAD_OBP_Residuals', + reg, + '', + '', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + OBP_input = np.loadtxt(mascon_dir.joinpath(subdir, OBP_file))[:nmon, :] # read atmospheric pressure leakage file - subdir = sd.format('AOD1B',DREL,'','',LMAX,ATM_START,ATM_END) + subdir = sd.format('AOD1B', DREL, '', '', LMAX, ATM_START, ATM_END) # ATM_file = ff.format('ATM-GAA_Residuals',reg,'_3D','',ocean_str,LMAX,gw_str) # ATM_file = ff.format('ATM_Differences',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) - ATM_file = ff.format('ATM_Differences',reg,'','',ocean_str,LMAX,gw_str,ds_str) - ATM_input = np.loadtxt(mascon_dir.joinpath(subdir,ATM_file))[:nmon,:] + ATM_file = ff.format( + 'ATM_Differences', reg, '', '', ocean_str, LMAX, gw_str, ds_str + ) + ATM_input = np.loadtxt(mascon_dir.joinpath(subdir, ATM_file))[:nmon, :] # read GLDAS terrestrial water RMS file - subdir = sd.format('GLDAS','TWC_V2.1_RMS','','',LMAX,GLDAS_START,GLDAS_END) - TWC_file = ff.format('GLDAS_TWC_RMS',reg,'','RAD1.5_','',LMAX,gw_str,ds_str) - TWC_input = np.loadtxt(base_dir.joinpath('GLDAS',subdir,TWC_file))[:nmon,:] - isvalid, = np.nonzero(np.isfinite(TWC_input[:,2]) & - (TWC_input[:,0] >= START_MON) & (TWC_input[:,0] <= END_MON)) - TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid,2]**2)/len(isvalid)) + subdir = sd.format( + 'GLDAS', 'TWC_V2.1_RMS', '', '', LMAX, GLDAS_START, GLDAS_END + ) + TWC_file = ff.format( + 'GLDAS_TWC_RMS', reg, '', 'RAD1.5_', '', LMAX, gw_str, ds_str + ) + TWC_input = np.loadtxt(base_dir.joinpath('GLDAS', subdir, TWC_file))[ + :nmon, : + ] + (isvalid,) = np.nonzero( + np.isfinite(TWC_input[:, 2]) + & (TWC_input[:, 0] >= START_MON) + & (TWC_input[:, 0] <= END_MON) + ) + TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid, 2] ** 2) / len(isvalid)) # input estimated SLF monte carlo variance file and calculate RMS - subdir = sd.format(PROC,DREL,'','_MC',LMAX,START_MON,END_MON) - SLF_file = ff.format('MC',reg,'','RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - SLF_input = np.loadtxt(mascon_dir.joinpath(subdir,SLF_file)) - SLF_RMS = np.sqrt(np.sum(SLF_input[:,1]**2)/len(SLF_input)) + subdir = sd.format(PROC, DREL, '', '_MC', LMAX, START_MON, END_MON) + SLF_file = ff.format( + 'MC', reg, '', 'RAD1.5_', ocean_str, LMAX, gw_str, ds_str + ) + SLF_input = np.loadtxt(mascon_dir.joinpath(subdir, SLF_file)) + SLF_RMS = np.sqrt(np.sum(SLF_input[:, 1] ** 2) / len(SLF_input)) # calculate mean of RMS over multiple reanalyses OBP_RMS = 0.0 ATM_RMS = 0.0 @@ -211,32 +284,49 @@ def create_public_timeseries(base_dir, PROC, DREL, DSET, LMAX, RAD, # ivalid, = np.nonzero(np.isfinite(OBP_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(OBP_input[:,j+3])) # OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(OBP_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(OBP_input[:,2])) - OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(OBP_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(OBP_input[:, 2])) + OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid, 2] ** 2) / valid_count) # for j in range(4): # ivalid, = np.nonzero(np.isfinite(ATM_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(ATM_input[:,j+3])) # ATM_RMS += np.sqrt(np.sum(OBP_input[ATM_input,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(ATM_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(ATM_input[:,2])) - ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(ATM_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(ATM_input[:, 2])) + ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid, 2] ** 2) / valid_count) # # divide by the number of reanalyses # OBP_RMS /= 2.0 # ATM_RMS /= 4.0 # input mascon file for GIA correction - subdir = sd.format(PROC,DREL,VERSION[DATA_VERSION], - FLAG[ITERATION],LMAX,START_MON,END_MON) - input_file = ff.format(gia_str,reg,'',atm_str,ocean_str,LMAX,gw_str,'') - dinput = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) - mon = dinput[:nmon,0].astype(np.int64) - tdec = dinput[:nmon,1] - mass = dinput[:nmon,2] - satellite_error = dinput[:nmon,3]**2 + subdir = sd.format( + PROC, + DREL, + VERSION[DATA_VERSION], + FLAG[ITERATION], + LMAX, + START_MON, + END_MON, + ) + input_file = ff.format( + gia_str, reg, '', atm_str, ocean_str, LMAX, gw_str, '' + ) + dinput = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) + mon = dinput[:nmon, 0].astype(np.int64) + tdec = dinput[:nmon, 1] + mass = dinput[:nmon, 2] + satellite_error = dinput[:nmon, 3] ** 2 # calculate total combined error - grace_error = np.sqrt(np.sum(satellite_error + SLF_RMS**2 + - OBP_RMS**2 + ATM_RMS**2 + TWC_RMS**2)/nmon) + grace_error = np.sqrt( + np.sum( + satellite_error + + SLF_RMS**2 + + OBP_RMS**2 + + ATM_RMS**2 + + TWC_RMS**2 + ) + / nmon + ) # open output file as in-memory object fid = io.StringIO() @@ -245,16 +335,21 @@ def create_public_timeseries(base_dir, PROC, DREL, DSET, LMAX, RAD, fid.write('{0}:\n'.format('header')) # data dimensions fid.write(' {0}:\n'.format('dimensions')) - fid.write(' {0:22}: {1:d}\n'.format('time',nmon)) + fid.write(' {0:22}: {1:d}\n'.format('time', nmon)) fid.write('\n') fid.write(' {0}:\n'.format('global_attributes')) - fid.write(' {0:22}: {1}\n'.format('summary','. '.join(SUMMARY))) - fid.write(' {0:22}: {1}\n'.format('project',', '.join(PROJECT))) - fid.write(' {0:22}: {1}\n'.format('keywords',', '.join(KEYWORDS))) - fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary',VOCABULARY)) - fid.write(' {0:22}: {1}\n'.format('acknowledgement', - '. '.join(ACKNOWLEDGEMENT))) - fid.write(' {0:22}: {1}\n'.format('product_version',PRODUCT_VERSION)) + fid.write(' {0:22}: {1}\n'.format('summary', '. '.join(SUMMARY))) + fid.write(' {0:22}: {1}\n'.format('project', ', '.join(PROJECT))) + fid.write(' {0:22}: {1}\n'.format('keywords', ', '.join(KEYWORDS))) + fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary', VOCABULARY)) + fid.write( + ' {0:22}: {1}\n'.format( + 'acknowledgement', '. '.join(ACKNOWLEDGEMENT) + ) + ) + fid.write( + ' {0:22}: {1}\n'.format('product_version', PRODUCT_VERSION) + ) fid.write(' {0:22}:\n'.format('references')) for ref in REFERENCE: fid.write(' - {0}\n'.format(ref)) @@ -262,21 +357,31 @@ def create_public_timeseries(base_dir, PROC, DREL, DSET, LMAX, RAD, fid.write(' {0:22}: {1}\n'.format('creator_email', EMAILS)) fid.write(' {0:22}: {1}\n'.format('creator_url', URL)) fid.write(' {0:22}: {1}\n'.format('creator_type', 'group')) - fid.write(' {0:22}: {1}\n'.format('creator_institution',', '.join(INSTITUTION))) + fid.write( + ' {0:22}: {1}\n'.format( + 'creator_institution', ', '.join(INSTITUTION) + ) + ) # date range and date created - calendar_year = np.floor((mon-1)/12 + 2002.0) - calendar_month = ((mon-1) % 12) + 1 - start_time = '{0:4.0f}-{1:02.0f}'.format(calendar_year[0],calendar_month[0]) + calendar_year = np.floor((mon - 1) / 12 + 2002.0) + calendar_month = ((mon - 1) % 12) + 1 + start_time = '{0:4.0f}-{1:02.0f}'.format( + calendar_year[0], calendar_month[0] + ) fid.write(' {0:22}: {1}\n'.format('time_coverage_start', start_time)) - end_time = '{0:4.0f}-{1:02.0f}'.format(calendar_year[-1],calendar_month[-1]) + end_time = '{0:4.0f}-{1:02.0f}'.format( + calendar_year[-1], calendar_month[-1] + ) fid.write(' {0:22}: {1}\n'.format('time_coverage_end', end_time)) - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) fid.write(' {0:22}: {1}\n'.format('date_created', today)) fid.write('\n') # non-standard attributes fid.write(' {0}:\n'.format('non-standard_attributes')) # data format - fid.write(' {0:22}: {1}\n'.format('formatting_string','(i4,3f11.4)')) + fid.write( + ' {0:22}: {1}\n'.format('formatting_string', '(i4,3f11.4)') + ) fid.write('\n') # variables fid.write(' {0}:\n'.format('variables')) @@ -312,72 +417,155 @@ def create_public_timeseries(base_dir, PROC, DREL, DSET, LMAX, RAD, fid.write('\n\n# End of YAML header\n') # add data for m in range(nmon): - args = (mon[m],tdec[m],mass[m],grace_error) + args = (mon[m], tdec[m], mass[m], grace_error) fid.write('{0:4d}{1:11.4f}{2:11.4f}{3:11.4f}\n'.format(*args)) # rewind in-memory file object fid.seek(0) # write in-memory object to zip - zp.writestr(ff.format(gia_str,reg,'','','',LMAX,gw_str,''), fid.read()) + zp.writestr( + ff.format(gia_str, reg, '', '', '', LMAX, gw_str, ''), fid.read() + ) # change output file permissions mode to MODE OUTPUT_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates public data for a mascon time series """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=filepath, - help='Output directory for public data files') + help='Output directory for public data files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -393,40 +581,64 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # sea level fingerprint parameters - parser.add_argument('--iteration','-I', - type=int, default=1, - help='Sea level fingerprint iteration') + parser.add_argument( + '--iteration', + '-I', + type=int, + default=1, + help='Sea level fingerprint iteration', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -454,7 +666,8 @@ def main(): REDISTRIBUTE_MASCONS=args.redistribute_mascons, ITERATION=args.iteration, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -462,6 +675,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/gia_covariance_errors_caron.py b/scripts/gia_covariance_errors_caron.py index 3e5a3d03..b37c54a7 100644 --- a/scripts/gia_covariance_errors_caron.py +++ b/scripts/gia_covariance_errors_caron.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gia_covariance_errors_caron.py Written by Yara Mohajerani (05/2019) Updated by Tyler Sutterley (05/2023) @@ -98,6 +98,7 @@ add up kernel for given cap numbers and output 1 error Written 05/2019 """ + from __future__ import print_function, division import sys @@ -111,6 +112,7 @@ import scipy.linalg import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -120,6 +122,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: read GIA covariance matrix file and flatten to mascon form def read_covariance_file(infile, LMAX, LMIN=None, MMAX=None): # file parser for reading index files @@ -133,46 +136,95 @@ def read_covariance_file(infile, LMAX, LMIN=None, MMAX=None): contents = [i for i in fid.read().splitlines() if parser.match(i)] # Calculating the number of cos and sin harmonics between LMIN and LMAX # taking into account MMAX (if MMAX == LMAX then LMAX-MMAX=0) - n_clm = (LMAX**2 - LMIN**2 + 3*LMAX - (LMAX-MMAX)**2 - (LMAX-MMAX))//2 + 1 - n_harm = int(LMAX**2 - LMIN**2 + 2*LMAX - (LMAX-MMAX)**2 - (LMAX-MMAX)) + 1 + n_clm = ( + LMAX**2 - LMIN**2 + 3 * LMAX - (LMAX - MMAX) ** 2 - (LMAX - MMAX) + ) // 2 + 1 + n_harm = ( + int(LMAX**2 - LMIN**2 + 2 * LMAX - (LMAX - MMAX) ** 2 - (LMAX - MMAX)) + + 1 + ) # flattened covariance matrix - cov = np.zeros((n_harm,n_harm)) + cov = np.zeros((n_harm, n_harm)) # for each line in the file for line in contents: # note clm orders are +ve and slm orders are -ve - l1,m1,l2,m2,Ylms = line.split() - l1,m1,l2,m2 = np.array([l1,m1,l2,m2],dtype=np.int64) + l1, m1, l2, m2, Ylms = line.split() + l1, m1, l2, m2 = np.array([l1, m1, l2, m2], dtype=np.int64) # indice for filling flattened covariance matrix for lm harmonics if (m1 >= 0) and (l1 >= LMIN) and (l1 <= LMAX) and (m1 <= MMAX): # cosine harmonics - i1 = m1 + ((l1-1)**2 - LMIN**2 + 3*(l1-1) - \ - np.max([l1-MMAX-1,0])**2 - np.max([l1-MMAX-1,0]))//2 + 1 + i1 = ( + m1 + + ( + (l1 - 1) ** 2 + - LMIN**2 + + 3 * (l1 - 1) + - np.max([l1 - MMAX - 1, 0]) ** 2 + - np.max([l1 - MMAX - 1, 0]) + ) + // 2 + + 1 + ) elif (l1 >= LMIN) and (l1 <= LMAX) and (np.abs(m1) <= MMAX): # sine harmonics - i1 = n_clm + np.abs(m1) + ((l1-1)**2 - LMIN**2 + (l1-1) - \ - np.max([l1-MMAX-1,0])**2 - np.max([l1-MMAX-1,0]))//2 + i1 = ( + n_clm + + np.abs(m1) + + ( + (l1 - 1) ** 2 + - LMIN**2 + + (l1 - 1) + - np.max([l1 - MMAX - 1, 0]) ** 2 + - np.max([l1 - MMAX - 1, 0]) + ) + // 2 + ) else: i1 = None # indice for filling flattened covariance matrix for pq harmonics if (m2 >= 0) and (l2 >= LMIN) and (l2 <= LMAX) and (m2 <= MMAX): # cosine harmonics - i2 = m2 + ((l2-1)**2 - LMIN**2 + 3*(l2-1) - \ - np.max([l2-MMAX-1,0])**2 - np.max([l2-MMAX-1,0]))//2 + 1 + i2 = ( + m2 + + ( + (l2 - 1) ** 2 + - LMIN**2 + + 3 * (l2 - 1) + - np.max([l2 - MMAX - 1, 0]) ** 2 + - np.max([l2 - MMAX - 1, 0]) + ) + // 2 + + 1 + ) elif (l2 >= LMIN) and (l2 <= LMAX) and (np.abs(m2) <= MMAX): # sine harmonics - i2 = n_clm + np.abs(m2) + ((l2-1)**2 - LMIN**2 + (l2-1) - \ - np.max([l2-MMAX-1,0])**2 - np.max([l2-MMAX-1,0]))//2 + i2 = ( + n_clm + + np.abs(m2) + + ( + (l2 - 1) ** 2 + - LMIN**2 + + (l2 - 1) + - np.max([l2 - MMAX - 1, 0]) ** 2 + - np.max([l2 - MMAX - 1, 0]) + ) + // 2 + ) else: i2 = None # add data to flattened covariance matrix - if (i1 and i2): - cov[i1,i2] = np.float64(Ylms) + if i1 and i2: + cov[i1, i2] = np.float64(Ylms) # free up memory contents = None return cov + # calculate GIA error for given mascon configuration -def gia_covariance_errors_caron(base_dir, LMAX, RAD, +def gia_covariance_errors_caron( + base_dir, + LMAX, + RAD, LMIN=None, MMAX=None, DESTRIPE=False, @@ -185,8 +237,8 @@ def gia_covariance_errors_caron(base_dir, LMAX, RAD, SOLVER=None, LANDMASK=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # input directory setup base_dir = pathlib.Path(base_dir).expanduser().absolute() # output directory setup @@ -202,8 +254,9 @@ def gia_covariance_errors_caron(base_dir, LMAX, RAD, parser = re.compile(r'^(?!\#|\%|$)', re.VERBOSE) # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) @@ -213,12 +266,12 @@ def gia_covariance_errors_caron(base_dir, LMAX, RAD, rad_e = factors.rad_e # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) gw_str = '' # output string for both LMAX==MMAX and LMAX != MMAX cases @@ -228,8 +281,7 @@ def gia_covariance_errors_caron(base_dir, LMAX, RAD, # Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_MASCONS: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, - LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) ocean_str = '_OCN' else: # not distributing uniformly over ocean @@ -249,22 +301,23 @@ def gia_covariance_errors_caron(base_dir, LMAX, RAD, mascon_list = [] for k in range(n_mas): # read mascon spherical harmonics - Ylms = gravtk.harmonics().from_file(mascon_files[k], - format=MASCON_FORMAT, date=False) + Ylms = gravtk.harmonics().from_file( + mascon_files[k], format=MASCON_FORMAT, date=False + ) # Calculating the total mass of each mascon (1 cmwe uniform) - area_tot[k] = 4.0*np.pi*(rad_e**3)*rho_e*Ylms.clm[0,0]/3.0 + 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 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] + 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 + 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] + 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)) # stem is the mascon file without directory or suffix @@ -279,24 +332,27 @@ def gia_covariance_errors_caron(base_dir, LMAX, RAD, # 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)) + n_harm = np.int64( + LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX) ** 2 - (LMAX - MMAX) + ) # read Caron et al. (2018) GIA covariance matrix files # list containing files to read based on spherical harmonic degree range gia_files = [] gia_files.append('covStokes_GIA_deg_2_to_60.txt') - if (LMAX > 60): + if LMAX > 60: gia_files.append('covStokes_GIA_deg_61_to_75.txt') - if (LMAX > 75): + if LMAX > 75: gia_files.append('covStokes_GIA_deg_76_to_83.txt') - if (LMAX > 83): + if LMAX > 83: gia_files.append('covStokes_GIA_deg_84_to_89.txt') # flattened combined covariance matrix gia_cov = np.zeros((n_harm, n_harm)) # for each GIA covariance file for fi in gia_files: - gia_cov[:,:] += read_covariance_file(base_dir.joinpath(fi), LMAX, - LMIN=LMIN, MMAX=MMAX) + gia_cov[:, :] += read_covariance_file( + base_dir.joinpath(fi), LMAX, LMIN=LMIN, MMAX=MMAX + ) # GIA title string for covariance-derived errors gia_str = '_Caron_Error' @@ -321,59 +377,60 @@ def gia_covariance_errors_caron(base_dir, LMAX, RAD, # 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 = rho_e * rad_e / 3.0 # Switching between Cosine and Sine Stokes - for cs,csharm in enumerate(['clm','slm']): + 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 l in range(LMIN, LMAX + 1): # for each spherical harmonic order # Sine Stokes for (m=0) = 0 - mn = np.min([MMAX,l]) + mn = np.min([MMAX, l]) # +1 to include l or MMAX (whichever is smaller) - for m in range(cs,mn+1): + for m in range(cs, mn + 1): # Mascon Spherical Harmonics - M_lm[ii,:] = np.copy(mascon_harm[l,m,:]) + 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 + LOVE.kl[l]) + fact[ii] = (2.0 * l + 1.0) / (1.0 + LOVE.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): + 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 - elif (FIT_METHOD == 2): + MA_lm[i, :] = M_lm[i, :] * wt_lm[i] * fact[i] + fit_factor = wt_lm * fact + elif FIT_METHOD == 2: # 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)) + 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] + kern_i[i] = 1.0 * fit_factor[i] # spherical harmonics solution for the # mascon sensitivity kernels - if (SOLVER == 'inv'): + if SOLVER == 'inv': kern_lm = np.dot(np.linalg.inv(MA_lm), kern_i) - elif (SOLVER == 'lstsq'): + elif SOLVER == 'lstsq': kern_lm = np.linalg.lstsq(MA_lm, kern_i, rcond=-1)[0] elif SOLVER in ('gelsd', 'gelsy', 'gelss'): - kern_lm, res, rnk, s = scipy.linalg.lstsq(MA_lm, kern_i, - lapack_driver=SOLVER) + kern_lm, res, rnk, s = scipy.linalg.lstsq( + MA_lm, kern_i, lapack_driver=SOLVER + ) # calculate the sensitivity kernel for each mascon for k in range(n_mas): - A_lm[i,k] = kern_lm[k]*area_tot[k] + A_lm[i, k] = kern_lm[k] * area_tot[k] # calculate total error for each kernel # for each spherical harmonic lm (order is [C00...Clm,S11...Slm]) @@ -381,17 +438,24 @@ def gia_covariance_errors_caron(base_dir, LMAX, RAD, # for each spherical harmonic pq (order is [C00...Cpq,S11...Spq]) for j in range(n_harm): # add to total mascon errors - M_err[:] += (A_lm[i,:]*A_lm[j,:]*gia_cov[i,j]) + M_err[:] += A_lm[i, :] * A_lm[j, :] * gia_cov[i, j] # for each mascon for k in range(n_mas): # output filename format (for both LMAX==MMAX and LMAX != MMAX cases): # mascon name, GIA model, LMAX, (MMAX,), Gaussian smoothing, filter - fargs = (mascon_name[k],gia_str.upper(),ocean_str,LMAX,order_str,gw_str) + fargs = ( + mascon_name[k], + gia_str.upper(), + ocean_str, + LMAX, + order_str, + gw_str, + ) file_format = '{0}{1}{2}_L{3:d}{4}{5}.txt' output_file = OUTPUT_DIRECTORY.joinpath(file_format.format(*fargs)) # take sqrt and convert from from g to gigatonnes - args = (np.sqrt(M_err[k])/1e15, area_tot[k]/1e10) + args = (np.sqrt(M_err[k]) / 1e15, area_tot[k] / 1e10) with output_file.open(mode='w', encoding='utf8') as fid1: print('{0:16.10f} {1:16.10f}'.format(*args), file=fid1) # change the permissions mode @@ -402,98 +466,160 @@ def gia_covariance_errors_caron(base_dir, LMAX, RAD, # return the list of output files return output_files + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculate GIA errors based on full covariance matrix """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # minimum spherical harmonic degree - parser.add_argument('--lmin', - type=int, default=1, - help='Minimum spherical harmonic degree') + parser.add_argument( + '--lmin', type=int, default=1, help='Minimum spherical harmonic degree' + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # mascon index file and parameters - parser.add_argument('--mascon-file', + parser.add_argument( + '--mascon-file', type=pathlib.Path, - help='Index file of mascons spherical harmonics') - parser.add_argument('--mascon-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format for mascon files') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + help='Index file of mascons spherical harmonics', + ) + parser.add_argument( + '--mascon-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format for mascon files', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # 1: mass coefficients # 2: geoid coefficients - parser.add_argument('--fit-method', - type=int, default=1, choices=(1,2), - help='Method for fitting sensitivity kernel to harmonics') + parser.add_argument( + '--fit-method', + type=int, + default=1, + choices=(1, 2), + help='Method for fitting sensitivity kernel to harmonics', + ) # least squares solver - choices = ('inv','lstsq','gelsd', 'gelsy', 'gelss') - parser.add_argument('--solver','-s', - type=str, default='lstsq', choices=choices, - help='Least squares solver for sensitivity kernel solutions') + choices = ('inv', 'lstsq', 'gelsd', 'gelsy', 'gelss') + parser.add_argument( + '--solver', + '-s', + type=str, + default='lstsq', + choices=choices, + help='Least squares solver for sensitivity kernel solutions', + ) # land-sea mask for redistributing mascon mass and land water flux - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing mascon mass') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing mascon mass', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -519,7 +645,8 @@ def main(): SOLVER=args.solver, LANDMASK=args.mask, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -527,6 +654,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/grace_mean_harmonics.py b/scripts/grace_mean_harmonics.py index 98fba868..770d9498 100644 --- a/scripts/grace_mean_harmonics.py +++ b/scripts/grace_mean_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_mean_harmonics.py Written by Tyler Sutterley (05/2023) @@ -110,6 +110,7 @@ with the multiprocessing module Written 05/2014 """ + from __future__ import print_function import sys @@ -123,6 +124,7 @@ import collections import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -132,9 +134,15 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: import GRACE/GRACE-FO files for a given months range # calculate the mean of the spherical harmonics and output to file -def grace_mean_harmonics(base_dir, PROC, DREL, DSET, LMAX, +def grace_mean_harmonics( + base_dir, + PROC, + DREL, + DSET, + LMAX, START=None, END=None, MISSING=None, @@ -153,8 +161,8 @@ def grace_mean_harmonics(base_dir, PROC, DREL, DSET, LMAX, MEAN_FILE=None, MEANFORM=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # input directory setup base_dir = pathlib.Path(base_dir).expanduser().absolute() @@ -173,17 +181,34 @@ def grace_mean_harmonics(base_dir, PROC, DREL, DSET, LMAX, attributes['max_order'] = MMAX # data formats for output: ascii, netCDF4, HDF5, gfc - suffix = dict(ascii='txt',netCDF4='nc',HDF5='H5',gfc='gfc')[MEANFORM] + suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5', gfc='gfc')[MEANFORM] # 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 Drift and Atmospheric Jumps if specified - input_Ylms = gravtk.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_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + input_Ylms = gravtk.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_C40=SLR_C40, + SLR_C50=SLR_C50, + DEG1_FILE=DEG1_FILE, + MODEL_DEG1=MODEL_DEG1, + ATM=ATM, + POLE_TIDE=POLE_TIDE, + ) grace_Ylms = gravtk.harmonics().from_dict(input_Ylms) # descriptor string for processing parameters grace_str = input_Ylms['title'] @@ -194,13 +219,25 @@ def grace_mean_harmonics(base_dir, PROC, DREL, DSET, LMAX, # number of months nt = grace_Ylms.shape[-1] # calculate RMS of harmonic errors - mean_Ylms.eclm = np.sqrt(np.sum(input_Ylms['eclm']**2,axis=2)/nt) - mean_Ylms.eslm = np.sqrt(np.sum(input_Ylms['eslm']**2,axis=2)/nt) + mean_Ylms.eclm = np.sqrt(np.sum(input_Ylms['eclm'] ** 2, axis=2) / nt) + mean_Ylms.eslm = np.sqrt(np.sum(input_Ylms['eslm'] ** 2, axis=2) / nt) # default output filename if not entering via parameter file if not MEAN_FILE: - DIRECTORY = pathlib.Path(input_Ylms['directory']).expanduser().absolute() - args = (PROC,DREL,DSET,grace_str,LMAX,order_str,START,END,suffix) + DIRECTORY = ( + pathlib.Path(input_Ylms['directory']).expanduser().absolute() + ) + args = ( + PROC, + DREL, + DSET, + grace_str, + LMAX, + order_str, + START, + END, + suffix, + ) file_format = '{0}_{1}_{2}_MEAN_CLM{3}_L{4:d}{5}_{6:03d}-{7:03d}.{8}' MEAN_FILE = DIRECTORY.joinpath(file_format.format(*args)) else: @@ -210,33 +247,35 @@ def grace_mean_harmonics(base_dir, PROC, DREL, DSET, LMAX, DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) # output spherical harmonics for the static field - if (MEANFORM == 'gfc'): + if MEANFORM == 'gfc': # output mean field to gfc format mean_Ylms.attributes['ROOT'] = attributes mean_Ylms.to_gfc(MEAN_FILE, verbose=VERBOSE) else: # add attributes from input GRACE fields attributes.update(input_Ylms.get('attributes')) - attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # output mean field to specified file format mean_Ylms.attributes['ROOT'] = attributes - mean_Ylms.to_file(MEAN_FILE, format=MEANFORM, - verbose=VERBOSE) + mean_Ylms.to_file(MEAN_FILE, format=MEANFORM, verbose=VERBOSE) # change the permissions mode MEAN_FILE.chmod(mode=MODE) # return the output file return MEAN_FILE + # PURPOSE: additional routines for the harmonics module class mean(gravtk.harmonics): def __init__(self, **kwargs): super().__init__(**kwargs) - self.center=None - self.release='RLxx' - self.product=None - self.eclm=None - self.eslm=None + self.center = None + self.release = 'RLxx' + self.product = None + self.eclm = None + self.eslm = None def from_harmonics(self, temp): """ @@ -244,8 +283,16 @@ def from_harmonics(self, temp): """ self = mean(lmax=temp.lmax, mmax=temp.mmax) # try to assign variables to self - for key in ['clm','slm','eclm','eslm','filename', - 'center','release','product']: + for key in [ + 'clm', + 'slm', + 'eclm', + 'eslm', + 'filename', + 'center', + 'release', + 'product', + ]: try: val = getattr(temp, key) setattr(self, key, np.copy(val)) @@ -265,20 +312,28 @@ def to_gfc(self, filename, **kwargs): """ self.filename = pathlib.Path(filename).expanduser().absolute() # set default verbosity - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) logging.info(str(self.filename)) # open the output file fid = self.filename.open(mode='w', encoding='utf8') # print the header informat self.print_header(fid) # output file format - file_format = ('{0:3} {1:4d} {2:4d} {3:+18.12E} {4:+18.12E} ' - '{5:11.5E} {6:11.5E}') + file_format = ( + '{0:3} {1:4d} {2:4d} {3:+18.12E} {4:+18.12E} {5:11.5E} {6:11.5E}' + ) # write to file for each spherical harmonic degree and order - for m in range(0, self.mmax+1): - for l in range(m, self.lmax+1): - args = ('gfc', l, m, self.clm[l,m], self.slm[l,m], - self.eclm[l,m], self.eslm[l,m]) + for m in range(0, self.mmax + 1): + for l in range(m, self.lmax + 1): + args = ( + 'gfc', + l, + m, + self.clm[l, m], + self.slm[l, m], + self.eclm[l, m], + self.eslm[l, m], + ) print(file_format.format(*args), file=fid) # close the output file fid.close() @@ -286,22 +341,26 @@ def to_gfc(self, filename, **kwargs): # PURPOSE: print gfc header to top of file def print_header(self, fid): # print header - fid.write('{0} {1}\n'.format('begin_of_head',73*'=')) - for att_name,att_val in self.attributes['ROOT'].items(): + fid.write('{0} {1}\n'.format('begin_of_head', 73 * '=')) + for att_name, att_val in self.attributes['ROOT'].items(): fid.write('{0:30}{1}\n'.format(att_name, att_val)) - fid.write('{0:30}{1:+16.10E}\n'.format('earth_gravity_constant', - 3.986004415E+14)) - fid.write('{0:30}{1:+16.10E}\n'.format('radius',6.378136300E+06)) - fid.write('{0:30}{1}\n'.format('errors','uncalibrated')) - fid.write('{0:30}{1}\n'.format('norm','fully_normalized')) - args = ('key','L','M','C','S','sigma C','sigma S') + fid.write( + '{0:30}{1:+16.10E}\n'.format( + 'earth_gravity_constant', 3.986004415e14 + ) + ) + fid.write('{0:30}{1:+16.10E}\n'.format('radius', 6.378136300e06)) + fid.write('{0:30}{1}\n'.format('errors', 'uncalibrated')) + fid.write('{0:30}{1}\n'.format('norm', 'fully_normalized')) + args = ('key', 'L', 'M', 'C', 'S', 'sigma C', 'sigma S') fid.write('\n{0:7}{1:5}{2:10}{3:20}{4:15}{5:13}{6:7}\n'.format(*args)) - fid.write('{0} {1}\n'.format('end_of_head',75*'=')) + fid.write('{0} {1}\n'.format('end_of_head', 75 * '=')) + # PURPOSE: print a file log for the GRACE/GRACE-FO mean program def output_log_file(input_arguments, output_file): # format: GRACE_mean_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_mean_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.directory) @@ -317,10 +376,11 @@ def output_log_file(input_arguments, output_file): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE/GRACE-FO mean program def output_error_log_file(input_arguments): # format: GRACE_mean_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_mean_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.directory) @@ -336,60 +396,136 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the temporal mean of the GRACE/GRACE-FO spherical harmonics """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Data processing center or satellite mission - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # correct for pole tide drift follow Wahr et al. (2015) - parser.add_argument('--pole-tide', - default=False, action='store_true', - help='Correct for pole tide drift') + parser.add_argument( + '--pole-tide', + default=False, + action='store_true', + help='Correct for pole tide drift', + ) # Update Degree 1 coefficients with SLR or derived values # Tellus: GRACE/GRACE-FO TN-13 from PO.DAAC # https://grace.jpl.nasa.gov/data/get-data/geocenter/ @@ -401,65 +537,113 @@ def arguments(): # https://doi.org/10.1029/2007JB005338 # GFZ: GRACE/GRACE-FO coefficients from GFZ GravIS # http://gravis.gfz-potsdam.de/corrections - parser.add_argument('--geocenter', - metavar='DEG1', type=str, - choices=['Tellus','SLR','SLF','UCI','Swenson','GFZ'], - help='Update Degree 1 coefficients with SLR or derived values') - parser.add_argument('--geocenter-file', + parser.add_argument( + '--geocenter', + metavar='DEG1', + type=str, + choices=['Tellus', 'SLR', 'SLF', 'UCI', 'Swenson', 'GFZ'], + help='Update Degree 1 coefficients with SLR or derived values', + ) + parser.add_argument( + '--geocenter-file', type=pathlib.Path, - help='Specific geocenter file if not default') - parser.add_argument('--interpolate-geocenter', - default=False, action='store_true', - help='Least-squares model missing Degree 1 coefficients') + help='Specific geocenter file if not default', + ) + parser.add_argument( + '--interpolate-geocenter', + default=False, + action='store_true', + help='Least-squares model missing Degree 1 coefficients', + ) # replace low degree harmonics with values from Satellite Laser Ranging - parser.add_argument('--slr-c20', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C20 coefficients with SLR values') - parser.add_argument('--slr-21', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C21 and S21 coefficients with SLR values') - parser.add_argument('--slr-22', - type=str, default=None, choices=['CSR','GSFC'], - help='Replace C22 and S22 coefficients with SLR values') - parser.add_argument('--slr-c30', - type=str, default=None, choices=['CSR','GFZ','GSFC','LARES'], - help='Replace C30 coefficients with SLR values') - parser.add_argument('--slr-c40', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C40 coefficients with SLR values') - parser.add_argument('--slr-c50', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C50 coefficients with SLR values') + parser.add_argument( + '--slr-c20', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C20 coefficients with SLR values', + ) + parser.add_argument( + '--slr-21', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C21 and S21 coefficients with SLR values', + ) + parser.add_argument( + '--slr-22', + type=str, + default=None, + choices=['CSR', 'GSFC'], + help='Replace C22 and S22 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c30', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC', 'LARES'], + help='Replace C30 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c40', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C40 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c50', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C50 coefficients with SLR values', + ) # mean file to remove - parser.add_argument('--mean-file', - type=pathlib.Path, - help='Output GRACE/GRACE-FO mean file') + parser.add_argument( + '--mean-file', type=pathlib.Path, help='Output GRACE/GRACE-FO mean file' + ) # input data format (ascii, netCDF4, HDF5, gfc) - parser.add_argument('--mean-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5','gfc'], - help='Output data format for GRACE/GRACE-FO mean file') + parser.add_argument( + '--mean-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5', 'gfc'], + help='Output data format for GRACE/GRACE-FO mean file', + ) # Output log file for each job in forms # GRACE_mean_run_2002-04-01_PID-00000.log # GRACE_mean_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -493,18 +677,20 @@ def main(): MEAN_FILE=args.mean_file, MEANFORM=args.mean_format, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_file) + if args.log: # write successful job completion log file + output_log_file(args, output_file) + # run main program if __name__ == '__main__': diff --git a/scripts/grace_raster_grids.py b/scripts/grace_raster_grids.py index bd16709c..b3507d3a 100644 --- a/scripts/grace_raster_grids.py +++ b/scripts/grace_raster_grids.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_raster_grids.py Written by Tyler Sutterley (06/2024) @@ -151,6 +151,7 @@ Updated 03/2024: increase mask buffer to twice the smoothing radius Written 08/2023 """ + from __future__ import print_function import sys @@ -168,6 +169,7 @@ geoidtk = gravtk.utilities.import_dependency('geoid_toolkit') pyproj = gravtk.utilities.import_dependency('pyproj') + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -177,28 +179,36 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: try to get the projection information def get_projection(PROJECTION): # EPSG projection code try: crs = pyproj.CRS.from_epsg(int(PROJECTION)) - except (ValueError,pyproj.exceptions.CRSError): + except (ValueError, pyproj.exceptions.CRSError): pass else: return crs # coordinate reference system string try: crs = pyproj.CRS.from_string(PROJECTION) - except (ValueError,pyproj.exceptions.CRSError): + except (ValueError, pyproj.exceptions.CRSError): pass else: return crs # no projection can be made raise pyproj.exceptions.CRSError + # PURPOSE: import GRACE/GRACE-FO files for a given months range # Converts the GRACE/GRACE-FO harmonics applying the specified procedures -def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, +def grace_raster_grids( + base_dir, + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -233,8 +243,8 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, LANDMASK=None, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, - MODE=0o775): - + MODE=0o775, +): # recursively create output directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -247,7 +257,9 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, attributes['ROOT']['product_name'] = DSET attributes['ROOT']['product_type'] = 'gravity_field' attributes['ROOT']['title'] = 'GRACE/GRACE-FO Spatial Data' - attributes['ROOT']['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['ROOT']['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # list object of output files for file logs (full path) output_files = [] @@ -255,15 +267,16 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, suffix = dict(netCDF4='nc', HDF5='H5')[DATAFORM] # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth model and love numbers attributes['ROOT']['earth_model'] = LOVE.model attributes['ROOT']['earth_love_numbers'] = LOVE.citation attributes['ROOT']['reference_frame'] = LOVE.reference # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): + if RAD != 0: gw_str = f'_r{RAD:0.0f}km' attributes['ROOT']['smoothing_radius'] = f'{RAD:0.0f} km' else: @@ -281,11 +294,28 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # 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 = gravtk.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_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + Ylms = gravtk.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_C40=SLR_C40, + 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 = gravtk.harmonics().from_dict(Ylms) nt = len(GRACE_Ylms.time) @@ -297,8 +327,9 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, if MEAN_FILE: # read data form for input mean file (ascii, netCDF4, HDF5, gfc) MEAN_FILE = pathlib.Path(MEAN_FILE).expanduser().absolute() - mean_Ylms = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GRACE_Ylms.subtract(mean_Ylms) attributes['ROOT']['lineage'].append(MEAN_FILE.name) @@ -331,17 +362,15 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # default file prefix if not FILE_PREFIX: - fargs = (PROC,DREL,DSET,Ylms['title'],gia_str) + fargs = (PROC, DREL, DSET, Ylms['title'], gia_str) FILE_PREFIX = '{0}_{1}_{2}{3}{4}_'.format(*fargs) # read Land-Sea Mask and convert to spherical harmonics - land_Ylms = gravtk.land_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + land_Ylms = gravtk.land_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) # Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_REMOVED: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) ocean_str = '_OCN' else: ocean_str = '' @@ -354,37 +383,41 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, 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) + 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'): + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - Ylms = gravtk.harmonics().from_file(REMOVE_FILE, - format=REMOVEFORM) + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) attributes['ROOT']['lineage'].append(Ylms.name) - elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'): + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,removeform = REMOVEFORM.split('-') + _, removeform = REMOVEFORM.split('-') # index containing files in data format - Ylms = gravtk.harmonics().from_index(REMOVE_FILE, - format=removeform) - attributes['ROOT']['lineage'].extend([f.name for f in Ylms.filename]) + Ylms = gravtk.harmonics().from_index( + REMOVE_FILE, format=removeform + ) + attributes['ROOT']['lineage'].extend( + [f.name for f in Ylms.filename] + ) # reduce to GRACE/GRACE-FO months and truncate to degree and order - Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX,mmax=MMAX) + 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] + 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 + 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] + 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() @@ -400,10 +433,10 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # dictionary of coordinate reference system variables crs_to_dict = crs1.to_dict() # Climate and Forecast (CF) Metadata Conventions - if (crs1.to_epsg() == 4326): - y_cf,x_cf = crs1.cs_to_cf() + if crs1.to_epsg() == 4326: + y_cf, x_cf = crs1.cs_to_cf() else: - x_cf,y_cf = crs1.cs_to_cf() + x_cf, y_cf = crs1.cs_to_cf() # output spatial units # Setting units factor for output @@ -425,19 +458,21 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # projection attributes attributes['crs'] = {} # add projection attributes - attributes['crs']['standard_name'] = \ - crs1.to_cf()['grid_mapping_name'].title() + attributes['crs']['standard_name'] = crs1.to_cf()[ + 'grid_mapping_name' + ].title() attributes['crs']['spatial_epsg'] = crs1.to_epsg() attributes['crs']['spatial_ref'] = crs1.to_wkt() attributes['crs']['proj4_params'] = crs1.to_proj4() - for att_name,att_val in crs1.to_cf().items(): + for att_name, att_val in crs1.to_cf().items(): attributes['crs'][att_name] = att_val - if ('lat_0' in crs_to_dict.keys() and (crs1.to_epsg() != 4326)): - attributes['crs']['latitude_of_projection_origin'] = \ - crs_to_dict['lat_0'] + if 'lat_0' in crs_to_dict.keys() and (crs1.to_epsg() != 4326): + attributes['crs']['latitude_of_projection_origin'] = crs_to_dict[ + 'lat_0' + ] # x and y - attributes['x'],attributes['y'] = ({},{}) - for att_name in ['long_name','standard_name','units']: + attributes['x'], attributes['y'] = ({}, {}) + for att_name in ['long_name', 'standard_name', 'units']: attributes['x'][att_name] = x_cf[att_name] attributes['y'][att_name] = y_cf[att_name] # time @@ -457,40 +492,48 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # output data variables output = {} # projection variable - output['crs'] = np.array((),dtype=np.byte) + output['crs'] = np.array((), dtype=np.byte) # spacing and bounds of output grid - dx,dy = np.broadcast_to(np.atleast_1d(SPACING),(2,)) - xmin,xmax,ymin,ymax = np.copy(BOUNDS) + dx, dy = np.broadcast_to(np.atleast_1d(SPACING), (2,)) + xmin, xmax, ymin, ymax = np.copy(BOUNDS) # create x and y from spacing and bounds - output['x'] = np.arange(xmin + dx/2.0, xmax + dx, dx) - output['y'] = np.arange(ymin + dx/2.0, ymax + dy, dy) - ny,nx = (len(output['y']),len(output['x'])) - gridx, gridy = np.meshgrid(output['x'],output['y']) + output['x'] = np.arange(xmin + dx / 2.0, xmax + dx, dx) + output['y'] = np.arange(ymin + dx / 2.0, ymax + dy, dy) + ny, nx = (len(output['y']), len(output['x'])) + gridx, gridy = np.meshgrid(output['x'], output['y']) gridlon, gridlat = transformer.transform(gridx, gridy) # semimajor axis of ellipsoid [m] a_axis = crs1.ellipsoid.semi_major_metre # ellipsoidal flattening - flat = 1.0/crs1.ellipsoid.inverse_flattening + flat = 1.0 / crs1.ellipsoid.inverse_flattening # calculate geocentric latitude and convert to degrees latitude_geocentric = geoidtk.spatial.geocentric_latitude( - gridlon, gridlat, a_axis=a_axis, flat=flat) + gridlon, gridlat, a_axis=a_axis, flat=flat + ) # calculate spatial mask with an extended radius THRESHOLD = 0.025 - mask = gravtk.clenshaw_summation(land_Ylms.clm, land_Ylms.slm, - gridlon.flatten(), latitude_geocentric.flatten(), RAD=2*RAD, - UNITS=1, LMAX=LMAX, LOVE=LOVE) - ii,jj = np.nonzero(mask.reshape(ny,nx) > THRESHOLD) + mask = gravtk.clenshaw_summation( + land_Ylms.clm, + land_Ylms.slm, + gridlon.flatten(), + latitude_geocentric.flatten(), + RAD=2 * RAD, + UNITS=1, + LMAX=LMAX, + LOVE=LOVE, + ) + ii, jj = np.nonzero(mask.reshape(ny, nx) > THRESHOLD) # output gridded raster data - output['z'] = np.ma.zeros((ny,nx,nt), fill_value=fill_value) - output['z'].mask = np.ones((ny,nx,nt), dtype=bool) + output['z'] = np.ma.zeros((ny, nx, nt), fill_value=fill_value) + output['z'].mask = np.ones((ny, nx, nt), dtype=bool) output['time'] = np.zeros((nt)) # converting harmonics to truncated, smoothed coefficients in units # combining harmonics to calculate output raster grids - for i,grace_month in enumerate(GRACE_Ylms.month): + for i, grace_month in enumerate(GRACE_Ylms.month): logging.debug(grace_month) # GRACE/GRACE-FO harmonics for time t Ylms = GRACE_Ylms.index(i) @@ -502,26 +545,37 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # truncate minimum degree to LMIN Ylms.truncate(LMAX, lmin=LMIN, mmax=MMAX) # convert spherical harmonics to output raster grid - output['z'].data[ii,jj,i] = gravtk.clenshaw_summation( - Ylms.clm, Ylms.slm, gridlon[ii,jj], latitude_geocentric[ii,jj], - RAD=RAD, UNITS=units, LMAX=LMAX, LOVE=LOVE) - output['z'].mask[ii,jj,i] = False + output['z'].data[ii, jj, i] = gravtk.clenshaw_summation( + Ylms.clm, + Ylms.slm, + gridlon[ii, jj], + latitude_geocentric[ii, jj], + RAD=RAD, + UNITS=units, + LMAX=LMAX, + LOVE=LOVE, + ) + output['z'].mask[ii, jj, i] = False # copy time variables for month output['time'][i] = np.copy(Ylms.time) # convert masked values to fill value output['z'].data[output['z'].mask] = output['z'].fill_value # output raster files to netCDF4 or HDF5 - FILE = (f'{FILE_PREFIX}{units}_L{LMAX:d}{order_str}{gw_str}{ds_str}_' - f'{START:03d}-{END:03d}.{suffix}') + FILE = ( + f'{FILE_PREFIX}{units}_L{LMAX:d}{order_str}{gw_str}{ds_str}_' + f'{START:03d}-{END:03d}.{suffix}' + ) output_file = OUTPUT_DIRECTORY.joinpath(FILE) # use spatial functions from geoid toolkit to write rasters - if (DATAFORM == 'netCDF4'): - geoidtk.spatial.to_netCDF4(output, attributes, output_file, - data_type='grid') - elif (DATAFORM == 'HDF5'): - geoidtk.spatial.to_HDF5(output, attributes, output_file, - data_type='grid') + if DATAFORM == 'netCDF4': + geoidtk.spatial.to_netCDF4( + output, attributes, output_file, data_type='grid' + ) + elif DATAFORM == 'HDF5': + geoidtk.spatial.to_HDF5( + output, attributes, output_file, data_type='grid' + ) # set the permissions mode of the output files output_file.chmod(mode=MODE) # add file to list @@ -530,10 +584,11 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # return the list of output files return output_files + # PURPOSE: print a file log for the GRACE analysis def output_log_file(input_arguments, output_files): # format: GRACE_processing_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_processing_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -550,10 +605,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE analysis def output_error_log_file(input_arguments): # format: GRACE_processing_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_processing_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -569,102 +625,211 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates monthly spatial raster grids from GRACE/GRACE-FO spherical harmonic coefficients """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for raster files') - parser.add_argument('--file-prefix','-P', + help='Output directory for raster files', + ) + parser.add_argument( + '--file-prefix', + '-P', type=str, - help='Prefix string for input and output files') + help='Prefix string for input and output files', + ) # Data processing center or satellite mission - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # minimum spherical harmonic degree - parser.add_argument('--lmin', - type=int, default=1, - help='Minimum spherical harmonic degree') + parser.add_argument( + '--lmin', type=int, default=1, help='Minimum spherical harmonic degree' + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # output units - parser.add_argument('--units','-U', - type=int, default=1, choices=[1,2,3,4,5], - help='Output units') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[1, 2, 3, 4, 5], + help='Output units', + ) # output grid spacing - parser.add_argument('--spacing', - type=float, default=1.0, nargs='+', - help='Output grid spacing') + parser.add_argument( + '--spacing', + type=float, + default=1.0, + nargs='+', + help='Output grid spacing', + ) # bounds of output grid - parser.add_argument('--bounds', type=float, - nargs=4, default=[-180.0,180.0,-90.0,90.0], - metavar=('xmin','xmax','ymin','ymax'), - help='Output grid extents') + parser.add_argument( + '--bounds', + type=float, + nargs=4, + default=[-180.0, 180.0, -90.0, 90.0], + metavar=('xmin', 'xmax', 'ymin', 'ymax'), + help='Output grid extents', + ) # spatial projection (EPSG code or PROJ4 string) - parser.add_argument('--projection', - type=str, default='4326', - help='Spatial projection as EPSG code or PROJ4 string') + parser.add_argument( + '--projection', + type=str, + default='4326', + help='Spatial projection as EPSG code or PROJ4 string', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -680,21 +845,32 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # correct for pole tide drift follow Wahr et al. (2015) - parser.add_argument('--pole-tide', - default=False, action='store_true', - help='Correct for pole tide drift') + parser.add_argument( + '--pole-tide', + default=False, + action='store_true', + help='Correct for pole tide drift', + ) # Update Degree 1 coefficients with SLR or derived values # Tellus: GRACE/GRACE-FO TN-13 from PO.DAAC # https://grace.jpl.nasa.gov/data/get-data/geocenter/ @@ -706,87 +882,155 @@ def arguments(): # https://doi.org/10.1029/2007JB005338 # GFZ: GRACE/GRACE-FO coefficients from GFZ GravIS # http://gravis.gfz-potsdam.de/corrections - parser.add_argument('--geocenter', - metavar='DEG1', type=str, - choices=['Tellus','SLR','SLF','UCI','Swenson','GFZ'], - help='Update Degree 1 coefficients with SLR or derived values') - parser.add_argument('--geocenter-file', + parser.add_argument( + '--geocenter', + metavar='DEG1', + type=str, + choices=['Tellus', 'SLR', 'SLF', 'UCI', 'Swenson', 'GFZ'], + help='Update Degree 1 coefficients with SLR or derived values', + ) + parser.add_argument( + '--geocenter-file', type=pathlib.Path, - help='Specific geocenter file if not default') - parser.add_argument('--interpolate-geocenter', - default=False, action='store_true', - help='Least-squares model missing Degree 1 coefficients') + help='Specific geocenter file if not default', + ) + parser.add_argument( + '--interpolate-geocenter', + default=False, + action='store_true', + help='Least-squares model missing Degree 1 coefficients', + ) # replace low degree harmonics with values from Satellite Laser Ranging - parser.add_argument('--slr-c20', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C20 coefficients with SLR values') - parser.add_argument('--slr-21', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C21 and S21 coefficients with SLR values') - parser.add_argument('--slr-22', - type=str, default=None, choices=['CSR','GSFC'], - help='Replace C22 and S22 coefficients with SLR values') - parser.add_argument('--slr-c30', - type=str, default=None, choices=['CSR','GFZ','GSFC','LARES'], - help='Replace C30 coefficients with SLR values') - parser.add_argument('--slr-c40', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C40 coefficients with SLR values') - parser.add_argument('--slr-c50', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C50 coefficients with SLR values') + parser.add_argument( + '--slr-c20', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C20 coefficients with SLR values', + ) + parser.add_argument( + '--slr-21', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C21 and S21 coefficients with SLR values', + ) + parser.add_argument( + '--slr-22', + type=str, + default=None, + choices=['CSR', 'GSFC'], + help='Replace C22 and S22 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c30', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC', 'LARES'], + help='Replace C30 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c40', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C40 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c50', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C50 coefficients with SLR values', + ) # input data format (netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['netCDF4', 'HDF5'], + help='Input/output data format', + ) # mean file to remove - parser.add_argument('--mean-file', + parser.add_argument( + '--mean-file', type=pathlib.Path, - help='GRACE/GRACE-FO mean file to remove from the harmonic data') + help='GRACE/GRACE-FO mean file to remove from the harmonic data', + ) # input data format for mean file (ascii, netCDF4, HDF5) - parser.add_argument('--mean-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5','gfc'], - help='Input data format for GRACE/GRACE-FO mean file') + parser.add_argument( + '--mean-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5', 'gfc'], + help='Input data format for GRACE/GRACE-FO mean file', + ) # monthly files to be removed from the GRACE/GRACE-FO data - parser.add_argument('--remove-file', - type=pathlib.Path, nargs='+', - help='Monthly files to be removed from the GRACE/GRACE-FO data') + parser.add_argument( + '--remove-file', + type=pathlib.Path, + nargs='+', + help='Monthly files to be removed from the GRACE/GRACE-FO data', + ) choices = [] - choices.extend(['ascii','netCDF4','HDF5']) - choices.extend(['index-ascii','index-netCDF4','index-HDF5']) - parser.add_argument('--remove-format', - type=str, nargs='+', choices=choices, - help='Input data format for files to be removed') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + choices.extend(['ascii', 'netCDF4', 'HDF5']) + choices.extend(['index-ascii', 'index-netCDF4', 'index-HDF5']) + parser.add_argument( + '--remove-format', + type=str, + nargs='+', + choices=choices, + help='Input data format for files to be removed', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # land-sea mask for redistributing fluxes - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing land water flux') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing land water flux', + ) # Output log file for each job in forms # GRACE_processing_run_2002-04-01_PID-00000.log # GRACE_processing_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -837,18 +1081,20 @@ def main(): LANDMASK=args.mask, OUTPUT_DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/grace_spatial_differences.py b/scripts/grace_spatial_differences.py index c5fac42f..f616aabf 100644 --- a/scripts/grace_spatial_differences.py +++ b/scripts/grace_spatial_differences.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_spatial_differences.py Written by Tyler Sutterley (05/2023) @@ -76,6 +76,7 @@ Updated 10/2018: output smoothed correction file Written 09/2018 """ + import sys import os import copy @@ -102,6 +103,7 @@ gia_mean_str['AW13-ICE6G'] = '_AW13_ICE6G_mean' gia_mean_str['ascii'] = '_AW13_IJ05_ICE6G_mean' + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -111,8 +113,14 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate the GRACE/GRACE-FO difference map for each GIA model -def grace_spatial_differences(PROC, DREL, DSET, LMAX, RAD, +def grace_spatial_differences( + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MMAX=None, @@ -128,8 +136,8 @@ def grace_spatial_differences(PROC, DREL, DSET, LMAX, RAD, OUTPUT_DIRECTORY=None, FLAGS=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # recursively create output directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -159,24 +167,24 @@ def grace_spatial_differences(PROC, DREL, DSET, LMAX, RAD, units = gravtk.units.bycode(UNITS) units_name, units_longname = gravtk.units.get_attributes(units) # input GRACE file format - ff='{0}_{1}_{2}{3}_{4}_{5}_L{6:d}{7}{8}{9}_{10}_{11:03d}-{12:03d}.{13}' + ff = '{0}_{1}_{2}{3}_{4}_{5}_L{6:d}{7}{8}{9}_{10}_{11:03d}-{12:03d}.{13}' # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output Degree Interval - if (INTERVAL == 1): + if INTERVAL == 1: # (-180:180,90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2): + 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) - elif (INTERVAL == 3): + nlon = np.int64(360.0 / dlon) + nlat = np.int64(180.0 / dlat) + elif INTERVAL == 3: # non-global grid set with BOUNDS parameter - minlon,maxlon,minlat,maxlat = BOUNDS.copy() - nlon = np.int64((maxlon-minlon)/dlon) - nlat = np.int64((maxlat-minlat)/dlat) + minlon, maxlon, minlat, maxlat = BOUNDS.copy() + nlon = np.int64((maxlon - minlon) / dlon) + nlat = np.int64((maxlat - minlat) / dlat) # allocate lists for input variables x1 = [None] * 2 @@ -187,7 +195,7 @@ def grace_spatial_differences(PROC, DREL, DSET, LMAX, RAD, # number of rheologies and ice histories to run N = len(GIA_FILES) # iterate over flags - for i,FLAG in enumerate(FLAGS): + for i, FLAG in enumerate(FLAGS): # allocate lists for input variables dinput = {} dinput['x1'] = [] @@ -197,46 +205,101 @@ def grace_spatial_differences(PROC, DREL, DSET, LMAX, RAD, # iterate GIA models for GIA_FILE in GIA_FILES: # input GIA spherical harmonic datafiles - GIA_FILE = pathlib.Path(GIA_FILE).expanduser().absolute() if GIA else None - GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA(GIA_FILE, GIA=GIA, mmax=MMAX) + GIA_FILE = ( + pathlib.Path(GIA_FILE).expanduser().absolute() if GIA else None + ) + GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA( + GIA_FILE, GIA=GIA, mmax=MMAX + ) gia_str = f'_{GIA_Ylms_rate.title}' # read mass trend file and mass trend error files for key in ['x1']: - F1 = ff.format(PROC,DREL,DSET,gia_str,FLAG,units, - LMAX,order_str,gw_str,ds_str,key,START,END,suffix[DATAFORM]) + F1 = ff.format( + PROC, + DREL, + DSET, + gia_str, + FLAG, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(F1) - field_mapping = dict(lon='lon', lat='lat', data='data', error='error') - temp = gravtk.spatial().from_file(INPUT_FILE, - format=DATAFORM, date=False, spacing=[dlon,dlat], - nlon=nlon, nlat=nlat, field_mapping=field_mapping) + field_mapping = dict( + lon='lon', lat='lat', data='data', error='error' + ) + temp = gravtk.spatial().from_file( + INPUT_FILE, + format=DATAFORM, + date=False, + spacing=[dlon, dlat], + nlon=nlon, + nlat=nlat, + field_mapping=field_mapping, + ) dinput[key].append(temp) # read AIC files - for key in ['AIC_x0','AIC_x1','AIC_x2']: - F1 = ff.format(PROC,DREL,DSET,gia_str,FLAG,units, - LMAX,order_str,gw_str,ds_str,key,START,END,suffix[DATAFORM]) + for key in ['AIC_x0', 'AIC_x1', 'AIC_x2']: + F1 = ff.format( + PROC, + DREL, + DSET, + gia_str, + FLAG, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(F1) field_mapping = dict(lon='lon', lat='lat', data='data') - temp = gravtk.spatial().from_file(INPUT_FILE, - format=DATAFORM, date=False, field_mapping=field_mapping, - spacing=[dlon,dlat], nlon=nlon, nlat=nlat) + temp = gravtk.spatial().from_file( + INPUT_FILE, + format=DATAFORM, + date=False, + field_mapping=field_mapping, + spacing=[dlon, dlat], + nlon=nlon, + nlat=nlat, + ) dinput[key].append(temp) # create combined spatial objects - combined = gravtk.spatial().from_list(dinput['x1'],date=False) + combined = gravtk.spatial().from_list(dinput['x1'], date=False) # calculate mean GIA-corrected x1 change (for all Earth rheologies) x1[i] = combined.mean() # GRACE satellite error component - error = combined.copy(); error.data = np.copy(combined.error) - e1[i] = error.sum(power=2.0).scale(1.0/N).power(0.5) + error = combined.copy() + error.data = np.copy(combined.error) + e1[i] = error.sum(power=2.0).scale(1.0 / N).power(0.5) # significance means - AICx0[i] = gravtk.spatial().from_list(dinput['AIC_x0'],date=False).mean() - AICx1[i] = gravtk.spatial().from_list(dinput['AIC_x1'],date=False).mean() - AICx2[i] = gravtk.spatial().from_list(dinput['AIC_x2'],date=False).mean() + AICx0[i] = ( + gravtk.spatial().from_list(dinput['AIC_x0'], date=False).mean() + ) + AICx1[i] = ( + gravtk.spatial().from_list(dinput['AIC_x1'], date=False).mean() + ) + AICx2[i] = ( + gravtk.spatial().from_list(dinput['AIC_x2'], date=False).mean() + ) # set invalid values for masked variables - ii,jj = np.nonzero((np.abs(x1[0].data) <= e1[0].data) | - (np.abs(x1[1].data) <= e1[1].data) | - (AICx1[0].data >= AICx0[0].data) | - (AICx1[1].data >= AICx0[1].data)) + ii, jj = np.nonzero( + (np.abs(x1[0].data) <= e1[0].data) + | (np.abs(x1[1].data) <= e1[1].data) + | (AICx1[0].data >= AICx0[0].data) + | (AICx1[1].data >= AICx0[1].data) + ) # output difference variables output = {} @@ -245,34 +308,34 @@ def grace_spatial_differences(PROC, DREL, DSET, LMAX, RAD, # calculate difference of corrected and uncorrected output['CORR'] = x1[0].zeros_like() output['CORR'].data = x1[0].data - x1[1].data - output_units['CORR'] = '{0} yr^{1:d}'.format(units_name,-1) + output_units['CORR'] = '{0} yr^{1:d}'.format(units_name, -1) output_longname['CORR'] = units_longname # masked correction difference output['MASKED_CORR'] = output['CORR'].copy() - output['MASKED_CORR'].mask[ii,jj] = True + output['MASKED_CORR'].mask[ii, jj] = True output['MASKED_CORR'].update_mask() - output_units['MASKED_CORR'] = '{0} yr^{1:d}'.format(units_name,-1) + output_units['MASKED_CORR'] = '{0} yr^{1:d}'.format(units_name, -1) output_longname['MASKED_CORR'] = units_longname # calculate RMS difference of corrected and uncorrected output['RMS'] = output['CORR'].power(2.0).power(0.5) - output_units['RMS'] = '{0} yr^{1:d}'.format(units_name,-1) + output_units['RMS'] = '{0} yr^{1:d}'.format(units_name, -1) output_longname['RMS'] = units_longname # masked RMS difference output['MASKED_RMS'] = output['RMS'].copy() - output['MASKED_RMS'].mask[ii,jj] = True + output['MASKED_RMS'].mask[ii, jj] = True output['MASKED_RMS'].update_mask() - output_units['MASKED_RMS'] = '{0} yr^{1:d}'.format(units_name,-1) + output_units['MASKED_RMS'] = '{0} yr^{1:d}'.format(units_name, -1) output_longname['MASKED_RMS'] = units_longname # calculate percent difference of corrected and uncorrected output['DIFF'] = x1[0].zeros_like() - output['DIFF'].data = 100.0*output['RMS'].data/np.abs(x1[0].data) + output['DIFF'].data = 100.0 * output['RMS'].data / np.abs(x1[0].data) output_units['DIFF'] = '%' output_longname['DIFF'] = 'Percent' # masked percent difference output['MASKED_DIFF'] = output['DIFF'].copy() - output['MASKED_DIFF'].mask[ii,jj] = True + output['MASKED_DIFF'].mask[ii, jj] = True output['MASKED_DIFF'].update_mask() output_units['MASKED_DIFF'] = '%' output_longname['MASKED_DIFF'] = 'Percent' @@ -287,16 +350,36 @@ def grace_spatial_differences(PROC, DREL, DSET, LMAX, RAD, attributes['title'] = 'GRACE/GRACE-FO Spatial Data' attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' # output data to file - for key,val in output.items(): + for key, val in output.items(): # add specific data attributes attributes['units'] = copy.copy(output_units[key]) attributes['longname'] = copy.copy(output_longname[key]) # output to file - F2 = ff.format(PROC,DREL,DSET,gia_mean_str[GIA],FLAGS[0],units, - LMAX,order_str,gw_str,ds_str,key,START,END,suffix[DATAFORM]) + F2 = ff.format( + PROC, + DREL, + DSET, + gia_mean_str[GIA], + FLAGS[0], + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(F2) - val.to_file(OUTPUT_FILE, format=DATAFORM, field_mapping=field_mapping, - date=False, verbose=VERBOSE, **attributes) + val.to_file( + OUTPUT_FILE, + format=DATAFORM, + field_mapping=field_mapping, + date=False, + verbose=VERBOSE, + **attributes, + ) # change the permissions mode OUTPUT_FILE.chmod(mode=MODE) # add to output file list @@ -305,77 +388,141 @@ def grace_spatial_differences(PROC, DREL, DSET, LMAX, RAD, # return the list of output files return output_files + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the impact of sea level fingerprints on the GRACE/GRACE-FO trend """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') + help='Output directory for spatial files', + ) # GRACE/GRACE-FO flags # 'wSLR_C20_wDEG1' # 'rmTWC' # 'rmSLF_rmTWC' - parser.add_argument('--flag','-f', - type=str, default=['rmTWC_rmSLF','rmTWC'], - nargs=2, help='GRACE/GRACE-FO specific data flags') + parser.add_argument( + '--flag', + '-f', + type=str, + default=['rmTWC_rmSLF', 'rmTWC'], + nargs=2, + help='GRACE/GRACE-FO specific data flags', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # output units - parser.add_argument('--units','-U', - type=int, default=1, choices=[1,2,3,4,5], - help='Output units') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[1, 2, 3, 4, 5], + help='Output units', + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2,3], - help=('Output grid interval ' - '(1: global, 2: centered global, 3: non-global)')) - parser.add_argument('--bounds', - type=float, nargs=4, metavar=('lon_min','lon_max','lat_min','lat_max'), - help='Bounding box for non-global grid') + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2, 3], + help=( + 'Output grid interval ' + '(1: global, 2: centered global, 3: non-global)' + ), + ) + parser.add_argument( + '--bounds', + type=float, + nargs=4, + metavar=('lon_min', 'lon_max', 'lat_min', 'lat_max'), + help='Bounding box for non-global grid', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -391,36 +538,58 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - nargs='+', help='GIA files to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, nargs='+', help='GIA files to read' + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -451,7 +620,8 @@ def main(): OUTPUT_DIRECTORY=args.output_directory, FLAGS=args.flag, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -459,6 +629,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/grace_spatial_error.py b/scripts/grace_spatial_error.py index f8f5f935..a1fc5e22 100755 --- a/scripts/grace_spatial_error.py +++ b/scripts/grace_spatial_error.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_spatial_error.py Written by Tyler Sutterley (05/2023) @@ -161,6 +161,7 @@ Updated 05/2013: algorithm updates following python processing scheme Written 08/2012 """ + from __future__ import print_function import sys @@ -176,6 +177,7 @@ import collections import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -185,9 +187,16 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: import GRACE files for a given months range # Estimates the GRACE/GRACE-FO errors applying the specified procedures -def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, +def grace_spatial_error( + base_dir, + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -217,8 +226,8 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # recursively create output directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -238,21 +247,22 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth model and love numbers attributes['earth_model'] = LOVE.model attributes['earth_love_numbers'] = LOVE.citation attributes['reference_frame'] = LOVE.reference # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' attributes['smoothing_radius'] = f'{RAD:0.0f} km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) gw_str = '' # flag for spherical harmonic order @@ -268,11 +278,28 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, # 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 = gravtk.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_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + Ylms = gravtk.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_C40=SLR_C40, + 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 = gravtk.harmonics().from_dict(Ylms) # add attributes for input GRACE/GRACE-FO spherical harmonics @@ -283,8 +310,9 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, if MEAN_FILE: # read data form for input mean file (ascii, netCDF4, HDF5, gfc) MEAN_FILE = pathlib.Path(MEAN_FILE).expanduser().absolute() - mean_Ylms = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GRACE_Ylms.subtract(mean_Ylms) attributes['lineage'].append(MEAN_FILE.name) @@ -302,15 +330,27 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, ds_str = '' # full path to directory for specific GRACE/GRACE-FO product - GRACE_Ylms.directory = pathlib.Path(Ylms['directory']).expanduser().absolute() + GRACE_Ylms.directory = ( + pathlib.Path(Ylms['directory']).expanduser().absolute() + ) # default file prefix if not FILE_PREFIX: - FILE_PREFIX = '{0}_{1}_{2}{3}_'.format(PROC,DREL,DSET,Ylms['title']) + FILE_PREFIX = '{0}_{1}_{2}{3}_'.format(PROC, DREL, DSET, Ylms['title']) # calculating GRACE error (Wahr et al 2006) # output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) - fargs = (PROC,DREL,DSET,LMAX,order_str,ds_str,atm_str,GRACE_Ylms.month[0], - GRACE_Ylms.month[-1],suffix[DATAFORM]) + fargs = ( + 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 = GRACE_Ylms.directory.joinpath(delta_format.format(*fargs)) # check full path of the GRACE directory for delta file @@ -322,34 +362,35 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, # Delta coefficients of GRACE time series (Error components) delta_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - delta_Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - delta_Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + 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'))): + 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 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 + 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']): + 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 = gravtk.time_series.smooth(GRACE_Ylms.time, - val1[l,m,:], HFWTH=HFWTH) + smth = gravtk.time_series.smooth( + GRACE_Ylms.time, val1[l, m, :], HFWTH=HFWTH + ) # number of smoothed points nsmth = len(smth['data']) tsmth = np.mean(smth['time']) # GRACE/GRACE-FO delta Ylms # variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) - val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth) + val2[l, m] = np.sqrt(np.sum(smth['noise'] ** 2) / nsmth) # attributes for output files kwargs = {} @@ -365,8 +406,7 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, output_files.append(DELTA_FILE) else: # read GRACE/GRACE-FO delta harmonics from file - delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, - format=DATAFORM) + delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, format=DATAFORM) # 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) @@ -375,25 +415,25 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, # Output spatial data object delta = gravtk.spatial() # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output Degree Interval - if (INTERVAL == 1): + if INTERVAL == 1: # (-180:180,90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((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): + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((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) + 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) - elif (INTERVAL == 3): + elif INTERVAL == 3: # non-global grid set with BOUNDS parameter - minlon,maxlon,minlat,maxlat = BOUNDS.copy() - delta.lon = np.arange(minlon+dlon/2.0, maxlon+dlon/2.0, dlon) - delta.lat = np.arange(maxlat-dlat/2.0, minlat-dlat/2.0, -dlat) + minlon, maxlon, minlat, maxlat = BOUNDS.copy() + delta.lon = np.arange(minlon + dlon / 2.0, maxlon + dlon / 2.0, dlon) + delta.lat = np.arange(maxlat - dlat / 2.0, minlat - dlat / 2.0, -dlat) nlon = len(delta.lon) nlat = len(delta.lat) @@ -419,43 +459,58 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, delta.attributes['ROOT'] = attributes # Computing plms for converting to spatial domain - phi = np.radians(delta.lon[np.newaxis,:]) + phi = np.radians(delta.lon[np.newaxis, :]) theta = np.radians(90.0 - delta.lat) PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(theta)) # square of legendre polynomials truncated to order MMAX - mm = np.arange(0, MMAX+1) - PLM2 = PLM[:,mm,:]**2 + 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 + 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) + 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] + 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) + 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 + 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 - fargs = (FILE_PREFIX,units,LMAX,order_str,gw_str,ds_str, - GRACE_Ylms.month[0],GRACE_Ylms.month[-1],suffix[DATAFORM]) + fargs = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + GRACE_Ylms.month[0], + GRACE_Ylms.month[-1], + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(file_format.format(*fargs)) - delta.to_file(OUTPUT_FILE, format=DATAFORM, date=False, - verbose=VERBOSE, units=units_name, longname=units_longname) + delta.to_file( + OUTPUT_FILE, + format=DATAFORM, + date=False, + verbose=VERBOSE, + units=units_name, + longname=units_longname, + ) # set the permissions mode of the output files OUTPUT_FILE.chmod(mode=MODE) # add file to list @@ -464,10 +519,11 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, # return the list of output files return output_files + # PURPOSE: print a file log for the GRACE analysis def output_log_file(input_arguments, output_files): # format: GRACE_error_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_error_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -484,10 +540,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE analysis def output_error_log_file(input_arguments): # format: GRACE_error_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_error_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -503,108 +560,227 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the GRACE/GRACE-FO spatial errors following Wahr et al. (2006) """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + help='Output directory for spatial files', + ) + parser.add_argument( + '--file-prefix', + '-P', type=str, - help='Prefix string for input and output files') + help='Prefix string for input and output files', + ) # Data processing center or satellite mission - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # minimum spherical harmonic degree - parser.add_argument('--lmin', - type=int, default=1, - help='Minimum spherical harmonic degree') + parser.add_argument( + '--lmin', type=int, default=1, help='Minimum spherical harmonic degree' + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # output units - parser.add_argument('--units','-U', - type=int, default=1, choices=[1,2,3,4,5], - help='Output units') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[1, 2, 3, 4, 5], + help='Output units', + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2,3], - help=('Output grid interval ' - '(1: global, 2: centered global, 3: non-global)')) - parser.add_argument('--bounds', - type=float, nargs=4, metavar=('lon_min','lon_max','lat_min','lat_max'), - help='Bounding box for non-global grid') + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2, 3], + help=( + 'Output grid interval ' + '(1: global, 2: centered global, 3: non-global)' + ), + ) + parser.add_argument( + '--bounds', + type=float, + nargs=4, + metavar=('lon_min', 'lon_max', 'lat_min', 'lat_max'), + help='Bounding box for non-global grid', + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # correct for pole tide drift follow Wahr et al. (2015) - parser.add_argument('--pole-tide', - default=False, action='store_true', - help='Correct for pole tide drift') + parser.add_argument( + '--pole-tide', + default=False, + action='store_true', + help='Correct for pole tide drift', + ) # Update Degree 1 coefficients with SLR or derived values # Tellus: GRACE/GRACE-FO TN-13 from PO.DAAC # https://grace.jpl.nasa.gov/data/get-data/geocenter/ @@ -616,69 +792,124 @@ def arguments(): # https://doi.org/10.1029/2007JB005338 # GFZ: GRACE/GRACE-FO coefficients from GFZ GravIS # http://gravis.gfz-potsdam.de/corrections - parser.add_argument('--geocenter', - metavar='DEG1', type=str, - choices=['Tellus','SLR','SLF','UCI','Swenson','GFZ'], - help='Update Degree 1 coefficients with SLR or derived values') - parser.add_argument('--geocenter-file', + parser.add_argument( + '--geocenter', + metavar='DEG1', + type=str, + choices=['Tellus', 'SLR', 'SLF', 'UCI', 'Swenson', 'GFZ'], + help='Update Degree 1 coefficients with SLR or derived values', + ) + parser.add_argument( + '--geocenter-file', type=pathlib.Path, - help='Specific geocenter file if not default') - parser.add_argument('--interpolate-geocenter', - default=False, action='store_true', - help='Least-squares model missing Degree 1 coefficients') + help='Specific geocenter file if not default', + ) + parser.add_argument( + '--interpolate-geocenter', + default=False, + action='store_true', + help='Least-squares model missing Degree 1 coefficients', + ) # replace low degree harmonics with values from Satellite Laser Ranging - parser.add_argument('--slr-c20', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C20 coefficients with SLR values') - parser.add_argument('--slr-21', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C21 and S21 coefficients with SLR values') - parser.add_argument('--slr-22', - type=str, default=None, choices=['CSR','GSFC'], - help='Replace C22 and S22 coefficients with SLR values') - parser.add_argument('--slr-c30', - type=str, default=None, choices=['CSR','GFZ','GSFC','LARES'], - help='Replace C30 coefficients with SLR values') - parser.add_argument('--slr-c40', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C40 coefficients with SLR values') - parser.add_argument('--slr-c50', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C50 coefficients with SLR values') + parser.add_argument( + '--slr-c20', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C20 coefficients with SLR values', + ) + parser.add_argument( + '--slr-21', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C21 and S21 coefficients with SLR values', + ) + parser.add_argument( + '--slr-22', + type=str, + default=None, + choices=['CSR', 'GSFC'], + help='Replace C22 and S22 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c30', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC', 'LARES'], + help='Replace C30 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c40', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C40 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c50', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C50 coefficients with SLR values', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # mean file to remove - parser.add_argument('--mean-file', + parser.add_argument( + '--mean-file', type=pathlib.Path, - help='GRACE/GRACE-FO mean file to remove from the harmonic data') + help='GRACE/GRACE-FO mean file to remove from the harmonic data', + ) # input data format for mean file (ascii, netCDF4, HDF5) - parser.add_argument('--mean-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5','gfc'], - help='Input data format for GRACE/GRACE-FO mean file') + parser.add_argument( + '--mean-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5', 'gfc'], + help='Input data format for GRACE/GRACE-FO mean file', + ) # Output log file for each job in forms # GRACE_error_run_2002-04-01_PID-00000.log # GRACE_error_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -724,18 +955,20 @@ def main(): OUTPUT_DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/grace_spatial_maps.py b/scripts/grace_spatial_maps.py index aff3cf83..b9b09759 100755 --- a/scripts/grace_spatial_maps.py +++ b/scripts/grace_spatial_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_spatial_maps.py Written by Tyler Sutterley (05/2023) @@ -182,6 +182,7 @@ Updated 06/2020: using spatial data class for output operations Updated 05/2020: for public release """ + from __future__ import print_function import sys @@ -197,6 +198,7 @@ import collections import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -206,9 +208,16 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: import GRACE/GRACE-FO files for a given months range # Converts the GRACE/GRACE-FO harmonics applying the specified procedures -def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, +def grace_spatial_maps( + base_dir, + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -244,8 +253,8 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # recursively create output directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -265,21 +274,22 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth model and love numbers attributes['earth_model'] = LOVE.model attributes['earth_love_numbers'] = LOVE.citation attributes['reference_frame'] = LOVE.reference # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' attributes['smoothing_radius'] = f'{RAD:0.0f} km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) gw_str = '' # flag for spherical harmonic order @@ -293,11 +303,28 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # 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 = gravtk.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_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + Ylms = gravtk.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_C40=SLR_C40, + 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 = gravtk.harmonics().from_dict(Ylms) # add attributes for input GRACE/GRACE-FO spherical harmonics @@ -307,8 +334,9 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, if MEAN_FILE: # read data form for input mean file (ascii, netCDF4, HDF5, gfc) MEAN_FILE = pathlib.Path(MEAN_FILE).expanduser().absolute() - mean_Ylms = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GRACE_Ylms.subtract(mean_Ylms) attributes['lineage'].append(MEAN_FILE.name) @@ -341,14 +369,13 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # default file prefix if not FILE_PREFIX: - fargs = (PROC,DREL,DSET,Ylms['title'],gia_str) + 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 = gravtk.ocean_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) ocean_str = '_OCN' else: ocean_str = '' @@ -361,37 +388,39 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, 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) + 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'): + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - Ylms = gravtk.harmonics().from_file(REMOVE_FILE, - format=REMOVEFORM) + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) attributes['lineage'].append(Ylms.name) - elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'): + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,removeform = REMOVEFORM.split('-') + _, removeform = REMOVEFORM.split('-') # index containing files in data format - Ylms = gravtk.harmonics().from_index(REMOVE_FILE, - format=removeform) + Ylms = gravtk.harmonics().from_index( + REMOVE_FILE, format=removeform + ) attributes['lineage'].extend([f.name for f in Ylms.filename]) # reduce to GRACE/GRACE-FO months and truncate to degree and order - Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX,mmax=MMAX) + 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] + 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 + 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] + 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() @@ -403,25 +432,25 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # Output spatial data object grid = gravtk.spatial() # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output Degree Interval - if (INTERVAL == 1): + 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): + 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) + 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): + 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) + 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) @@ -454,7 +483,7 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, 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): + 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 @@ -462,22 +491,43 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # Remove monthly files to be removed Ylms.subtract(remove_Ylms.index(i)) # smooth harmonics and convert to output units - Ylms.convolve(dfactor*wt) + Ylms.convolve(dfactor * wt) # convert spherical harmonics to output spatial grid - grid.data = gravtk.harmonic_summation(Ylms.clm, Ylms.slm, - grid.lon, grid.lat, LMIN=LMIN, LMAX=LMAX, - MMAX=MMAX, PLM=PLM).T + grid.data = gravtk.harmonic_summation( + Ylms.clm, + Ylms.slm, + grid.lon, + grid.lat, + LMIN=LMIN, + LMAX=LMAX, + MMAX=MMAX, + PLM=PLM, + ).T grid.mask = np.zeros_like(grid.data, dtype=bool) # 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 - fargs = (FILE_PREFIX,units,LMAX,order_str,gw_str, - ds_str,grace_month,suffix[DATAFORM]) + fargs = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + grace_month, + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(file_format.format(*fargs)) - grid.to_file(OUTPUT_FILE, format=DATAFORM, date=True, - verbose=VERBOSE, units=units_name, longname=units_longname) + grid.to_file( + OUTPUT_FILE, + format=DATAFORM, + date=True, + verbose=VERBOSE, + units=units_name, + longname=units_longname, + ) # set the permissions mode of the output files OUTPUT_FILE.chmod(mode=MODE) # add file to list @@ -486,10 +536,11 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # return the list of output files return output_files + # PURPOSE: print a file log for the GRACE analysis def output_log_file(input_arguments, output_files): # format: GRACE_processing_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_processing_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -506,10 +557,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE analysis def output_error_log_file(input_arguments): # format: GRACE_processing_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_processing_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -525,100 +577,213 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates monthly spatial maps from GRACE/GRACE-FO spherical harmonic coefficients """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + help='Output directory for spatial files', + ) + parser.add_argument( + '--file-prefix', + '-P', type=str, - help='Prefix string for input and output files') + help='Prefix string for input and output files', + ) # Data processing center or satellite mission - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # minimum spherical harmonic degree - parser.add_argument('--lmin', - type=int, default=1, - help='Minimum spherical harmonic degree') + parser.add_argument( + '--lmin', type=int, default=1, help='Minimum spherical harmonic degree' + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # output units - parser.add_argument('--units','-U', - type=int, default=1, choices=[1,2,3,4,5], - help='Output units') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[1, 2, 3, 4, 5], + help='Output units', + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2,3], - help=('Output grid interval ' - '(1: global, 2: centered global, 3: non-global)')) - parser.add_argument('--bounds', - type=float, nargs=4, metavar=('lon_min','lon_max','lat_min','lat_max'), - help='Bounding box for non-global grid') + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2, 3], + help=( + 'Output grid interval ' + '(1: global, 2: centered global, 3: non-global)' + ), + ) + parser.add_argument( + '--bounds', + type=float, + nargs=4, + metavar=('lon_min', 'lon_max', 'lat_min', 'lat_max'), + help='Bounding box for non-global grid', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -634,21 +799,32 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # correct for pole tide drift follow Wahr et al. (2015) - parser.add_argument('--pole-tide', - default=False, action='store_true', - help='Correct for pole tide drift') + parser.add_argument( + '--pole-tide', + default=False, + action='store_true', + help='Correct for pole tide drift', + ) # Update Degree 1 coefficients with SLR or derived values # Tellus: GRACE/GRACE-FO TN-13 from PO.DAAC # https://grace.jpl.nasa.gov/data/get-data/geocenter/ @@ -660,87 +836,155 @@ def arguments(): # https://doi.org/10.1029/2007JB005338 # GFZ: GRACE/GRACE-FO coefficients from GFZ GravIS # http://gravis.gfz-potsdam.de/corrections - parser.add_argument('--geocenter', - metavar='DEG1', type=str, - choices=['Tellus','SLR','SLF','UCI','Swenson','GFZ'], - help='Update Degree 1 coefficients with SLR or derived values') - parser.add_argument('--geocenter-file', + parser.add_argument( + '--geocenter', + metavar='DEG1', + type=str, + choices=['Tellus', 'SLR', 'SLF', 'UCI', 'Swenson', 'GFZ'], + help='Update Degree 1 coefficients with SLR or derived values', + ) + parser.add_argument( + '--geocenter-file', type=pathlib.Path, - help='Specific geocenter file if not default') - parser.add_argument('--interpolate-geocenter', - default=False, action='store_true', - help='Least-squares model missing Degree 1 coefficients') + help='Specific geocenter file if not default', + ) + parser.add_argument( + '--interpolate-geocenter', + default=False, + action='store_true', + help='Least-squares model missing Degree 1 coefficients', + ) # replace low degree harmonics with values from Satellite Laser Ranging - parser.add_argument('--slr-c20', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C20 coefficients with SLR values') - parser.add_argument('--slr-21', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C21 and S21 coefficients with SLR values') - parser.add_argument('--slr-22', - type=str, default=None, choices=['CSR','GSFC'], - help='Replace C22 and S22 coefficients with SLR values') - parser.add_argument('--slr-c30', - type=str, default=None, choices=['CSR','GFZ','GSFC','LARES'], - help='Replace C30 coefficients with SLR values') - parser.add_argument('--slr-c40', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C40 coefficients with SLR values') - parser.add_argument('--slr-c50', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C50 coefficients with SLR values') + parser.add_argument( + '--slr-c20', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C20 coefficients with SLR values', + ) + parser.add_argument( + '--slr-21', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C21 and S21 coefficients with SLR values', + ) + parser.add_argument( + '--slr-22', + type=str, + default=None, + choices=['CSR', 'GSFC'], + help='Replace C22 and S22 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c30', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC', 'LARES'], + help='Replace C30 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c40', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C40 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c50', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C50 coefficients with SLR values', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # mean file to remove - parser.add_argument('--mean-file', + parser.add_argument( + '--mean-file', type=pathlib.Path, - help='GRACE/GRACE-FO mean file to remove from the harmonic data') + help='GRACE/GRACE-FO mean file to remove from the harmonic data', + ) # input data format for mean file (ascii, netCDF4, HDF5) - parser.add_argument('--mean-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5','gfc'], - help='Input data format for GRACE/GRACE-FO mean file') + parser.add_argument( + '--mean-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5', 'gfc'], + help='Input data format for GRACE/GRACE-FO mean file', + ) # monthly files to be removed from the GRACE/GRACE-FO data - parser.add_argument('--remove-file', - type=pathlib.Path, nargs='+', - help='Monthly files to be removed from the GRACE/GRACE-FO data') + parser.add_argument( + '--remove-file', + type=pathlib.Path, + nargs='+', + help='Monthly files to be removed from the GRACE/GRACE-FO data', + ) choices = [] - choices.extend(['ascii','netCDF4','HDF5']) - choices.extend(['index-ascii','index-netCDF4','index-HDF5']) - parser.add_argument('--remove-format', - type=str, nargs='+', choices=choices, - help='Input data format for files to be removed') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + choices.extend(['ascii', 'netCDF4', 'HDF5']) + choices.extend(['index-ascii', 'index-netCDF4', 'index-HDF5']) + parser.add_argument( + '--remove-format', + type=str, + nargs='+', + choices=choices, + help='Input data format for files to be removed', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # land-sea mask for redistributing fluxes - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing land water flux') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing land water flux', + ) # Output log file for each job in forms # GRACE_processing_run_2002-04-01_PID-00000.log # GRACE_processing_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -792,18 +1036,20 @@ def main(): OUTPUT_DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/grace_spatial_mask.py b/scripts/grace_spatial_mask.py index d124ffb7..740251c8 100644 --- a/scripts/grace_spatial_mask.py +++ b/scripts/grace_spatial_mask.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_spatial_mask.py Written by Tyler Sutterley (11/2024) @@ -38,6 +38,7 @@ UPDATE HISTORY: Written 11/2024 """ + import sys import os import copy @@ -49,6 +50,7 @@ import scipy.stats import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -58,24 +60,26 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') -# PURPOSE: calculate the GRACE/GRACE-FO mask files -def grace_spatial_mask(LMAX, RAD, - START=None, - END=None, - MMAX=None, - DESTRIPE=False, - UNITS=None, - DDEG=None, - INTERVAL=None, - BOUNDS=None, - DATAFORM=None, - REDISTRIBUTE_REMOVED=False, - OUTPUT_DIRECTORY=None, - FILE_PREFIX=None, - VERBOSE=0, - MODE=0o775 - ): +# PURPOSE: calculate the GRACE/GRACE-FO mask files +def grace_spatial_mask( + LMAX, + RAD, + START=None, + END=None, + MMAX=None, + DESTRIPE=False, + UNITS=None, + DDEG=None, + INTERVAL=None, + BOUNDS=None, + DATAFORM=None, + REDISTRIBUTE_REMOVED=False, + OUTPUT_DIRECTORY=None, + FILE_PREFIX=None, + VERBOSE=0, + MODE=0o775, +): # recursively create output directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -109,21 +113,21 @@ def grace_spatial_mask(LMAX, RAD, output_format = '{0}{1}_L{2:d}{3}{4}{5}_{6}_{7:03d}-{8:03d}.{9}' # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output Degree Interval - if (INTERVAL == 1): + if INTERVAL == 1: # (-180:180,90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2): + 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) - elif (INTERVAL == 3): + nlon = np.int64(360.0 / dlon) + nlat = np.int64(180.0 / dlat) + elif INTERVAL == 3: # non-global grid set with BOUNDS parameter - minlon,maxlon,minlat,maxlat = BOUNDS.copy() - nlon = np.int64((maxlon-minlon)/dlon) - nlat = np.int64((maxlat-minlat)/dlat) + minlon, maxlon, minlat, maxlat = BOUNDS.copy() + nlon = np.int64((maxlon - minlon) / dlon) + nlat = np.int64((maxlat - minlat) / dlat) # allocate lists for input variables dinput = {} @@ -131,34 +135,82 @@ def grace_spatial_mask(LMAX, RAD, DOF = dict(x0=0, x1=0, x2=0) # read mass trend file and mass trend error files - for key in ['x1','x2']: - F1 = output_format.format(FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, key, START, END, suffix[DATAFORM]) + for key in ['x1', 'x2']: + F1 = output_format.format( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(F1) field_mapping = dict(lon='lon', lat='lat', data='data', error='error') - dinput[key] = gravtk.spatial().from_file(INPUT_FILE, - format=DATAFORM, date=False, spacing=[dlon,dlat], - nlon=nlon, nlat=nlat, field_mapping=field_mapping) + dinput[key] = gravtk.spatial().from_file( + INPUT_FILE, + format=DATAFORM, + date=False, + spacing=[dlon, dlat], + nlon=nlon, + nlat=nlat, + field_mapping=field_mapping, + ) # read AIC files - for key in ['AIC_x0','AIC_x1','AIC_x2']: - F1 = output_format.format(FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, key, START, END, suffix[DATAFORM]) + for key in ['AIC_x0', 'AIC_x1', 'AIC_x2']: + F1 = output_format.format( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(F1) field_mapping = dict(lon='lon', lat='lat', data='data') - dinput[key] = gravtk.spatial().from_file(INPUT_FILE, - format=DATAFORM, date=False, field_mapping=field_mapping, - spacing=[dlon,dlat], nlon=nlon, nlat=nlat) + dinput[key] = gravtk.spatial().from_file( + INPUT_FILE, + format=DATAFORM, + date=False, + field_mapping=field_mapping, + spacing=[dlon, dlat], + nlon=nlon, + nlat=nlat, + ) # read SSE files - for key in ['SSE_x0','SSE_x1','SSE_x2']: - F1 = output_format.format(FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, key, START, END, suffix[DATAFORM]) + for key in ['SSE_x0', 'SSE_x1', 'SSE_x2']: + F1 = output_format.format( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(F1) field_mapping = dict(lon='lon', lat='lat', data='data') - dinput[key] = gravtk.spatial().from_file(INPUT_FILE, - format=DATAFORM, date=False, field_mapping=field_mapping, - spacing=[dlon,dlat], nlon=nlon, nlat=nlat) + dinput[key] = gravtk.spatial().from_file( + INPUT_FILE, + format=DATAFORM, + date=False, + field_mapping=field_mapping, + spacing=[dlon, dlat], + nlon=nlon, + nlat=nlat, + ) # save degrees of freedom for fit order - order = key.replace('SSE_','') + order = key.replace('SSE_', '') DOF[order] = int(dinput[key].attributes['ROOT']['title']) # output mask files @@ -167,72 +219,133 @@ def grace_spatial_mask(LMAX, RAD, # invalid value for masked grids fill_value = -9999.0 # masked trend values - ii,jj = np.nonzero((np.abs(dinput['x1'].data) <= dinput['x1'].error) | - (dinput['AIC_x1'].data >= dinput['AIC_x0'].data)) + ii, jj = np.nonzero( + (np.abs(dinput['x1'].data) <= dinput['x1'].error) + | (dinput['AIC_x1'].data >= dinput['AIC_x0'].data) + ) output['MASKED_x1'] = dinput['x1'].copy() output['MASKED_x1'].fill_value = fill_value - output['MASKED_x1'].mask[ii,jj] = True + output['MASKED_x1'].mask[ii, jj] = True output['MASKED_x1'].update_mask() - output_units['MASKED_x1'] = '{0} yr^{1:d}'.format(units_name,-1) + output_units['MASKED_x1'] = '{0} yr^{1:d}'.format(units_name, -1) # write masked acceleration values to file - ii,jj = np.nonzero((np.abs(dinput['x2'].data) <= dinput['x2'].error) | - (dinput['AIC_x2'].data >= dinput['AIC_x1'].data)) + ii, jj = np.nonzero( + (np.abs(dinput['x2'].data) <= dinput['x2'].error) + | (dinput['AIC_x2'].data >= dinput['AIC_x1'].data) + ) output['MASKED_x2'] = dinput['x2'].scale(2.0) output['MASKED_x2'].fill_value = fill_value - output['MASKED_x2'].mask[ii,jj] = True + output['MASKED_x2'].mask[ii, jj] = True output['MASKED_x2'].update_mask() - output_units['MASKED_x2'] = '{0} yr^{1:d}'.format(units_name,-2) + output_units['MASKED_x2'] = '{0} yr^{1:d}'.format(units_name, -2) # attributes for output files title = 'GRACE/GRACE-FO Spatial Data' # output data to file - for key,val in output.items(): - F2 = output_format.format(FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, key, START, END, suffix[DATAFORM]) + for key, val in output.items(): + F2 = output_format.format( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(F2) - output_data(val, FILENAME=OUTPUT_FILE, DATAFORM=DATAFORM, - UNITS=output_units[key], LONGNAME=units_longname, - TITLE=title, CONF=0.95, VERBOSE=VERBOSE, MODE=MODE) + output_data( + val, + FILENAME=OUTPUT_FILE, + DATAFORM=DATAFORM, + UNITS=output_units[key], + LONGNAME=units_longname, + TITLE=title, + CONF=0.95, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add to output file list output_files.append(OUTPUT_FILE) # Calculate F-statistics output['F_x1'] = dinput['SSE_x1'].zeros_like() - output['F_x1'].data = (dinput['SSE_x0'].data - dinput['SSE_x1'].data) / \ - (DOF['x0'] - DOF['x1'])*(dinput['SSE_x1'].data / DOF['x1']) + output['F_x1'].data = ( + (dinput['SSE_x0'].data - dinput['SSE_x1'].data) + / (DOF['x0'] - DOF['x1']) + * (dinput['SSE_x1'].data / DOF['x1']) + ) output['F_x2'] = dinput['SSE_x2'].zeros_like() - output['F_x2'].data = (dinput['SSE_x1'].data - dinput['SSE_x2'].data) / \ - (DOF['x1'] - DOF['x2'])*(dinput['SSE_x2'].data / DOF['x2']) + output['F_x2'].data = ( + (dinput['SSE_x1'].data - dinput['SSE_x2'].data) + / (DOF['x1'] - DOF['x2']) + * (dinput['SSE_x2'].data / DOF['x2']) + ) # calculate F-test p-values output['P_x1'] = dinput['SSE_x1'].zeros_like() - output['P_x1'].data = 1.0 - scipy.stats.f.cdf(output['F_x1'].data, - DOF['x0'] - DOF['x1'], DOF['x1']) + output['P_x1'].data = 1.0 - scipy.stats.f.cdf( + output['F_x1'].data, DOF['x0'] - DOF['x1'], DOF['x1'] + ) output['P_x2'] = dinput['SSE_x2'].zeros_like() - output['P_x2'].data = 1.0 - scipy.stats.f.cdf(output['F_x2'].data, - DOF['x1'] - DOF['x2'], DOF['x2']) + output['P_x2'].data = 1.0 - scipy.stats.f.cdf( + output['F_x2'].data, DOF['x1'] - DOF['x2'], DOF['x2'] + ) # for each F-test significance term - signif_longname = {'F_x1':'F Statistic','F_x2':'F Statistic', - 'P_x1':'F-test P-value','P_x2':'F-test P-value'} + signif_longname = { + 'F_x1': 'F Statistic', + 'F_x2': 'F Statistic', + 'P_x1': 'F-test P-value', + 'P_x2': 'F-test P-value', + } # output data to file for key in ['F_x1', 'F_x2', 'P_x1', 'P_x2']: # F-test significance term # output file names for fit significance - F3 = output_format.format(FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, key, START, END, suffix[DATAFORM]) + F3 = output_format.format( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(F3) - output_data(output[key], FILENAME=OUTPUT_FILE, - DATAFORM=DATAFORM, UNITS='1', LONGNAME=signif_longname[key], - TITLE=title, VERBOSE=VERBOSE, MODE=MODE) + output_data( + output[key], + FILENAME=OUTPUT_FILE, + DATAFORM=DATAFORM, + UNITS='1', + LONGNAME=signif_longname[key], + TITLE=title, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add to output file list output_files.append(OUTPUT_FILE) # return the list of files return output_files + # PURPOSE: wrapper function for outputting data to file -def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, - LONGNAME=None, TITLE=None, CONF=0, VERBOSE=0, MODE=0o775): +def output_data( + data, + FILENAME=None, + DATAFORM=None, + UNITS=None, + LONGNAME=None, + TITLE=None, + CONF=0, + VERBOSE=0, + MODE=0o775, +): # field mapping for output regression data field_mapping = {} field_mapping['lat'] = 'lat' @@ -257,104 +370,180 @@ def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, attributes['error']['description'] = 'Uncertainty in model fit' attributes['error']['long_name'] = LONGNAME attributes['error']['units'] = UNITS - attributes['error']['confidence'] = 100*CONF + attributes['error']['confidence'] = 100 * CONF # output global attributes REFERENCE = f'Output from {pathlib.Path(sys.argv[0]).name}' # write to output file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) data.to_ascii(FILENAME, date=False, verbose=VERBOSE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netcdf (.nc) - data.to_netCDF4(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) - elif (DATAFORM == 'HDF5'): + data.to_netCDF4( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) - data.to_HDF5(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) + data.to_HDF5( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) # change the permissions mode of the output file FILENAME.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates mask files for GRACE/GRACE-FO trends following Velicogna (2014) """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + help='Output directory for spatial files', + ) + parser.add_argument( + '--file-prefix', + '-P', type=str, - help='Prefix string for input and output files') + help='Prefix string for input and output files', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # output units - parser.add_argument('--units','-U', - type=int, default=1, choices=[1,2,3,4,5], - help='Output units') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[1, 2, 3, 4, 5], + help='Output units', + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2,3], - help=('Output grid interval ' - '(1: global, 2: centered global, 3: non-global)')) - parser.add_argument('--bounds', - type=float, nargs=4, metavar=('lon_min','lon_max','lat_min','lat_max'), - help='Bounding box for non-global grid') + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2, 3], + help=( + 'Output grid interval ' + '(1: global, 2: centered global, 3: non-global)' + ), + ) + parser.add_argument( + '--bounds', + type=float, + nargs=4, + metavar=('lon_min', 'lon_max', 'lat_min', 'lat_max'), + help='Bounding box for non-global grid', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -380,7 +569,8 @@ def main(): OUTPUT_DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -388,6 +578,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/grace_spatial_mean.py b/scripts/grace_spatial_mean.py index d88d38d5..dc1f93a7 100644 --- a/scripts/grace_spatial_mean.py +++ b/scripts/grace_spatial_mean.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_spatial_mean.py Written by Tyler Sutterley (05/2024) @@ -80,6 +80,7 @@ set grid dimensions, calculate mean of ICE-6G VM5 GIA models Written 08/2017 """ + import sys import os import copy @@ -107,6 +108,7 @@ gia_mean_str['AW13-ICE6G'] = '_AW13_ICE6G_mean' gia_mean_str['ascii'] = '_AW13_IJ05_ICE6G_mean' + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -116,8 +118,14 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate the GRACE/GRACE-FO mean maps for a set of GIA models -def grace_spatial_mean(PROC, DREL, DSET, LMAX, RAD, +def grace_spatial_mean( + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MMAX=None, @@ -133,8 +141,8 @@ def grace_spatial_mean(PROC, DREL, DSET, LMAX, RAD, OUTPUT_DIRECTORY=None, FLAG=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # recursively create output directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -165,24 +173,24 @@ def grace_spatial_mean(PROC, DREL, DSET, LMAX, RAD, units_name, units_longname = gravtk.units.get_attributes(units) # input GRACE file format - ff='{0}_{1}_{2}{3}_{4}_{5}_L{6:d}{7}{8}{9}_{10}_{11:03d}-{12:03d}.{13}' + ff = '{0}_{1}_{2}{3}_{4}_{5}_L{6:d}{7}{8}{9}_{10}_{11:03d}-{12:03d}.{13}' # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output Degree Interval - if (INTERVAL == 1): + if INTERVAL == 1: # (-180:180,90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2): + 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) - elif (INTERVAL == 3): + nlon = np.int64(360.0 / dlon) + nlat = np.int64(180.0 / dlat) + elif INTERVAL == 3: # non-global grid set with BOUNDS parameter - minlon,maxlon,minlat,maxlat = BOUNDS.copy() - nlon = np.int64((maxlon-minlon)/dlon) - nlat = np.int64((maxlat-minlat)/dlat) + minlon, maxlon, minlat, maxlat = BOUNDS.copy() + nlon = np.int64((maxlon - minlon) / dlon) + nlat = np.int64((maxlat - minlat) / dlat) # allocate lists for input variables dinput = {} @@ -200,142 +208,279 @@ def grace_spatial_mean(PROC, DREL, DSET, LMAX, RAD, # number of rheologies and ice histories to run N = len(GIA_FILES) # iterate GIA models - for h,GIA_FILE in enumerate(GIA_FILES): + for h, GIA_FILE in enumerate(GIA_FILES): # input GIA spherical harmonic datafiles - GIA_FILE = pathlib.Path(GIA_FILE).expanduser().absolute() if GIA else None - GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA(GIA_FILE, GIA=GIA, mmax=MMAX) + GIA_FILE = ( + pathlib.Path(GIA_FILE).expanduser().absolute() if GIA else None + ) + GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA( + GIA_FILE, GIA=GIA, mmax=MMAX + ) gia_str = f'_{GIA_Ylms_rate.title}' # read mass trend file and mass trend error files - for key in ['x1','x2']: - F1 = ff.format(PROC,DREL,DSET,gia_str,FLAG,units,LMAX, - order_str,gw_str,ds_str,key,START,END,suffix[DATAFORM]) + for key in ['x1', 'x2']: + F1 = ff.format( + PROC, + DREL, + DSET, + gia_str, + FLAG, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(F1) - field_mapping = dict(lon='lon', lat='lat', data='data', error='error') - temp = gravtk.spatial().from_file(INPUT_FILE, - format=DATAFORM, date=False, spacing=[dlon,dlat], - nlon=nlon, nlat=nlat, field_mapping=field_mapping) + field_mapping = dict( + lon='lon', lat='lat', data='data', error='error' + ) + temp = gravtk.spatial().from_file( + INPUT_FILE, + format=DATAFORM, + date=False, + spacing=[dlon, dlat], + nlon=nlon, + nlat=nlat, + field_mapping=field_mapping, + ) dinput[key].append(temp) # read AIC files - for key in ['AIC_x0','AIC_x1','AIC_x2']: - F1 = ff.format(PROC,DREL,DSET,gia_str,FLAG,units,LMAX, - order_str,gw_str,ds_str,key,START,END,suffix[DATAFORM]) + for key in ['AIC_x0', 'AIC_x1', 'AIC_x2']: + F1 = ff.format( + PROC, + DREL, + DSET, + gia_str, + FLAG, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(F1) field_mapping = dict(lon='lon', lat='lat', data='data') - temp = gravtk.spatial().from_file(INPUT_FILE, - format=DATAFORM, date=False, field_mapping=field_mapping, - spacing=[dlon,dlat], nlon=nlon, nlat=nlat) + temp = gravtk.spatial().from_file( + INPUT_FILE, + format=DATAFORM, + date=False, + field_mapping=field_mapping, + spacing=[dlon, dlat], + nlon=nlon, + nlat=nlat, + ) dinput[key].append(temp) # read SSE files - for key in ['SSE_x0','SSE_x1','SSE_x2']: - F1 = ff.format(PROC,DREL,DSET,gia_str,FLAG,units,LMAX, - order_str,gw_str,ds_str,key,START,END,suffix[DATAFORM]) + for key in ['SSE_x0', 'SSE_x1', 'SSE_x2']: + F1 = ff.format( + PROC, + DREL, + DSET, + gia_str, + FLAG, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(F1) field_mapping = dict(lon='lon', lat='lat', data='data') - temp = gravtk.spatial().from_file(INPUT_FILE, - format=DATAFORM, date=False, field_mapping=field_mapping, - spacing=[dlon,dlat], nlon=nlon, nlat=nlat) + temp = gravtk.spatial().from_file( + INPUT_FILE, + format=DATAFORM, + date=False, + field_mapping=field_mapping, + spacing=[dlon, dlat], + nlon=nlon, + nlat=nlat, + ) dinput[key].append(temp) # save degrees of freedom for fit order - order = key.replace('SSE_','') + order = key.replace('SSE_', '') DOF[order] = int(temp.attributes['ROOT']['title']) # create combined spatial objects output = {} output_units = {} # calculate mean GIA-corrected x1 change (for all Earth rheologies) - x1 = gravtk.spatial().from_list(dinput['x1'],date=False) + x1 = gravtk.spatial().from_list(dinput['x1'], date=False) output['x1'] = x1.mean() - output_units['x1'] = '{0} yr^{1:d}'.format(units_name,-1) + output_units['x1'] = '{0} yr^{1:d}'.format(units_name, -1) # calculate mean acceleration x2 change (for all Earth rheologies) - x2 = gravtk.spatial().from_list(dinput['x2'],date=False) + x2 = gravtk.spatial().from_list(dinput['x2'], date=False) output['x2'] = x2.mean().scale(2.0) - output_units['x2'] = '{0} yr^{1:d}'.format(units_name,-2) + output_units['x2'] = '{0} yr^{1:d}'.format(units_name, -2) # GRACE satellite error component - e1 = x1.copy(); e1.data = np.copy(x1.error) - output['x1'].error = e1.sum(power=2.0).scale(1.0/N).power(0.5).data - e2 = x2.copy(); e2.data = np.copy(x2.error) - output['x2'].error = e2.sum(power=2.0).scale(4.0/N).power(0.5).data + e1 = x1.copy() + e1.data = np.copy(x1.error) + output['x1'].error = e1.sum(power=2.0).scale(1.0 / N).power(0.5).data + e2 = x2.copy() + e2.data = np.copy(x2.error) + output['x2'].error = e2.sum(power=2.0).scale(4.0 / N).power(0.5).data # significance means - AICx0 = gravtk.spatial().from_list(dinput['AIC_x0'],date=False).mean() - AICx1 = gravtk.spatial().from_list(dinput['AIC_x1'],date=False).mean() - AICx2 = gravtk.spatial().from_list(dinput['AIC_x2'],date=False).mean() + AICx0 = gravtk.spatial().from_list(dinput['AIC_x0'], date=False).mean() + AICx1 = gravtk.spatial().from_list(dinput['AIC_x1'], date=False).mean() + AICx2 = gravtk.spatial().from_list(dinput['AIC_x2'], date=False).mean() # calculate residual sum of squares means - s0 = gravtk.spatial().from_list(dinput['SSE_x0'],date=False) - SSEx0 = s0.sum(power=2.0).scale(1.0/N).power(0.5) - s1 = gravtk.spatial().from_list(dinput['SSE_x1'],date=False) - SSEx1 = s1.sum(power=2.0).scale(1.0/N).power(0.5) - s2 = gravtk.spatial().from_list(dinput['SSE_x2'],date=False) - SSEx2 = s2.sum(power=2.0).scale(1.0/N).power(0.5) + s0 = gravtk.spatial().from_list(dinput['SSE_x0'], date=False) + SSEx0 = s0.sum(power=2.0).scale(1.0 / N).power(0.5) + s1 = gravtk.spatial().from_list(dinput['SSE_x1'], date=False) + SSEx1 = s1.sum(power=2.0).scale(1.0 / N).power(0.5) + s2 = gravtk.spatial().from_list(dinput['SSE_x2'], date=False) + SSEx2 = s2.sum(power=2.0).scale(1.0 / N).power(0.5) # invalid value for masked grids fill_value = -9999.0 # masked trend values - ii,jj = np.nonzero((np.abs(output['x1'].data) <= output['x1'].error) | - (AICx1.data >= AICx0.data)) + ii, jj = np.nonzero( + (np.abs(output['x1'].data) <= output['x1'].error) + | (AICx1.data >= AICx0.data) + ) output['MASKED_x1'] = output['x1'].copy() output['MASKED_x1'].fill_value = fill_value - output['MASKED_x1'].mask[ii,jj] = True + output['MASKED_x1'].mask[ii, jj] = True output['MASKED_x1'].update_mask() - output_units['MASKED_x1'] = '{0} yr^{1:d}'.format(units_name,-1) + output_units['MASKED_x1'] = '{0} yr^{1:d}'.format(units_name, -1) # write masked acceleration values to file - ii,jj = np.nonzero((np.abs(output['x2'].data) <= output['x2'].error) | - (AICx2.data >= AICx1.data)) + ii, jj = np.nonzero( + (np.abs(output['x2'].data) <= output['x2'].error) + | (AICx2.data >= AICx1.data) + ) output['MASKED_x2'] = output['x2'].copy() output['MASKED_x2'].fill_value = fill_value - output['MASKED_x2'].mask[ii,jj] = True + output['MASKED_x2'].mask[ii, jj] = True output['MASKED_x2'].update_mask() - output_units['MASKED_x2'] = '{0} yr^{1:d}'.format(units_name,-2) + output_units['MASKED_x2'] = '{0} yr^{1:d}'.format(units_name, -2) # attributes for output files title = 'GRACE/GRACE-FO Spatial Data' # output data to file - for key,val in output.items(): - F2 = ff.format(PROC,DREL,DSET,gia_mean_str[GIA],FLAG,units, - LMAX,order_str,gw_str,ds_str,key,START,END,suffix[DATAFORM]) + for key, val in output.items(): + F2 = ff.format( + PROC, + DREL, + DSET, + gia_mean_str[GIA], + FLAG, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(F2) - output_data(val, FILENAME=OUTPUT_FILE, DATAFORM=DATAFORM, - UNITS=output_units[key], LONGNAME=units_longname, - TITLE=title, CONF=0.95, VERBOSE=VERBOSE, MODE=MODE) + output_data( + val, + FILENAME=OUTPUT_FILE, + DATAFORM=DATAFORM, + UNITS=output_units[key], + LONGNAME=units_longname, + TITLE=title, + CONF=0.95, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add to output file list output_files.append(OUTPUT_FILE) # Calculate F-statistics output['F_x1'] = SSEx1.zeros_like() - output['F_x1'].data = (SSEx0.data - SSEx1.data) / \ - (DOF['x0'] - DOF['x1'])*(SSEx1.data / DOF['x1']) + output['F_x1'].data = ( + (SSEx0.data - SSEx1.data) + / (DOF['x0'] - DOF['x1']) + * (SSEx1.data / DOF['x1']) + ) output['F_x2'] = SSEx2.zeros_like() - output['F_x2'].data = (SSEx1.data - SSEx2.data) / \ - (DOF['x1'] - DOF['x2'])*(SSEx2.data / DOF['x2']) + output['F_x2'].data = ( + (SSEx1.data - SSEx2.data) + / (DOF['x1'] - DOF['x2']) + * (SSEx2.data / DOF['x2']) + ) # calculate F-test p-values output['P_x1'] = SSEx1.zeros_like() - output['P_x1'].data = 1.0 - scipy.stats.f.cdf(output['F_x1'].data, - DOF['x0'] - DOF['x1'], DOF['x1']) + output['P_x1'].data = 1.0 - scipy.stats.f.cdf( + output['F_x1'].data, DOF['x0'] - DOF['x1'], DOF['x1'] + ) output['P_x2'] = SSEx2.zeros_like() - output['P_x2'].data = 1.0 - scipy.stats.f.cdf(output['F_x2'].data, - DOF['x1'] - DOF['x2'], DOF['x2']) + output['P_x2'].data = 1.0 - scipy.stats.f.cdf( + output['F_x2'].data, DOF['x1'] - DOF['x2'], DOF['x2'] + ) # for each F-test significance term - signif_longname = {'F_x1':'F Statistic','F_x2':'F Statistic', - 'P_x1':'F-test P-value','P_x2':'F-test P-value'} + signif_longname = { + 'F_x1': 'F Statistic', + 'F_x2': 'F Statistic', + 'P_x1': 'F-test P-value', + 'P_x2': 'F-test P-value', + } # output data to file for key in ['F_x1', 'F_x2', 'P_x1', 'P_x2']: # F-test significance term # output file names for fit significance - F3 = ff.format(PROC,DREL,DSET,gia_mean_str[GIA],FLAG,units, - LMAX,order_str,gw_str,ds_str,key,START,END,suffix[DATAFORM]) + F3 = ff.format( + PROC, + DREL, + DSET, + gia_mean_str[GIA], + FLAG, + units, + LMAX, + order_str, + gw_str, + ds_str, + key, + START, + END, + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(F3) - output_data(output[key], FILENAME=OUTPUT_FILE, - DATAFORM=DATAFORM, UNITS='1', LONGNAME=signif_longname[key], - TITLE=title, VERBOSE=VERBOSE, MODE=MODE) + output_data( + output[key], + FILENAME=OUTPUT_FILE, + DATAFORM=DATAFORM, + UNITS='1', + LONGNAME=signif_longname[key], + TITLE=title, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add to output file list output_files.append(OUTPUT_FILE) # return the list of files return output_files + # PURPOSE: wrapper function for outputting data to file -def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, - LONGNAME=None, TITLE=None, CONF=0, VERBOSE=0, MODE=0o775): +def output_data( + data, + FILENAME=None, + DATAFORM=None, + UNITS=None, + LONGNAME=None, + TITLE=None, + CONF=0, + VERBOSE=0, + MODE=0o775, +): # field mapping for output regression data field_mapping = {} field_mapping['lat'] = 'lat' @@ -360,97 +505,172 @@ def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, attributes['error']['description'] = 'Uncertainty in model fit' attributes['error']['long_name'] = LONGNAME attributes['error']['units'] = UNITS - attributes['error']['confidence'] = 100*CONF + attributes['error']['confidence'] = 100 * CONF # output global attributes REFERENCE = f'Output from {pathlib.Path(sys.argv[0]).name}' # write to output file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) data.to_ascii(FILENAME, date=False, verbose=VERBOSE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netcdf (.nc) - data.to_netCDF4(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) - elif (DATAFORM == 'HDF5'): + data.to_netCDF4( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) - data.to_HDF5(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) + data.to_HDF5( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) # change the permissions mode of the output file FILENAME.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the mean GRACE/GRACE-FO trend for a range of GIA outputs """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') + help='Output directory for spatial files', + ) # GRACE/GRACE-FO flags # 'wSLR_C20_wDEG1' # 'rmTWC' # 'rmSLF_rmTWC' - parser.add_argument('--flag','-f', - type=str, default='rmSLF_rmTWC', - help='GRACE/GRACE-FO specific data flags') + parser.add_argument( + '--flag', + '-f', + type=str, + default='rmSLF_rmTWC', + help='GRACE/GRACE-FO specific data flags', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # output units - parser.add_argument('--units','-U', - type=int, default=1, choices=[1,2,3,4,5], - help='Output units') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[1, 2, 3, 4, 5], + help='Output units', + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2,3], - help=('Output grid interval ' - '(1: global, 2: centered global, 3: non-global)')) - parser.add_argument('--bounds', - type=float, nargs=4, metavar=('lon_min','lon_max','lat_min','lat_max'), - help='Bounding box for non-global grid') + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2, 3], + help=( + 'Output grid interval ' + '(1: global, 2: centered global, 3: non-global)' + ), + ) + parser.add_argument( + '--bounds', + type=float, + nargs=4, + metavar=('lon_min', 'lon_max', 'lat_min', 'lat_max'), + help='Bounding box for non-global grid', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -466,36 +686,58 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - nargs='+', help='GIA files to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, nargs='+', help='GIA files to read' + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -526,7 +768,8 @@ def main(): OUTPUT_DIRECTORY=args.output_directory, FLAG=args.flag, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -534,6 +777,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/make_sea_level_error_shells.py b/scripts/make_sea_level_error_shells.py index b3a001ae..f70696de 100755 --- a/scripts/make_sea_level_error_shells.py +++ b/scripts/make_sea_level_error_shells.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" make_sea_level_error_shells.py Written by Tyler Sutterley (05/2023) @@ -120,6 +120,7 @@ Updated 08/2017: added flag for polar feedback Written 08/2017 """ + from __future__ import print_function import sys @@ -133,6 +134,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -142,8 +144,14 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate harmonics for a set of mascons from a coordinate index -def make_sea_level_error_shells(PROC, DREL, DSET, LMAX, RAD, +def make_sea_level_error_shells( + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -169,8 +177,8 @@ def make_sea_level_error_shells(PROC, DREL, DSET, LMAX, RAD, LANDMASK=None, OUTPUT_DIRECTORY=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # recursively create output directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -179,7 +187,7 @@ def make_sea_level_error_shells(PROC, DREL, DSET, LMAX, RAD, # output filename suffix suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # number of GRACE/GRACE-FO months - nmon = len(sorted(set(np.arange(START,END+1)) - set(MISSING))) + nmon = len(sorted(set(np.arange(START, END + 1)) - set(MISSING))) # for datasets not GSM: will add a label for the dataset dset_str = '' if (DSET == 'GSM') else f'_{DSET}' @@ -230,14 +238,15 @@ def make_sea_level_error_shells(PROC, DREL, DSET, LMAX, RAD, # column 1: cap number # column 2: longitude of center point # column 3: latitude of center point - num = coord[:,0].astype(np.int64) - lon = coord[:,1] - lat = coord[:,2] + num = coord[:, 0].astype(np.int64) + lon = coord[:, 1] + lat = coord[:, 2] n_crd = len(num) # read load love numbers - LOVE = gravtk.load_love_numbers(EXPANSION, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE) + LOVE = gravtk.load_love_numbers( + EXPANSION, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE + ) # input mascon spherical harmonic datafiles # Read mascon index file and get contents @@ -246,7 +255,7 @@ def make_sea_level_error_shells(PROC, DREL, DSET, LMAX, RAD, mascon_files = f.read().splitlines() # number of mascons n_mas = len(mascon_files) - if (n_crd != n_mas): + if n_crd != n_mas: errmsg = f'Mismatching number of mascons ({n_crd:d},{n_mas:d})' logging.critical(errmsg) # spatial area of the mascon @@ -255,13 +264,13 @@ def make_sea_level_error_shells(PROC, DREL, DSET, LMAX, RAD, mascon_name = [] # allocate for mascon Ylms expanded up to degree of sea level fingerprints mascon_Ylms = gravtk.harmonics(lmax=EXPANSION, mmax=EXPANSION) - mascon_Ylms.clm = np.zeros((EXPANSION+1, EXPANSION+1, n_mas)) - mascon_Ylms.slm = np.zeros((EXPANSION+1, EXPANSION+1, n_mas)) + mascon_Ylms.clm = np.zeros((EXPANSION + 1, EXPANSION + 1, n_mas)) + mascon_Ylms.slm = np.zeros((EXPANSION + 1, EXPANSION + 1, n_mas)) mascon_Ylms.error = np.zeros((n_mas)) # for each mascon # calculate the spherical harmonic coefs # write spherical harmonics to file - for i,fi in enumerate(mascon_files): + for i, fi in enumerate(mascon_files): # stem is the mascon file without directory or suffix # if lower case: will capitalize # if mascon name contains degree and order info: scrub from string @@ -272,36 +281,59 @@ def make_sea_level_error_shells(PROC, DREL, DSET, LMAX, RAD, # mascon name, GRACE dataset, GIA model, LMAX, (MMAX,) # Gaussian smoothing, filter flag, remove reconstructed fields flag # output GRACE error file - file_input='{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}.txt'.format(mascon_name[i], - dset_str,gia_str,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( + mascon_name[i], + dset_str, + gia_str, + atm_str, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + ) dinput = np.loadtxt(OUTPUT_DIRECTORY.joinpath(file_input)) - mascon_Ylms.month = dinput[:,0].astype(np.int64) - mascon_Ylms.time = dinput[:,1].copy() - total_area[i] = dinput[0,4].copy() + mascon_Ylms.month = dinput[:, 0].astype(np.int64) + mascon_Ylms.time = dinput[:, 1].copy() + total_area[i] = dinput[0, 4].copy() # Calculate spherical harmonic coefficients for given type # truncate spherical harmonics at degree EXPANSION - if (MASCON_TYPE == 'DISC'): + if MASCON_TYPE == 'DISC': # Calculate for a disc load using 1 Gt - Ylms = gravtk.gen_disc_load(1.0,lon[i],lat[i],total_area[i], - LMAX=EXPANSION,LOVE=LOVE) - elif (MASCON_TYPE == 'POINT'): + Ylms = gravtk.gen_disc_load( + 1.0, lon[i], lat[i], total_area[i], LMAX=EXPANSION, LOVE=LOVE + ) + elif MASCON_TYPE == 'POINT': # Calculate for a point load using 1 Gt - Ylms = gravtk.gen_point_load(np.array(1.0),lon[i],lat[i], - LMAX=EXPANSION,UNITS=2,LOVE=LOVE) - elif (MASCON_TYPE == 'CAP'): + Ylms = gravtk.gen_point_load( + np.array(1.0), + lon[i], + lat[i], + LMAX=EXPANSION, + UNITS=2, + LOVE=LOVE, + ) + elif MASCON_TYPE == 'CAP': # Calculate for a spherical cap using 1 Gt - Ylms = gravtk.gen_spherical_cap(1.0,lon[i],lat[i],LMAX=EXPANSION, - AREA=total_area[i]*1e10,UNITS=2,LOVE=LOVE) + Ylms = gravtk.gen_spherical_cap( + 1.0, + lon[i], + lat[i], + LMAX=EXPANSION, + AREA=total_area[i] * 1e10, + UNITS=2, + LOVE=LOVE, + ) # save spherical harmonics for mascon - mascon_Ylms.clm[:,:,i] = Ylms.clm[:,:].copy() - mascon_Ylms.slm[:,:,i] = Ylms.slm[:,:].copy() + mascon_Ylms.clm[:, :, i] = Ylms.clm[:, :].copy() + mascon_Ylms.slm[:, :, i] = Ylms.slm[:, :].copy() # save mascon error - mascon_Ylms.error[i] = dinput[0,3].copy() + mascon_Ylms.error[i] = dinput[0, 3].copy() # list of output files output_files = [] # create sea level shell script - args = ('MC',ITERATION,dset_str,ocean_str,EXPANSION,START,END) + args = ('MC', ITERATION, dset_str, ocean_str, EXPANSION, START, END) f1 = '{0}_ITERATION_{1}_INDEX{2}{3}_L{4:d}_{5:03d}-{6:03}.sh'.format(*args) output_shell_script = OUTPUT_DIRECTORY.joinpath(f1) fid = output_shell_script.open(mode='w', encoding='utf8') @@ -318,16 +350,24 @@ def make_sea_level_error_shells(PROC, DREL, DSET, LMAX, RAD, for n in range(RUNS): # spherical harmonics for iteration of monte carlo Ylms = gravtk.harmonics(lmax=EXPANSION, mmax=EXPANSION) - Ylms.clm = np.zeros((EXPANSION+1, EXPANSION+1)) - Ylms.slm = np.zeros((EXPANSION+1, EXPANSION+1)) + Ylms.clm = np.zeros((EXPANSION + 1, EXPANSION + 1)) + Ylms.slm = np.zeros((EXPANSION + 1, EXPANSION + 1)) # create random values between 0 and 1 for each mascon random_value = np.random.rand(n_mas) - for i,fi in enumerate(mascon_files): + for i, fi in enumerate(mascon_files): # calculate uniformly distributed error and calculate harmonics - error_random = (1.0-2.0*random_value[i])*mascon_Ylms.error[i] + error_random = (1.0 - 2.0 * random_value[i]) * mascon_Ylms.error[i] Ylms.add(mascon_Ylms.index(i, date=False).scale(error_random)) # output to file formatted for use in the sea level equation functions - args=(MASCON_TYPE,ITERATION,dset_str,ocean_str,EXPANSION,n,suffix[DATAFORM]) + args = ( + MASCON_TYPE, + ITERATION, + dset_str, + ocean_str, + EXPANSION, + n, + suffix[DATAFORM], + ) input_load = OUTPUT_DIRECTORY.joinpath(file_format.format(*args)) # output spherical harmonic file in data format Ylms.to_file(input_load, format=DATAFORM, date=False, **attributes) @@ -336,14 +376,33 @@ def make_sea_level_error_shells(PROC, DREL, DSET, LMAX, RAD, # print file name to index output_files.append(input_load) # output sea level fingerprint file - args=('SLF',ITERATION,dset_str,ocean_str,EXPANSION,n,suffix[DATAFORM]) + args = ( + 'SLF', + ITERATION, + dset_str, + ocean_str, + EXPANSION, + n, + suffix[DATAFORM], + ) output_slf = OUTPUT_DIRECTORY.joinpath(file_format.format(*args)) # print shell script commands - args = (child_program, expansion_flag, polar_flag, love_flag, - body_flag, fluid_flag, reference_flag, mask_flag, - iter_flag, format_flag, verbosity_flag, oct(MODE), + args = ( + child_program, + expansion_flag, + polar_flag, + love_flag, + body_flag, + fluid_flag, + reference_flag, + mask_flag, + iter_flag, + format_flag, + verbosity_flag, + oct(MODE), Ylms.compressuser(input_load), - gravtk.spatial().compressuser(output_slf)) + gravtk.spatial().compressuser(output_slf), + ) print(shell_format.format(*args), file=fid) # close the shell script fid.close() @@ -353,11 +412,12 @@ def make_sea_level_error_shells(PROC, DREL, DSET, LMAX, RAD, # return list of output files return output_files + # PURPOSE: print a file log for the mascon harmonic calculation def output_log_file(input_arguments, output_files): # format: mascon_disc_run_2002-04-01_PID-70335.log TYPE = arguments.mascon_type.lower() - args = (TYPE,time.strftime('%Y-%m-%d',time.localtime()),os.getpid()) + args = (TYPE, time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'mascon_{0}_run_{1}_PID-{2:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -374,11 +434,12 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the mascon harmonic calculation def output_error_log_file(input_arguments): # format: failed_mascon_disc_run_2002-04-01_PID-70335.log TYPE = arguments.mascon_type.lower() - args = (TYPE,time.strftime('%Y-%m-%d',time.localtime()),os.getpid()) + args = (TYPE, time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'failed_mascon_{0}_run_{1}_PID-{2:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -394,91 +455,191 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Computes spherical harmonics for a set of mascon files. Creates a shell script for running sea level variation code. """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # different treatments of the body tide Love numbers of degree 2 # 0: Wahr (1981) and Wahr (1985) values from PREM # 1: Farrell (1972) values from Gutenberg-Bullen oceanic mantle model - parser.add_argument('--body','-b', - type=int, default=0, choices=[0,1], - help='Treatment of the body tide Love number') + parser.add_argument( + '--body', + '-b', + type=int, + default=0, + choices=[0, 1], + help='Treatment of the body tide Love number', + ) # different treatments of the fluid Love number of gravitational potential # 0: Han and Wahr (1989) fluid love number # 1: Munk and MacDonald (1960) secular love number # 2: Munk and MacDonald (1960) fluid love number # 3: Lambeck (1980) fluid love number - parser.add_argument('--fluid','-f', - type=int, default=0, choices=[0,1,2,3], - help='Treatment of the fluid Love number') + parser.add_argument( + '--fluid', + '-f', + type=int, + default=0, + choices=[0, 1, 2, 3], + help='Treatment of the fluid Love number', + ) # option for polar feedback - parser.add_argument('--polar-feedback', - default=False, action='store_true', - help='Include effects of polar feedback') + parser.add_argument( + '--polar-feedback', + default=False, + action='store_true', + help='Include effects of polar feedback', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -494,77 +655,131 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # mascon index file and parameters - parser.add_argument('--mascon-file', + parser.add_argument( + '--mascon-file', type=pathlib.Path, - help='Index file of mascons spherical harmonics') - parser.add_argument('--coordinate-file', + help='Index file of mascons spherical harmonics', + ) + parser.add_argument( + '--coordinate-file', type=pathlib.Path, required=True, - help='File with spatial coordinates of mascon centers') + help='File with spatial coordinates of mascon centers', + ) # number of header lines to skip in coordinate file - parser.add_argument('--header','-H', - type=int, default=0, - help='Number of header lines to skip in coordinate file') + parser.add_argument( + '--header', + '-H', + type=int, + default=0, + help='Number of header lines to skip in coordinate file', + ) # input load type (DISC, POINT or CAP) - parser.add_argument('--mascon-type','-T', - type=str.upper, default='CAP', choices=['DISC','POINT','CAP'], - help='Input load type') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--mascon-type', + '-T', + type=str.upper, + default='CAP', + choices=['DISC', 'POINT', 'CAP'], + help='Input load type', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # sea level fingerprint parameters - parser.add_argument('--iteration','-I', - type=int, default=1, - help='Sea level fingerprint iteration') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--iteration', + '-I', + type=int, + default=1, + help='Sea level fingerprint iteration', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # number of monte carlo iterations - parser.add_argument('--runs', - type=int, default=10000, - help='Number of Monte Carlo iterations') + parser.add_argument( + '--runs', + type=int, + default=10000, + help='Number of Monte Carlo iterations', + ) # land-sea mask for redistributing mascon mass and land water flux - parser.add_argument('--mask', + parser.add_argument( + '--mask', type=pathlib.Path, - help='Land-sea mask for redistributing mascon mass and land water flux') + help='Land-sea mask for redistributing mascon mass and land water flux', + ) # Output log file for each job in forms # mascon_disc_run_2002-04-01_PID-00000.log # failed_mascon_disc_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -605,18 +820,20 @@ def main(): LANDMASK=args.mask, OUTPUT_DIRECTORY=args.output_directory, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/make_sea_level_mascon_shells.py b/scripts/make_sea_level_mascon_shells.py index 07e9ffc2..a7d6a113 100755 --- a/scripts/make_sea_level_mascon_shells.py +++ b/scripts/make_sea_level_mascon_shells.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" make_sea_level_mascon_shells.py Written by Tyler Sutterley (05/2023) @@ -118,6 +118,7 @@ Updated 08/2017: added flag to incorporate polar feedback Written 08/2017 """ + from __future__ import print_function import sys @@ -131,6 +132,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -140,8 +142,14 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate harmonics for a set of mascons from a coordinate index -def make_sea_level_mascon_shells(PROC, DREL, DSET, LMAX, RAD, +def make_sea_level_mascon_shells( + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -166,8 +174,8 @@ def make_sea_level_mascon_shells(PROC, DREL, DSET, LMAX, RAD, LANDMASK=None, OUTPUT_DIRECTORY=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # recursively create output directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -176,7 +184,7 @@ def make_sea_level_mascon_shells(PROC, DREL, DSET, LMAX, RAD, # output filename suffix suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # number of GRACE/GRACE-FO months - nmon = len(sorted(set(np.arange(START,END+1)) - set(MISSING))) + nmon = len(sorted(set(np.arange(START, END + 1)) - set(MISSING))) # for datasets not GSM: will add a label for the dataset dset_str = '' if (DSET == 'GSM') else f'_{DSET}' @@ -229,14 +237,15 @@ def make_sea_level_mascon_shells(PROC, DREL, DSET, LMAX, RAD, # column 1: cap number # column 2: longitude of center point # column 3: latitude of center point - num = coord[:,0].astype(np.int64) - lon = coord[:,1] - lat = coord[:,2] + num = coord[:, 0].astype(np.int64) + lon = coord[:, 1] + lat = coord[:, 2] n_crd = len(num) # read load love numbers - LOVE = gravtk.load_love_numbers(EXPANSION, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE) + LOVE = gravtk.load_love_numbers( + EXPANSION, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE + ) # input mascon spherical harmonic datafiles # Read mascon index file and get contents @@ -245,7 +254,7 @@ def make_sea_level_mascon_shells(PROC, DREL, DSET, LMAX, RAD, mascon_files = f.read().splitlines() # number of mascons n_mas = len(mascon_files) - if (n_crd != n_mas): + if n_crd != n_mas: errmsg = f'Mismatching number of mascons ({n_crd:d},{n_mas:d})' logging.critical(errmsg) # spatial area of the mascon @@ -254,12 +263,12 @@ def make_sea_level_mascon_shells(PROC, DREL, DSET, LMAX, RAD, mascon_name = [] # allocate for mascon Ylms expanded up to degree of sea level fingerprints mascon_Ylms = gravtk.harmonics(lmax=EXPANSION, mmax=EXPANSION) - mascon_Ylms.clm = np.zeros((EXPANSION+1, EXPANSION+1, nmon)) - mascon_Ylms.slm = np.zeros((EXPANSION+1, EXPANSION+1, nmon)) + mascon_Ylms.clm = np.zeros((EXPANSION + 1, EXPANSION + 1, nmon)) + mascon_Ylms.slm = np.zeros((EXPANSION + 1, EXPANSION + 1, nmon)) # for each mascon # calculate the spherical harmonic coefs # write spherical harmonics to file - for i,fi in enumerate(mascon_files): + for i, fi in enumerate(mascon_files): # stem is the mascon file without directory or suffix # if lower case: will capitalize # if mascon name contains degree and order info: scrub from string @@ -270,37 +279,75 @@ def make_sea_level_mascon_shells(PROC, DREL, DSET, LMAX, RAD, # mascon name, GRACE dataset, GIA model, LMAX, (MMAX,) # Gaussian smoothing, filter flag, remove reconstructed fields flag # output GRACE error file - file_input='{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}.txt'.format(mascon_name[i], - dset_str,gia_str,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( + mascon_name[i], + dset_str, + gia_str, + atm_str, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + ) dinput = np.loadtxt(OUTPUT_DIRECTORY.joinpath(file_input)) - mascon_Ylms.month = dinput[:,0].astype(np.int64) - mascon_Ylms.time = dinput[:,1].copy() - total_area[i] = dinput[0,4].copy() + mascon_Ylms.month = dinput[:, 0].astype(np.int64) + mascon_Ylms.time = dinput[:, 1].copy() + total_area[i] = dinput[0, 4].copy() # Calculate spherical harmonic coefficients for given type # truncate spherical harmonics at degree EXPANSION - if (MASCON_TYPE == 'DISC'): + if MASCON_TYPE == 'DISC': # Calculate for a disc load using 1 Gt - Ylms = gravtk.gen_disc_load(1.0,lon[i],lat[i],total_area[i], - LMAX=EXPANSION,LOVE=LOVE) - elif (MASCON_TYPE == 'POINT'): + Ylms = gravtk.gen_disc_load( + 1.0, lon[i], lat[i], total_area[i], LMAX=EXPANSION, LOVE=LOVE + ) + elif MASCON_TYPE == 'POINT': # Calculate for a point load using 1 Gt - Ylms = gravtk.gen_point_load(np.array(1.0),lon[i],lat[i], - LMAX=EXPANSION,UNITS=2,LOVE=LOVE) - elif (MASCON_TYPE == 'CAP'): + Ylms = gravtk.gen_point_load( + np.array(1.0), + lon[i], + lat[i], + LMAX=EXPANSION, + UNITS=2, + LOVE=LOVE, + ) + elif MASCON_TYPE == 'CAP': # Calculate for a spherical cap using 1 Gt - Ylms = gravtk.gen_spherical_cap(1.0,lon[i],lat[i],LMAX=EXPANSION, - AREA=total_area[i]*1e10,UNITS=2,LOVE=LOVE) + Ylms = gravtk.gen_spherical_cap( + 1.0, + lon[i], + lat[i], + LMAX=EXPANSION, + AREA=total_area[i] * 1e10, + UNITS=2, + LOVE=LOVE, + ) # calculate total coefficients for each date for t in range(nmon): - mascon_Ylms.clm[:,:,t] += dinput[t,2]*Ylms.clm[:,:] - mascon_Ylms.slm[:,:,t] += dinput[t,2]*Ylms.slm[:,:] + mascon_Ylms.clm[:, :, t] += dinput[t, 2] * Ylms.clm[:, :] + mascon_Ylms.slm[:, :, t] += dinput[t, 2] * Ylms.slm[:, :] # list of output files output_files = [] # create sea level shell script and index file - args = (MASCON_TYPE, ITERATION, dset_str, gia_str, ocean_str, EXPANSION, START, END) - f1 = '{0}_ITERATION_{1}_INDEX{2}{3}{4}_L{5:d}_{6:03d}-{7:03}.sh'.format(*args) - f2 = '{0}_ITERATION_{1}_INDEX{2}{3}{4}_CLM_L{5:d}_{6:03d}-{7:03}.txt'.format(*args) + args = ( + MASCON_TYPE, + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + START, + END, + ) + f1 = '{0}_ITERATION_{1}_INDEX{2}{3}{4}_L{5:d}_{6:03d}-{7:03}.sh'.format( + *args + ) + f2 = ( + '{0}_ITERATION_{1}_INDEX{2}{3}{4}_CLM_L{5:d}_{6:03d}-{7:03}.txt'.format( + *args + ) + ) output_shell_script = OUTPUT_DIRECTORY.joinpath(f1) output_index_file = OUTPUT_DIRECTORY.joinpath(f2) fid1 = output_shell_script.open(mode='w', encoding='utf8') @@ -310,15 +357,25 @@ def make_sea_level_mascon_shells(PROC, DREL, DSET, LMAX, RAD, # output file format for input_distribution and output_slf file_format = '{0}_ITERATION_{1}{2}{3}{4}_L{5:d}_{6:03d}.{7}' # formatting string for each line in the shell script - shell_format='{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11} --mode {12} {13} {14}' + shell_format = ( + '{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11} --mode {12} {13} {14}' + ) # attributes for output files attributes = {} attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' # output file for each date - for t,grace_month in enumerate(mascon_Ylms.month): + for t, grace_month in enumerate(mascon_Ylms.month): # output to file formatted for use in the sea level equation functions - args = (MASCON_TYPE, ITERATION, dset_str, gia_str, ocean_str, EXPANSION, - grace_month, suffix[DATAFORM]) + args = ( + MASCON_TYPE, + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + grace_month, + suffix[DATAFORM], + ) input_load = OUTPUT_DIRECTORY.joinpath(file_format.format(*args)) # output spherical harmonic file in data format Ylms = mascon_Ylms.index(t) @@ -328,15 +385,35 @@ def make_sea_level_mascon_shells(PROC, DREL, DSET, LMAX, RAD, # print file name to index output_files.append(input_load) # print shell script commands - args = ('SLF', ITERATION, dset_str, gia_str, ocean_str, EXPANSION, - grace_month, suffix[DATAFORM]) + args = ( + 'SLF', + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + grace_month, + suffix[DATAFORM], + ) output_slf = OUTPUT_DIRECTORY.joinpath(file_format.format(*args)) # shell script command - args = (child_program, expansion_flag, polar_flag, love_flag, - body_flag, fluid_flag, reference_flag, date_flag, mask_flag, - iter_flag, format_flag, verbosity_flag, oct(MODE), + args = ( + child_program, + expansion_flag, + polar_flag, + love_flag, + body_flag, + fluid_flag, + reference_flag, + date_flag, + mask_flag, + iter_flag, + format_flag, + verbosity_flag, + oct(MODE), Ylms.compressuser(input_load), - gravtk.spatial().compressuser(output_slf)) + gravtk.spatial().compressuser(output_slf), + ) print(shell_format.format(*args), file=fid1) # print harmonics to index file print(Ylms.compressuser(input_load), file=fid2) @@ -350,11 +427,12 @@ def make_sea_level_mascon_shells(PROC, DREL, DSET, LMAX, RAD, # return list of output files return output_files + # PURPOSE: print a file log for the mascon harmonic calculation def output_log_file(input_arguments, output_files): # format: mascon_disc_run_2002-04-01_PID-70335.log TYPE = arguments.mascon_type.lower() - args = (TYPE,time.strftime('%Y-%m-%d',time.localtime()),os.getpid()) + args = (TYPE, time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'mascon_{0}_run_{1}_PID-{2:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -371,11 +449,12 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the mascon harmonic calculation def output_error_log_file(input_arguments): # format: failed_mascon_disc_run_2002-04-01_PID-70335.log TYPE = arguments.mascon_type.lower() - args = (TYPE,time.strftime('%Y-%m-%d',time.localtime()),os.getpid()) + args = (TYPE, time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'failed_mascon_{0}_run_{1}_PID-{2:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -391,91 +470,191 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Computes spherical harmonics for a set of mascon files. Creates a shell script for running sea level variation code. """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # different treatments of the body tide Love numbers of degree 2 # 0: Wahr (1981) and Wahr (1985) values from PREM # 1: Farrell (1972) values from Gutenberg-Bullen oceanic mantle model - parser.add_argument('--body','-b', - type=int, default=0, choices=[0,1], - help='Treatment of the body tide Love number') + parser.add_argument( + '--body', + '-b', + type=int, + default=0, + choices=[0, 1], + help='Treatment of the body tide Love number', + ) # different treatments of the fluid Love number of gravitational potential # 0: Han and Wahr (1989) fluid love number # 1: Munk and MacDonald (1960) secular love number # 2: Munk and MacDonald (1960) fluid love number # 3: Lambeck (1980) fluid love number - parser.add_argument('--fluid','-f', - type=int, default=0, choices=[0,1,2,3], - help='Treatment of the fluid Love number') + parser.add_argument( + '--fluid', + '-f', + type=int, + default=0, + choices=[0, 1, 2, 3], + help='Treatment of the fluid Love number', + ) # option for polar feedback - parser.add_argument('--polar-feedback', - default=False, action='store_true', - help='Include effects of polar feedback') + parser.add_argument( + '--polar-feedback', + default=False, + action='store_true', + help='Include effects of polar feedback', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -491,73 +670,124 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # mascon index file and parameters - parser.add_argument('--mascon-file', + parser.add_argument( + '--mascon-file', type=pathlib.Path, - help='Index file of mascons spherical harmonics') - parser.add_argument('--coordinate-file', + help='Index file of mascons spherical harmonics', + ) + parser.add_argument( + '--coordinate-file', type=pathlib.Path, required=True, - help='File with spatial coordinates of mascon centers') + help='File with spatial coordinates of mascon centers', + ) # number of header lines to skip in coordinate file - parser.add_argument('--header','-H', - type=int, default=0, - help='Number of header lines to skip in coordinate file') + parser.add_argument( + '--header', + '-H', + type=int, + default=0, + help='Number of header lines to skip in coordinate file', + ) # input load type (DISC, POINT or CAP) - parser.add_argument('--mascon-type','-T', - type=str.upper, default='CAP', choices=['DISC','POINT','CAP'], - help='Input load type') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--mascon-type', + '-T', + type=str.upper, + default='CAP', + choices=['DISC', 'POINT', 'CAP'], + help='Input load type', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # sea level fingerprint parameters - parser.add_argument('--iteration','-I', - type=int, default=1, - help='Sea level fingerprint iteration') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--iteration', + '-I', + type=int, + default=1, + help='Sea level fingerprint iteration', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # land-sea mask for redistributing mascon mass and land water flux - parser.add_argument('--mask', + parser.add_argument( + '--mask', type=pathlib.Path, - help='Land-sea mask for redistributing mascon mass and land water flux') + help='Land-sea mask for redistributing mascon mass and land water flux', + ) # Output log file for each job in forms # mascon_disc_run_2002-04-01_PID-00000.log # failed_mascon_disc_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -597,18 +827,20 @@ def main(): LANDMASK=args.mask, OUTPUT_DIRECTORY=args.output_directory, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/make_sea_level_shells.py b/scripts/make_sea_level_shells.py index abe24fed..09124453 100755 --- a/scripts/make_sea_level_shells.py +++ b/scripts/make_sea_level_shells.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" make_sea_level_shells.py Written by Tyler Sutterley (05/2023) @@ -63,6 +63,7 @@ Updated 11/2018: can vary the land-sea mask Written 09/2018 """ + from __future__ import print_function import sys @@ -74,6 +75,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -83,8 +85,10 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: create a shell script for running the sea level program -def make_sea_level_shells(input_file, +def make_sea_level_shells( + input_file, DATE=False, LOVE_NUMBERS=0, BODY_TIDE_LOVE=0, @@ -97,8 +101,8 @@ def make_sea_level_shells(input_file, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # create output directory if currently non-existent OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -134,8 +138,9 @@ def make_sea_level_shells(input_file, verbosity_flag = ' --verbose' if VERBOSE else '' # input spherical harmonic data and get GRACE/GRACE-FO months - Ylms = gravtk.harmonics().from_index(input_file, format=DATAFORM, - date=True, sort=True) + Ylms = gravtk.harmonics().from_index( + input_file, format=DATAFORM, date=True, sort=True + ) # create sea level shell script f1 = f'{FILE_PREFIX}INDEX_L{EXPANSION:d}.sh' @@ -146,110 +151,176 @@ def make_sea_level_shells(input_file, # output file format file_format = '{0}L{1:d}_{2:03d}.{3}' # formatting string for each line in the shell script - shell_format='{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11} --mode {12} -V {13} {14}' + shell_format = ( + '{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11} --mode {12} -V {13} {14}' + ) # for each grace month and input file for grace_month in sorted(Ylms.month): # input file input_Ylms = Ylms.subset(grace_month) - input_load, = input_Ylms.filename + (input_load,) = input_Ylms.filename # output file - args = (FILE_PREFIX,EXPANSION,grace_month,suffix[DATAFORM]) + args = (FILE_PREFIX, EXPANSION, grace_month, suffix[DATAFORM]) output_slf = OUTPUT_DIRECTORY.joinpath(file_format.format(*args)) # print shell script commands - args = (child_program, expansion_flag, polar_flag, love_flag, - body_flag, fluid_flag, reference_flag, date_flag, mask_flag, - iter_flag, format_flag, verbosity_flag, oct(MODE), + args = ( + child_program, + expansion_flag, + polar_flag, + love_flag, + body_flag, + fluid_flag, + reference_flag, + date_flag, + mask_flag, + iter_flag, + format_flag, + verbosity_flag, + oct(MODE), Ylms.compressuser(input_load), - gravtk.spatial().compressuser(output_slf)) + gravtk.spatial().compressuser(output_slf), + ) print(shell_format.format(*args), file=fid) # close the shell script fid.close() # change the permissions mode of the shell script output_shell_script.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Creates a shell script for running sea level code """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('infile', + parser.add_argument( + 'infile', type=pathlib.Path, - help='Input index file with spherical harmonic data files') + help='Input index file with spherical harmonic data files', + ) # output working data directory - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for sea level files') - parser.add_argument('--file-prefix','-P', - type=str, - help='Prefix string for output files') - parser.add_argument('--date','-D', - default=False, action='store_true', - help='Model harmonics are a time series') + help='Output directory for sea level files', + ) + parser.add_argument( + '--file-prefix', '-P', type=str, help='Prefix string for output files' + ) + parser.add_argument( + '--date', + '-D', + default=False, + action='store_true', + help='Model harmonics are a time series', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # different treatments of the body tide Love numbers of degree 2 # 0: Wahr (1981) and Wahr (1985) values from PREM # 1: Farrell (1972) values from Gutenberg-Bullen oceanic mantle model - parser.add_argument('--body','-b', - type=int, default=0, choices=[0,1], - help='Treatment of the body tide Love number') + parser.add_argument( + '--body', + '-b', + type=int, + default=0, + choices=[0, 1], + help='Treatment of the body tide Love number', + ) # different treatments of the fluid Love number of gravitational potential # 0: Han and Wahr (1989) fluid love number # 1: Munk and MacDonald (1960) secular love number # 2: Munk and MacDonald (1960) fluid love number # 3: Lambeck (1980) fluid love number - parser.add_argument('--fluid','-f', - type=int, default=0, choices=[0,1,2,3], - help='Treatment of the fluid Love number') + parser.add_argument( + '--fluid', + '-f', + type=int, + default=0, + choices=[0, 1, 2, 3], + help='Treatment of the fluid Love number', + ) # option for polar feedback - parser.add_argument('--polar-feedback', - default=False, action='store_true', - help='Include effects of polar feedback') + parser.add_argument( + '--polar-feedback', + default=False, + action='store_true', + help='Include effects of polar feedback', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # sea level fingerprint parameters - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # land-sea mask for redistributing mascon mass and land water flux - parser.add_argument('--mask', + parser.add_argument( + '--mask', type=pathlib.Path, - help='Land-sea mask for redistributing mascon mass and land water flux') + help='Land-sea mask for redistributing mascon mass and land water flux', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -259,7 +330,8 @@ def main(): try: info(args) # run make_sea_level_shells algorithm with parameters - make_sea_level_shells(args.infile, + make_sea_level_shells( + args.infile, DATE=args.date, LOVE_NUMBERS=args.love, BODY_TIDE_LOVE=args.body, @@ -272,7 +344,8 @@ def main(): OUTPUT_DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -280,6 +353,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/mascon_reconstruct.py b/scripts/mascon_reconstruct.py index a63065e8..1063ebf9 100644 --- a/scripts/mascon_reconstruct.py +++ b/scripts/mascon_reconstruct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" mascon_reconstruct.py Written by Tyler Sutterley (05/2023) @@ -113,6 +113,7 @@ Updated 09/2014: Converted to function with main args Updated 05/2014 """ + from __future__ import print_function import sys @@ -125,6 +126,7 @@ import traceback import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -134,9 +136,13 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: Reconstruct spherical harmonic fields from the mascon # time series calculated in calc_mascon -def mascon_reconstruct(DSET, LMAX, RAD, +def mascon_reconstruct( + DSET, + LMAX, + RAD, START=None, END=None, MMAX=None, @@ -152,8 +158,8 @@ def mascon_reconstruct(DSET, LMAX, RAD, RECONSTRUCT_FILE=None, LANDMASK=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # create output directory if currently non-existent OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -187,8 +193,9 @@ def mascon_reconstruct(DSET, LMAX, RAD, file_format = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}_{9:03d}-{10:03d}.{11}' # read load love numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) # Average Density of the Earth [g/cm^3] @@ -198,8 +205,7 @@ def mascon_reconstruct(DSET, LMAX, RAD, # Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_MASCONS: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, - LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) ocean_str = '_OCN' else: # not distributing uniformly over ocean @@ -211,24 +217,25 @@ def mascon_reconstruct(DSET, LMAX, RAD, mascon_files = [l for l in f.read().splitlines() if parser.match(l)] # for each mascon file - for k,mascon_file in enumerate(mascon_files): + for k, mascon_file in enumerate(mascon_files): # read mascon spherical harmonics - Ylms = gravtk.harmonics().from_file(mascon_file, - format=DATAFORM, date=False) + Ylms = gravtk.harmonics().from_file( + mascon_file, format=DATAFORM, 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 + 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] + 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 + 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] + 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_name is the mascon file without directory or suffix @@ -240,25 +247,48 @@ def mascon_reconstruct(DSET, LMAX, RAD, # 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) + 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(OUTPUT_DIRECTORY.joinpath(file_input)) # convert mascon time-series from Gt to cmwe - mascon_sigma = 1e15*mascon_data_input[:,2]/total_area + 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) + 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]) + args = ( + mascon_name, + dset_str, + gia_str.upper(), + atm_str, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + START, + END, + suffix[DATAFORM], + ) output_file = OUTPUT_DIRECTORY.joinpath(file_format.format(*args)) # attributes for output files attributes = {} - attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # output harmonics to file mascon_Ylms.to_file(output_file, format=DATAFORM, **attributes) # print file name to index @@ -270,60 +300,98 @@ def mascon_reconstruct(DSET, LMAX, RAD, # change the permissions mode of the index file RECONSTRUCT_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( - description="""Calculates the equivalent spherical + description="""Calculates the equivalent spherical harmonics from a mascon time series """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -339,53 +407,85 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input data format for auxiliary files') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input data format for auxiliary files', + ) # mascon index file and parameters - parser.add_argument('--mascon-file', + parser.add_argument( + '--mascon-file', type=pathlib.Path, - help='Index file of mascons spherical harmonics') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + help='Index file of mascons spherical harmonics', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # mascon reconstruct parameters - parser.add_argument('--reconstruct-file', + parser.add_argument( + '--reconstruct-file', type=pathlib.Path, - help='Reconstructed mascon time series file') + help='Reconstructed mascon time series file', + ) # land-sea mask for redistributing mascon mass - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing mascon mass') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing mascon mass', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -414,7 +514,8 @@ def main(): RECONSTRUCT_FILE=args.reconstruct_file, LANDMASK=args.mask, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -422,6 +523,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/piecewise_grace_maps.py b/scripts/piecewise_grace_maps.py index a4ee0a3a..e3bbdafd 100755 --- a/scripts/piecewise_grace_maps.py +++ b/scripts/piecewise_grace_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" piecewise_grace_maps.py Written by Tyler Sutterley (07/2026) @@ -97,6 +97,7 @@ Updated 06/2015: added output_files for log files Written 09/2013 """ + from __future__ import print_function, division import sys @@ -109,6 +110,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -118,8 +120,11 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # program module to run with specified parameters -def piecewise_grace_maps(LMAX, RAD, +def piecewise_grace_maps( + LMAX, + RAD, START=None, END=None, BREAKPOINT=None, @@ -136,8 +141,8 @@ def piecewise_grace_maps(LMAX, RAD, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # create output directory if currently non-existent OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -168,42 +173,51 @@ def piecewise_grace_maps(LMAX, RAD, output_format = '{0}{1}_L{2:d}{3}{4}{5}_{6}{7}_{8:03d}-{9:03d}.{10}' # GRACE months to read - months = sorted(set(np.arange(START,END+1)) - set(MISSING)) + months = sorted(set(np.arange(START, END + 1)) - set(MISSING)) # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output Degree Interval - if (INTERVAL == 1): + if INTERVAL == 1: # (-180:180,90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2): + 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) - elif (INTERVAL == 3): + nlon = np.int64(360.0 / dlon) + nlat = np.int64(180.0 / dlat) + elif INTERVAL == 3: # non-global grid set with BOUNDS parameter - minlon,maxlon,minlat,maxlat = BOUNDS.copy() - lon = np.arange(minlon+dlon/2.0, maxlon+dlon/2.0, dlon) - lat = np.arange(maxlat-dlat/2.0, minlat-dlat/2.0, -dlat) + minlon, maxlon, minlat, maxlat = BOUNDS.copy() + lon = np.arange(minlon + dlon / 2.0, maxlon + dlon / 2.0, dlon) + lat = np.arange(maxlat - dlat / 2.0, minlat - dlat / 2.0, -dlat) nlon = len(lon) nlat = len(lat) # input data spatial object spatial_list = [] - for t,grace_month in enumerate(months): + for t, grace_month in enumerate(months): # input GRACE/GRACE-FO spatial file - fargs = (FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, grace_month, suffix) + fargs = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + grace_month, + suffix, + ) input_file = OUTPUT_DIRECTORY.joinpath(input_format.format(*fargs)) # read GRACE/GRACE-FO spatial file - if (DATAFORM == 'ascii'): - dinput = gravtk.spatial().from_ascii(input_file, - spacing=[dlon,dlat], nlon=nlon, nlat=nlat) - elif (DATAFORM == 'netCDF4'): + if DATAFORM == 'ascii': + dinput = gravtk.spatial().from_ascii( + input_file, spacing=[dlon, dlat], nlon=nlon, nlat=nlat + ) + elif DATAFORM == 'netCDF4': # netcdf (.nc) dinput = gravtk.spatial().from_netCDF4(input_file) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 (.H5) dinput = gravtk.spatial().from_HDF5(input_file) # append to spatial list @@ -217,7 +231,7 @@ def piecewise_grace_maps(LMAX, RAD, # find index of breakpoint within GRACE/GRACE-FO months if BREAKPOINT not in grid.month: raise ValueError(f'{BREAKPOINT} not found in GRACE/GRACE-FO months') - breakpoint_index, = np.flatnonzero(grid.month == BREAKPOINT) + (breakpoint_index,) = np.flatnonzero(grid.month == BREAKPOINT) # Setting output parameters coef_str = ['x0', 'px1', 'px1'] @@ -243,28 +257,29 @@ def piecewise_grace_maps(LMAX, RAD, # extra terms for tidal aliasing components or custom fits TERMS = [] term_index = [] - for i,c in enumerate(CYCLES): + for i, c in enumerate(CYCLES): # check if fitting with semi-annual or annual terms - if (c == 0.5): - coef_str.extend(['SS','SC']) + if c == 0.5: + coef_str.extend(['SS', 'SC']) amp_str.append('SEMI') amp_title['SEMI'] = 'Semi-Annual Amplitude' ph_title['SEMI'] = 'Semi-Annual Phase' fit_longname.extend(['Semi-Annual Sine', 'Semi-Annual Cosine']) - unit_suffix.extend(['','']) - elif (c == 1.0): - coef_str.extend(['AS','AC']) + unit_suffix.extend(['', '']) + elif c == 1.0: + coef_str.extend(['AS', 'AC']) amp_str.append('ANN') amp_title['ANN'] = 'Annual Amplitude' ph_title['ANN'] = 'Annual Phase' fit_longname.extend(['Annual Sine', 'Annual Cosine']) - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # check if fitting with tidal aliasing terms - for t,period in tidal_aliasing.items(): - if np.isclose(c, (period/365.25)): + for t, period in tidal_aliasing.items(): + if np.isclose(c, (period / 365.25)): # terms for tidal aliasing during GRACE and GRACE-FO periods - TERMS.extend(gravtk.time_series.aliasing_terms(grid.time, - period=period)) + TERMS.extend( + gravtk.time_series.aliasing_terms(grid.time, period=period) + ) # labels for tidal aliasing during GRACE period coef_str.extend([f'{t}SGRC', f'{t}CGRC']) amp_str.append(f'{t}GRC') @@ -272,7 +287,7 @@ def piecewise_grace_maps(LMAX, RAD, ph_title[f'{t}GRC'] = f'{t} Tidal Alias (GRACE) Phase' fit_longname.append(f'{t} Tidal Alias (GRACE) Sine') fit_longname.append(f'{t} Tidal Alias (GRACE) Cosine') - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # labels for tidal aliasing during GRACE-FO period coef_str.extend([f'{t}SGFO', f'{t}CGFO']) amp_str.append(f'{t}GFO') @@ -280,7 +295,7 @@ def piecewise_grace_maps(LMAX, RAD, ph_title[f'{t}GFO'] = f'{t} Tidal Alias (GRACE-FO) Phase' fit_longname.append(f'{t} Tidal Alias (GRACE-FO) Sine') fit_longname.append(f'{t} Tidal Alias (GRACE-FO) Cosine') - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # index to remove the original tidal aliasing term term_index.append(i) # remove the original tidal aliasing terms @@ -288,7 +303,7 @@ def piecewise_grace_maps(LMAX, RAD, # Fitting seasonal components ncomp = len(coef_str) - ncycles = 2*len(CYCLES) + len(TERMS) + ncycles = 2 * len(CYCLES) + len(TERMS) # output start and end months with breakpoint output_start = np.zeros((ncomp), dtype=int) + START output_end = np.zeros((ncomp), dtype=int) + END @@ -303,44 +318,60 @@ def piecewise_grace_maps(LMAX, RAD, out = dinput.zeros_like() out.data = np.zeros((nlat, nlon, ncomp)) out.error = np.zeros((nlat, nlon, ncomp)) - out.mask = np.ones((nlat, nlon, ncomp),dtype=bool) + out.mask = np.ones((nlat, nlon, ncomp), dtype=bool) # Fit Significance FS = {} # SSE: Sum of Squares Error # AIC: Akaike information criterion # BIC: Bayesian information criterion # R2Adj: Adjusted Coefficient of Determination - for key in ['SSE','AIC','BIC','R2Adj']: + for key in ['SSE', 'AIC', 'BIC', 'R2Adj']: FS[key] = dinput.zeros_like() # calculate the regression coefficients and fit significance for i in range(nlat): for j in range(nlon): # Calculating the regression coefficients - tsbeta = gravtk.time_series.piecewise(grid.time, grid.data[i,j,:], - BREAKPOINT=breakpoint_index, CYCLES=CYCLES, TERMS=TERMS, - CONF=CONF) + tsbeta = gravtk.time_series.piecewise( + grid.time, + grid.data[i, j, :], + BREAKPOINT=breakpoint_index, + CYCLES=CYCLES, + TERMS=TERMS, + CONF=CONF, + ) # save regression components for k in range(0, ncomp): - out.data[i,j,k] = tsbeta['beta'][k] - out.error[i,j,k] = tsbeta['error'][k] - out.mask[i,j,k] = False + out.data[i, j, k] = tsbeta['beta'][k] + out.error[i, j, k] = tsbeta['error'][k] + out.mask[i, j, k] = False # Fit significance terms # Degrees of Freedom nu = tsbeta['DOF'] # Converting Mean Square Error to Sum of Squares Error - FS['SSE'].data[i,j] = tsbeta['MSE']*nu - FS['AIC'].data[i,j] = tsbeta['AIC'] - FS['BIC'].data[i,j] = tsbeta['BIC'] - FS['R2Adj'].data[i,j] = tsbeta['R2Adj'] + FS['SSE'].data[i, j] = tsbeta['MSE'] * nu + FS['AIC'].data[i, j] = tsbeta['AIC'] + FS['BIC'].data[i, j] = tsbeta['BIC'] + FS['R2Adj'].data[i, j] = tsbeta['R2Adj'] # list of output files output_files = [] # Output spatial files - for i in range(0,ncomp): + for i in range(0, ncomp): # output spatial file name - f1 = (FILE_PREFIX, units, LMAX, order_str, gw_str, ds_str, - coef_str[i], '', output_start[i], output_end[i], suffix) + f1 = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + coef_str[i], + '', + output_start[i], + output_end[i], + suffix, + ) file1 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f1)) # full attributes UNITS_TITLE = f'{units_name}{unit_suffix[i]}' @@ -348,40 +379,70 @@ def piecewise_grace_maps(LMAX, RAD, FILE_TITLE = f'GRACE/GRACE-FO_Spatial_Data_{fit_longname[i]}' # output regression fit to file output = out.index(i, date=False) - output_data(output, FILENAME=file1, DATAFORM=DATAFORM, - UNITS=UNITS_TITLE, LONGNAME=LONGNAME, TITLE=FILE_TITLE, - CONF=CONF, VERBOSE=VERBOSE, MODE=MODE) + output_data( + output, + FILENAME=file1, + DATAFORM=DATAFORM, + UNITS=UNITS_TITLE, + LONGNAME=LONGNAME, + TITLE=FILE_TITLE, + CONF=CONF, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add output files to list object output_files.append(file1) # if fitting coefficients with cyclical components # output amplitude and phase of cyclical components - for i,flag in enumerate(amp_str): + for i, flag in enumerate(amp_str): # Indice pointing to the cyclical components - j = 3 + 2*i + j = 3 + 2 * i # Allocating memory for output amplitude and phase amp = dinput.zeros_like() ph = dinput.zeros_like() # calculating amplitude and phase of spatial field - amp.data,ph.data = gravtk.time_series.amplitude( - out.data[:,:,j], out.data[:,:,j+1] + amp.data, ph.data = gravtk.time_series.amplitude( + out.data[:, :, j], out.data[:, :, j + 1] ) # convert phase from -180:180 to 0:360 ph.data = np.where(ph.data < 0, ph.data + 360.0, ph.data) # Amplitude Error - comp1 = out.error[:,:,j]*out.data[:,:,j]/amp.data - comp2 = out.error[:,:,j+1]*out.data[:,:,j+1]/amp.data + comp1 = out.error[:, :, j] * out.data[:, :, j] / amp.data + comp2 = out.error[:, :, j + 1] * out.data[:, :, j + 1] / amp.data amp.error = np.hypot(comp1, comp2) # Phase Error (degrees) - comp1 = out.error[:,:,j]*out.data[:,:,j+1]/(amp.data**2) - comp2 = out.error[:,:,j+1]*out.data[:,:,j]/(amp.data**2) + comp1 = out.error[:, :, j] * out.data[:, :, j + 1] / (amp.data**2) + comp2 = out.error[:, :, j + 1] * out.data[:, :, j] / (amp.data**2) ph.error = np.degrees(np.hypot(comp1, comp2)) # output file names for amplitude, phase and errors - f2 = (FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, flag, '', START, END, suffix) - f3 = (FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, flag,'_PHASE', START, END, suffix) + f2 = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + flag, + '', + START, + END, + suffix, + ) + f3 = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + flag, + '_PHASE', + START, + END, + suffix, + ) file2 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f2)) file3 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f3)) # full attributes @@ -391,12 +452,28 @@ def piecewise_grace_maps(LMAX, RAD, AMP_TITLE = f'GRACE/GRACE-FO_Spatial_Data_{amp_title[flag]}' PH_TITLE = f'GRACE/GRACE-FO_Spatial_Data_{ph_title[flag]}' # Output seasonal amplitude and phase to files - output_data(amp, FILENAME=file2, DATAFORM=DATAFORM, - UNITS=AMP_UNITS, LONGNAME=LONGNAME, TITLE=AMP_TITLE, - CONF=CONF, VERBOSE=VERBOSE, MODE=MODE) - output_data(ph, FILENAME=file3, DATAFORM=DATAFORM, - UNITS=PH_UNITS, LONGNAME='Phase', TITLE=PH_TITLE, - CONF=CONF, VERBOSE=VERBOSE, MODE=MODE) + output_data( + amp, + FILENAME=file2, + DATAFORM=DATAFORM, + UNITS=AMP_UNITS, + LONGNAME=LONGNAME, + TITLE=AMP_TITLE, + CONF=CONF, + VERBOSE=VERBOSE, + MODE=MODE, + ) + output_data( + ph, + FILENAME=file3, + DATAFORM=DATAFORM, + UNITS=PH_UNITS, + LONGNAME='Phase', + TITLE=PH_TITLE, + CONF=CONF, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add output files to list object output_files.append(file2) output_files.append(file3) @@ -408,27 +485,55 @@ def piecewise_grace_maps(LMAX, RAD, signif_longname['BIC'] = 'Bayesian information criterion' signif_longname['R2Adj'] = 'Adjusted Coefficient of Determination' # for each fit significance term - for key,fs in FS.items(): + for key, fs in FS.items(): # output file names for fit significance signif_str = f'{key}_' - f4 = (FILE_PREFIX, units, LMAX, order_str, gw_str, ds_str, - signif_str, 'px1', START, END, suffix) + f4 = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + signif_str, + 'px1', + START, + END, + suffix, + ) file4 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f4)) # full attributes LONGNAME = signif_longname[key] # output fit significance to file - output_data(fs, FILENAME=file4, DATAFORM=DATAFORM, - UNITS=key, LONGNAME=LONGNAME, TITLE=nu, - VERBOSE=VERBOSE, MODE=MODE) + output_data( + fs, + FILENAME=file4, + DATAFORM=DATAFORM, + UNITS=key, + LONGNAME=LONGNAME, + TITLE=nu, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add output files to list object output_files.append(file4) # return the list of output files return output_files + # PURPOSE: wrapper function for outputting data to file -def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, - LONGNAME=None, TITLE=None, CONF=0, VERBOSE=0, MODE=0o775): +def output_data( + data, + FILENAME=None, + DATAFORM=None, + UNITS=None, + LONGNAME=None, + TITLE=None, + CONF=0, + VERBOSE=0, + MODE=0o775, +): # field mapping for output regression data field_mapping = {} field_mapping['lat'] = 'lat' @@ -453,30 +558,43 @@ def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, attributes['error']['description'] = 'Uncertainty_in_model_fit' attributes['error']['long_name'] = LONGNAME attributes['error']['units'] = UNITS - attributes['error']['confidence'] = 100*CONF + attributes['error']['confidence'] = 100 * CONF # output global attributes REFERENCE = f'Output from {pathlib.Path(sys.argv[0]).name}' # write to output file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) data.to_ascii(FILENAME, date=False, verbose=VERBOSE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netcdf (.nc) - data.to_netCDF4(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) - elif (DATAFORM == 'HDF5'): + data.to_netCDF4( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) - data.to_HDF5(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) + data.to_HDF5( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) # change the permissions mode of the output file FILENAME.chmod(mode=MODE) + # PURPOSE: print a file log for the GRACE/GRACE-FO regression def output_log_file(input_arguments, output_files): # format: GRACE_processing_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_processing_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -493,10 +611,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE/GRACE-FO regression def output_error_log_file(input_arguments): # format: GRACE_processing_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_processing_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -512,102 +631,220 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Reads in GRACE/GRACE-FO spatial files and calculates the trends at each grid point following an input regression model """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + help='Output directory for spatial files', + ) + parser.add_argument( + '--file-prefix', + '-P', type=str, - help='Prefix string for input and output files') + help='Prefix string for input and output files', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series regression') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month for time series regression') - parser.add_argument('--breakpoint','-B', - type=int, default=129, - help='Breakpoint GRACE/GRACE-FO month for piecewise regression') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series regression', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=232, + help='Ending GRACE/GRACE-FO month for time series regression', + ) + parser.add_argument( + '--breakpoint', + '-B', + type=int, + default=129, + help='Breakpoint GRACE/GRACE-FO month for piecewise regression', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # output units - parser.add_argument('--units','-U', - type=int, default=1, choices=[1,2,3,4,5], - help='Output units') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[1, 2, 3, 4, 5], + help='Output units', + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2,3], - help=('Output grid interval ' - '(1: global, 2: centered global, 3: non-global)')) - parser.add_argument('--bounds', - type=float, nargs=4, metavar=('lon_min','lon_max','lat_min','lat_max'), - help='Bounding box for non-global grid') + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2, 3], + help=( + 'Output grid interval ' + '(1: global, 2: centered global, 3: non-global)' + ), + ) + parser.add_argument( + '--bounds', + type=float, + nargs=4, + metavar=('lon_min', 'lon_max', 'lat_min', 'lat_max'), + help='Bounding box for non-global grid', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # regression parameters # regression fit cyclical terms - parser.add_argument('--cycles', - type=float, default=[0.5,1.0,161.0/365.25], nargs='+', - help='Regression fit cyclical terms') + parser.add_argument( + '--cycles', + type=float, + default=[0.5, 1.0, 161.0 / 365.25], + nargs='+', + help='Regression fit cyclical terms', + ) # Output log file for each job in forms # GRACE_processing_run_2002-04-01_PID-00000.log # GRACE_processing_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -636,18 +873,20 @@ def main(): OUTPUT_DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/plot_SLR_azimuthal.py b/scripts/plot_SLR_azimuthal.py index 22431604..674ce748 100644 --- a/scripts/plot_SLR_azimuthal.py +++ b/scripts/plot_SLR_azimuthal.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_SLR_azimuthal.py (05/2023) Plots degree-two order-one harmonics from GRACE/GRACE-FO and SLR Compares with a climatology calculated using GRACE months @@ -26,6 +26,7 @@ Updated 11/2021: add GSFC low-degree harmonics Written 05/2021 """ + from __future__ import print_function, division import inspect @@ -39,26 +40,42 @@ try: import matplotlib import matplotlib.pyplot as plt + matplotlib.rcParams['mathtext.default'] = 'regular' matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] import matplotlib.offsetbox except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) # current file path for the child programs filename = inspect.getframeinfo(inspect.currentframe()).filename filepath = pathlib.Path(filename).absolute().parent + # plot SLR azimuthal dependence coefficients def plot_SLR_azimuthal(base_dir, PROC, DREL, START_MON, END_MON, MISSING): # GRACE/GRACE-FO mission gap - GAP = [187,188,189,190,191,192,193,194,195,196,197] + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] missing = sorted(set(MISSING) | set(GAP)) # CSR GRACE/GRACE-FO monthly harmonics - grace_Ylms = gravtk.grace_input_months(base_dir, PROC, DREL, 'GSM', 5, - START_MON, END_MON, missing, None, 0, MMAX=None, MODEL_DEG1=False, - ATM=False, POLE_TIDE=False, SLR_C30='N') + grace_Ylms = gravtk.grace_input_months( + base_dir, + PROC, + DREL, + 'GSM', + 5, + START_MON, + END_MON, + missing, + None, + 0, + MMAX=None, + MODEL_DEG1=False, + ATM=False, + POLE_TIDE=False, + SLR_C30='N', + ) GRACE = dict(date=grace_Ylms['time'], month=grace_Ylms['month']) grace_Ylms = gravtk.harmonics().from_dict(grace_Ylms) grace_Ylms.mean(apply=True) @@ -67,25 +84,27 @@ def plot_SLR_azimuthal(base_dir, PROC, DREL, START_MON, END_MON, MISSING): SLR_file = base_dir.joinpath('C21_S21_RL06.txt') CSR_CS2 = gravtk.SLR.CS2(SLR_file) # GSFC coefficients - SLR_file = base_dir.joinpath('gsfc_slr_5x5c61s61.txt',DATE=GRACE['date']) + SLR_file = base_dir.joinpath('gsfc_slr_5x5c61s61.txt', DATE=GRACE['date']) GSFC_CS2 = gravtk.SLR.CS2(SLR_file) # GFZ GravIS coefficients - SLR_file = base_dir.joinpath('GRAVIS-2B_GFZOP_GRACE+SLR_LOW_DEGREES_0002.dat') + SLR_file = base_dir.joinpath( + 'GRAVIS-2B_GFZOP_GRACE+SLR_LOW_DEGREES_0002.dat' + ) GFZ_CS2 = gravtk.SLR.CS2(SLR_file) # calculate common months - month = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + month = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) common_months = np.copy(month) - for v in [GRACE,CSR_CS2,GFZ_CS2]: + for v in [GRACE, CSR_CS2, GFZ_CS2]: common_months = sorted(set(common_months) & set(v['month'])) # read arrays of kl, hl, and ll Love Numbers - love_numbers_file = gravtk.utilities.get_data_path(['data','love_numbers']) - hl,kl,ll = gravtk.read_love_numbers(love_numbers_file, FORMAT='tuple') + love_numbers_file = gravtk.utilities.get_data_path(['data', 'love_numbers']) + hl, kl, ll = gravtk.read_love_numbers(love_numbers_file, FORMAT='tuple') # Earth Parameters - factors = gravtk.units(lmax=5).harmonic(hl,kl,ll) - rho_e = factors.rho_e# Average Density of the Earth [g/cm^3] - rad_e = factors.rad_e# Average Radius of the Earth [cm] + factors = gravtk.units(lmax=5).harmonic(hl, kl, ll) + rho_e = factors.rho_e # Average Density of the Earth [g/cm^3] + rad_e = factors.rad_e # Average Radius of the Earth [cm] # create a separate plot with azimuthal dependence files l = 2 @@ -94,91 +113,136 @@ def plot_SLR_azimuthal(base_dir, PROC, DREL, START_MON, END_MON, MISSING): # create output plot ax1 = {} ax2 = {} - f1, (ax1['C21'],ax1['S21']) = plt.subplots(num=1,nrows=2, - sharex=True,figsize=(6,4)) - f2, (ax2['C21'],ax2['S21']) = plt.subplots(num=2,nrows=2, - sharex=True,figsize=(6,4)) - plot_colors = ['mediumseagreen','0.5','darkorchid','dodgerblue','darkorange'] - plot_labels = ['CSR GRACE/GRACE-FO','CSR GRACE Climatology', - 'CSR SLR','GSFC SLR','GFZ GravIS'] - plot_zorder = [0,3,1,2] - fig_text = {'C21':'a)','S21':'b)'} - plot_ylabel = {'C21':r'C$\mathregular{_{21}}$','S21':r'S$\mathregular{_{21}}$'} - CSR = dict(time=CSR_CS2['time'],month=CSR_CS2['month']) - GSFC = dict(time=GSFC_CS2['time'],month=GSFC_CS2['month']) - GFZ = dict(time=GFZ_CS2['time'],month=GFZ_CS2['month']) - for cs,ax in ax1.items(): + f1, (ax1['C21'], ax1['S21']) = plt.subplots( + num=1, nrows=2, sharex=True, figsize=(6, 4) + ) + f2, (ax2['C21'], ax2['S21']) = plt.subplots( + num=2, nrows=2, sharex=True, figsize=(6, 4) + ) + plot_colors = [ + 'mediumseagreen', + '0.5', + 'darkorchid', + 'dodgerblue', + 'darkorange', + ] + plot_labels = [ + 'CSR GRACE/GRACE-FO', + 'CSR GRACE Climatology', + 'CSR SLR', + 'GSFC SLR', + 'GFZ GravIS', + ] + plot_zorder = [0, 3, 1, 2] + fig_text = {'C21': 'a)', 'S21': 'b)'} + plot_ylabel = { + 'C21': r'C$\mathregular{_{21}}$', + 'S21': r'S$\mathregular{_{21}}$', + } + CSR = dict(time=CSR_CS2['time'], month=CSR_CS2['month']) + GSFC = dict(time=GSFC_CS2['time'], month=GSFC_CS2['month']) + GFZ = dict(time=GFZ_CS2['time'], month=GFZ_CS2['month']) + for cs, ax in ax1.items(): # GRACE data for zonal harmonic - if (cs == 'C21'): - GRACE['data'] = grace_Ylms.clm[2,1,:].copy() + if cs == 'C21': + GRACE['data'] = grace_Ylms.clm[2, 1, :].copy() # SLR azimuthal dependence data CSR['data'] = CSR_CS2['C2m'].copy() GSFC['data'] = GSFC_CS2['C2m'].copy() GFZ['data'] = GFZ_CS2['C2m'].copy() - elif (cs == 'S21'): - GRACE['data'] = grace_Ylms.slm[2,1,:].copy() + elif cs == 'S21': + GRACE['data'] = grace_Ylms.slm[2, 1, :].copy() # SLR azimuthal dependence data CSR['data'] = CSR_CS2['S2m'].copy() GSFC['data'] = GSFC_CS2['S2m'].copy() GFZ['data'] = GFZ_CS2['S2m'].copy() # calculate GRACE climatology for "good months" CLIMATE = {} - YY = np.arange(2002+1./24.,2022,1./12.).astype(np.int64) + YY = np.arange(2002 + 1.0 / 24.0, 2022, 1.0 / 12.0).astype(np.int64) CLIMATE['month'] = 1 + np.arange(len(YY)) - MM = 1 + ((CLIMATE['month']-1) % 12) + MM = 1 + ((CLIMATE['month'] - 1) % 12) CLIMATE['date'] = gravtk.time.convert_calendar_decimal(YY, MM) - ii, = np.nonzero((GRACE['month'] >= 13) & (GRACE['month'] <= 176)) - CLIMATE['data'] = regress_model(GRACE['date'][ii], GRACE['data'][ii], - CLIMATE['date'], ORDER=2, CYCLES=[0.25,0.5,1.0,2.0,4.0,5.0], - RELATIVE=GRACE['date'][0]) + (ii,) = np.nonzero((GRACE['month'] >= 13) & (GRACE['month'] <= 176)) + CLIMATE['data'] = regress_model( + GRACE['date'][ii], + GRACE['data'][ii], + CLIMATE['date'], + ORDER=2, + CYCLES=[0.25, 0.5, 1.0, 2.0, 4.0, 5.0], + RELATIVE=GRACE['date'][0], + ) # plot items - plot_items = [GRACE,CLIMATE,CSR,GSFC,GFZ] - for i,v in enumerate(plot_items): - ii = [i for i,m in enumerate(v['month']) if m in common_months] + plot_items = [GRACE, CLIMATE, CSR, GSFC, GFZ] + for i, v in enumerate(plot_items): + ii = [i for i, m in enumerate(v['month']) if m in common_months] # create a time series with nans for missing months - tdec = np.full_like(month,np.nan,dtype=np.float64) - cmwe = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): + tdec = np.full_like(month, np.nan, dtype=np.float64) + cmwe = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): valid = np.count_nonzero(v['month'] == m) if valid: - mm, = np.nonzero(CLIMATE['month'] == m) + (mm,) = np.nonzero(CLIMATE['month'] == m) tdec[d] = CLIMATE['date'][mm] - mm, = np.nonzero(v['month'] == m) + (mm,) = np.nonzero(v['month'] == m) # tdec[d] = v['date'][mm] # cm w.e.: centimeters water equivalent - cmwe[d] = dfactor*(v['data'][mm]-v['data'][ii].mean()) + cmwe[d] = dfactor * (v['data'][mm] - v['data'][ii].mean()) # plot all dates - ax.plot(tdec, cmwe, zorder=plot_zorder[i], - color=plot_colors[i], label=plot_labels[i]) - ax2[cs].plot(tdec, cmwe, zorder=plot_zorder[i], - color=plot_colors[i], label=plot_labels[i]) + ax.plot( + tdec, + cmwe, + zorder=plot_zorder[i], + color=plot_colors[i], + label=plot_labels[i], + ) + ax2[cs].plot( + tdec, + cmwe, + zorder=plot_zorder[i], + color=plot_colors[i], + label=plot_labels[i], + ) # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9,day=3,hour=12,minute=12) - ax.axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) - ax2[cs].axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax.axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) + ax2[cs].axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(GRACE['month'] == 186) - kk, = np.flatnonzero(GRACE['month'] == 198) + (jj,) = np.flatnonzero(GRACE['month'] == 186) + (kk,) = np.flatnonzero(GRACE['month'] == 198) # ax.axvline(GRACE['time'][jj],color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) # ax.axvline(GRACE['time'][kk],color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) - vs = ax.axvspan(GRACE['date'][jj],GRACE['date'][kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) - vs = ax2[cs].axvspan(GRACE['date'][jj],GRACE['date'][kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) + vs = ax.axvspan( + GRACE['date'][jj], + GRACE['date'][kk], + color='0.5', + ls='dashed', + alpha=0.15, + ) + vs._dashes = (6, 3) + vs = ax2[cs].axvspan( + GRACE['date'][jj], + GRACE['date'][kk], + color='0.5', + ls='dashed', + alpha=0.15, + ) + vs._dashes = (6, 3) # set ticks - xmax = 2002 + (END_MON + 1.0)/12.0 + xmax = 2002 + (END_MON + 1.0) / 12.0 # set limits for first plot - major_ticks = np.arange(2010,xmax,2) + major_ticks = np.arange(2010, xmax, 2) minor_ticks = sorted(set(np.arange(2002, xmax, 1)) - set(major_ticks)) ax.xaxis.set_ticks(minor_ticks, minor=True) - ax.set_xlim(2010,xmax) + ax.set_xlim(2010, xmax) # set limits for second plot - major_ticks = np.arange(2002,xmax,2) + major_ticks = np.arange(2002, xmax, 2) minor_ticks = sorted(set(np.arange(2002, xmax, 1)) - set(major_ticks)) ax2[cs].xaxis.set_ticks(major_ticks) ax2[cs].xaxis.set_ticks(minor_ticks, minor=True) - ax2[cs].set_xlim(2001.75,xmax) + ax2[cs].set_xlim(2001.75, xmax) # add labels ax.set_ylabel('{0} [cm]'.format(cs)) ax2[cs].set_ylabel('{0} [cm]'.format(cs)) @@ -191,30 +255,38 @@ def plot_SLR_azimuthal(base_dir, PROC, DREL, START_MON, END_MON, MISSING): ax.xaxis.get_major_formatter().set_useOffset(False) ax2[cs].xaxis.get_major_formatter().set_useOffset(False) # add labels to the first plot - at = matplotlib.offsetbox.AnchoredText(fig_text[cs], - prop=dict(size=14,weight='bold'), - pad=0, frameon=False, loc=2) + at = matplotlib.offsetbox.AnchoredText( + fig_text[cs], + prop=dict(size=14, weight='bold'), + pad=0, + frameon=False, + loc=2, + ) ax.add_artist(at) # add labels to the second plot - at = matplotlib.offsetbox.AnchoredText(fig_text[cs], - prop=dict(size=14,weight='bold'), - pad=0, frameon=False, loc=2) + at = matplotlib.offsetbox.AnchoredText( + fig_text[cs], + prop=dict(size=14, weight='bold'), + pad=0, + frameon=False, + loc=2, + ) ax2[cs].add_artist(at) # set y limits for each axis in the first plot - ax1['C21'].set_ylim(-1.6,1.1) - ax1['S21'].set_ylim(-1.1,1.6) - ax1['C21'].yaxis.set_ticks([-1,0,1]) - ax1['S21'].yaxis.set_ticks([-1,0,1]) + ax1['C21'].set_ylim(-1.6, 1.1) + ax1['S21'].set_ylim(-1.1, 1.6) + ax1['C21'].yaxis.set_ticks([-1, 0, 1]) + ax1['S21'].yaxis.set_ticks([-1, 0, 1]) # set y limits for each axis in the second plot - ax2['C21'].set_ylim(-2.1,2.1) - ax2['S21'].set_ylim(-1.6,1.6) + ax2['C21'].set_ylim(-2.1, 2.1) + ax2['S21'].set_ylim(-1.6, 1.6) # add label and legend ax1['S21'].set_xlabel('Time [Yr]') ax2['S21'].set_xlabel('Time [Yr]') # add legend to the first plot - lgd = ax1['C21'].legend(loc=1,frameon=True,ncol=2) + lgd = ax1['C21'].legend(loc=1, frameon=True, ncol=2) # set width, color and style of lines lgd.get_frame().set_boxstyle('square,pad=0.01') lgd.get_frame().set_edgecolor('white') @@ -222,7 +294,7 @@ def plot_SLR_azimuthal(base_dir, PROC, DREL, START_MON, END_MON, MISSING): for line in lgd.get_lines(): line.set_linewidth(6) # add legend to the second plot - lgd = ax2['C21'].legend(loc=1,frameon=True,ncol=2) + lgd = ax2['C21'].legend(loc=1, frameon=True, ncol=2) # set width, color and style of lines lgd.get_frame().set_boxstyle('square,pad=0.1') lgd.get_frame().set_edgecolor('white') @@ -231,11 +303,15 @@ def plot_SLR_azimuthal(base_dir, PROC, DREL, START_MON, END_MON, MISSING): line.set_linewidth(6) # adjust the plot and save to file - f1.subplots_adjust(left=0.08,right=0.97,bottom=0.05,top=0.99,hspace=0.06) + f1.subplots_adjust( + left=0.08, right=0.97, bottom=0.05, top=0.99, hspace=0.06 + ) figurefile = filepath.joinpath('fs03ab.pdf') f1.savefig(figurefile, format='pdf') # adjust the plot and save to file - f2.subplots_adjust(left=0.08,right=0.97,bottom=0.05,top=0.99,hspace=0.06) + f2.subplots_adjust( + left=0.08, right=0.97, bottom=0.05, top=0.99, hspace=0.06 + ) figurefile = filepath.joinpath('fs03ab_all.pdf') f2.savefig(figurefile, format='pdf') # close everything @@ -243,6 +319,7 @@ def plot_SLR_azimuthal(base_dir, PROC, DREL, START_MON, END_MON, MISSING): plt.clf() plt.close() + # PURPOSE: calculate a regression model for extrapolating values def regress_model(t_in, d_in, t_out, ORDER=2, CYCLES=None, RELATIVE=None): # remove singleton dimensions @@ -250,26 +327,27 @@ def regress_model(t_in, d_in, t_out, ORDER=2, CYCLES=None, RELATIVE=None): d_in = np.squeeze(d_in) t_out = np.squeeze(t_out) # check dimensions of output - if (np.ndim(t_out) == 0): + if np.ndim(t_out) == 0: t_out = np.array([t_out]) # CREATING DESIGN MATRIX FOR REGRESSION DMAT = [] MMAT = [] # add polynomial orders (0=constant, 1=linear, 2=quadratic) - for o in range(ORDER+1): - DMAT.append((t_in-RELATIVE)**o) - MMAT.append((t_out-RELATIVE)**o) + for o in range(ORDER + 1): + DMAT.append((t_in - RELATIVE) ** o) + MMAT.append((t_out - RELATIVE) ** o) # add cyclical terms (0.5=semi-annual, 1=annual) for c in CYCLES: - DMAT.append(np.sin(2.0*np.pi*t_in/np.float64(c))) - DMAT.append(np.cos(2.0*np.pi*t_in/np.float64(c))) - MMAT.append(np.sin(2.0*np.pi*t_out/np.float64(c))) - MMAT.append(np.cos(2.0*np.pi*t_out/np.float64(c))) + DMAT.append(np.sin(2.0 * np.pi * t_in / np.float64(c))) + DMAT.append(np.cos(2.0 * np.pi * t_in / np.float64(c))) + MMAT.append(np.sin(2.0 * np.pi * t_out / np.float64(c))) + MMAT.append(np.cos(2.0 * np.pi * t_out / np.float64(c))) # Calculating Least-Squares Coefficients # Standard Least-Squares fitting (the [0] denotes coefficients output) beta_mat = np.linalg.lstsq(np.transpose(DMAT), d_in, rcond=-1)[0] # return modeled time-series - return np.dot(np.transpose(MMAT),beta_mat) + return np.dot(np.transpose(MMAT), beta_mat) + # PURPOSE: create argument parser def arguments(): @@ -279,44 +357,101 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-C', - metavar='PROC', type=str, - default=None, choices=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO data processing center') + parser.add_argument( + '--center', + '-C', + metavar='PROC', + type=str, + default=None, + choices=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO data processing center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, - default='RL06', choices=['RL04','RL05','RL06'], - help='GRACE/GRACE-FO data release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + choices=['RL04', 'RL05', 'RL06'], + help='GRACE/GRACE-FO data release', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series') - parser.add_argument('--end','-E', - type=int, default=231, - help='Ending GRACE/GRACE-FO month for time series') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167,172, - 177,178,182,200,201] - parser.add_argument('--missing','-M', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months in time series') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=231, + help='Ending GRACE/GRACE-FO month for time series', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-M', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months in time series', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run program with parameters - plot_SLR_azimuthal(args.directory, args.center, args.release, - args.start, args.end, args.missing) + plot_SLR_azimuthal( + args.directory, + args.center, + args.release, + args.start, + args.end, + args.missing, + ) + # run main program if __name__ == '__main__': diff --git a/scripts/plot_SLR_zonals.py b/scripts/plot_SLR_zonals.py index 6f2bf48f..1bde7089 100644 --- a/scripts/plot_SLR_zonals.py +++ b/scripts/plot_SLR_zonals.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_SLR_zonals.py (05/2023) Plots low-degree zonal harmonics from GRACE/GRACE-FO and SLR Compares with a climatology calculated using GRACE months @@ -33,6 +33,7 @@ Updated 03/2017: output legend similar to matplotlib 1.0 Written 05/2016 """ + from __future__ import print_function, division import inspect @@ -46,26 +47,42 @@ try: import matplotlib import matplotlib.pyplot as plt + matplotlib.rcParams['mathtext.default'] = 'regular' matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] import matplotlib.offsetbox except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) # current file path for the child programs filename = inspect.getframeinfo(inspect.currentframe()).filename filepath = pathlib.Path(filename).absolute().parent + # plot SLR low-degree zonal coefficients def plot_SLR_zonals(base_dir, PROC, DREL, START_MON, END_MON, MISSING): # GRACE/GRACE-FO mission gap - GAP = [187,188,189,190,191,192,193,194,195,196,197] + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] missing = sorted(set(MISSING) | set(GAP)) # GRACE/GRACE-FO monthly harmonics - grace_Ylms = gravtk.grace_input_months(base_dir, PROC, DREL, 'GSM', 5, - START_MON, END_MON, missing, None, 0, MMAX=None, MODEL_DEG1=False, - ATM=False, POLE_TIDE=False, SLR_C30='N') + grace_Ylms = gravtk.grace_input_months( + base_dir, + PROC, + DREL, + 'GSM', + 5, + START_MON, + END_MON, + missing, + None, + 0, + MMAX=None, + MODEL_DEG1=False, + ATM=False, + POLE_TIDE=False, + SLR_C30='N', + ) GRACE = dict(date=grace_Ylms['time'], month=grace_Ylms['month']) grace_Ylms = gravtk.harmonics().from_dict(grace_Ylms) grace_Ylms.mean(apply=True) @@ -76,9 +93,10 @@ def plot_SLR_zonals(base_dir, PROC, DREL, START_MON, END_MON, MISSING): SLR_file = base_dir.joinpath('gsfc_slr_5x5c61s61.txt') G55 = gravtk.read_SLR_harmonics(SLR_file, HEADER=True) # converting from MJD into month, day and year to calculate GRACE month - YY,MM,DD,hh,mm,ss = gravtk.time.convert_julian(C55['MJD']+2400000.5, - format='tuple') - C55['month'] = np.array(12.0*(YY - 2002.0)+MM, dtype=np.int64) + YY, MM, DD, hh, mm, ss = gravtk.time.convert_julian( + C55['MJD'] + 2400000.5, format='tuple' + ) + C55['month'] = np.array(12.0 * (YY - 2002.0) + MM, dtype=np.int64) # filtered coefficients from John Ries SLR_file = base_dir.joinpath('C30_LARES_filtered.txt') @@ -93,107 +111,145 @@ def plot_SLR_zonals(base_dir, PROC, DREL, START_MON, END_MON, MISSING): # GFZ GravIS coefficients SLR_file = base_dir.joinpath('GFZ_RL06_C20_SLR.dat') GFZ_C20 = gravtk.SLR.C20(SLR_file) - SLR_file = base_dir.joinpath('GRAVIS-2B_GFZOP_GRACE+SLR_LOW_DEGREES_0002.dat') + SLR_file = base_dir.joinpath( + 'GRAVIS-2B_GFZOP_GRACE+SLR_LOW_DEGREES_0002.dat' + ) GFZ_C30 = gravtk.SLR.C30(SLR_file) # calculate common months - month = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + month = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) common_months = np.copy(month) - for v in [GRACE,C55,LARES_C30,GSFC_C30]: + for v in [GRACE, C55, LARES_C30, GSFC_C30]: common_months = sorted(set(common_months) & set(v['month'])) # read arrays of kl, hl, and ll Love Numbers - love_numbers_file = gravtk.utilities.get_data_path(['data','love_numbers']) - hl,kl,ll = gravtk.read_love_numbers(love_numbers_file, FORMAT='tuple') + love_numbers_file = gravtk.utilities.get_data_path(['data', 'love_numbers']) + hl, kl, ll = gravtk.read_love_numbers(love_numbers_file, FORMAT='tuple') # Earth Parameters - factors = gravtk.units(lmax=5).harmonic(hl,kl,ll) - rho_e = factors.rho_e# Average Density of the Earth [g/cm^3] - rad_e = factors.rad_e# Average Radius of the Earth [cm] + factors = gravtk.units(lmax=5).harmonic(hl, kl, ll) + rho_e = factors.rho_e # Average Density of the Earth [g/cm^3] + rad_e = factors.rad_e # Average Radius of the Earth [cm] # create output plot ax1 = {} ax2 = {} - f1, (ax1[2],ax1[3],ax1[4],ax1[5]) = plt.subplots(num=1,nrows=4, - sharex=True,figsize=(6,8)) - f2, (ax2[2],ax2[3],ax2[4],ax2[5]) = plt.subplots(num=2,nrows=4, - sharex=True,figsize=(6,8)) - plot_colors = ['mediumseagreen','0.5','darkorchid','darkorange'] - plot_labels = [f'{PROC} GRACE/GRACE-FO',f'{PROC} GRACE Climatology', - 'GSFC SLR','GFZ GravIS']#'CSR TN-11 SLR' - plot_zorder = [0,3,1,2] - fig_text = {2:'a)',3:'b)',4:'c)',5:'d)'} - for l,ax in ax1.items(): + f1, (ax1[2], ax1[3], ax1[4], ax1[5]) = plt.subplots( + num=1, nrows=4, sharex=True, figsize=(6, 8) + ) + f2, (ax2[2], ax2[3], ax2[4], ax2[5]) = plt.subplots( + num=2, nrows=4, sharex=True, figsize=(6, 8) + ) + plot_colors = ['mediumseagreen', '0.5', 'darkorchid', 'darkorange'] + plot_labels = [ + f'{PROC} GRACE/GRACE-FO', + f'{PROC} GRACE Climatology', + 'GSFC SLR', + 'GFZ GravIS', + ] #'CSR TN-11 SLR' + plot_zorder = [0, 3, 1, 2] + fig_text = {2: 'a)', 3: 'b)', 4: 'c)', 5: 'd)'} + for l, ax in ax1.items(): # GRACE data for zonal harmonic - GRACE['data'] = grace_Ylms.clm[l,0,:].copy() + GRACE['data'] = grace_Ylms.clm[l, 0, :].copy() # CSR and GSFC 5x5 data for zonal harmonic - C55['data'] = C55['clm'][l,0,:].copy() - G55['data'] = G55['clm'][l,0,:].copy() + C55['data'] = C55['clm'][l, 0, :].copy() + G55['data'] = G55['clm'][l, 0, :].copy() # calculate GRACE climatology for "good months" CLIMATE = {} - YY = np.arange(2002+1./24.,2024,1./12.).astype(np.int64) + YY = np.arange(2002 + 1.0 / 24.0, 2024, 1.0 / 12.0).astype(np.int64) CLIMATE['month'] = 1 + np.arange(len(YY)) - MM = 1 + ((CLIMATE['month']-1) % 12) + MM = 1 + ((CLIMATE['month'] - 1) % 12) CLIMATE['date'] = gravtk.time.convert_calendar_decimal(YY, MM) - ii, = np.nonzero((GRACE['month'] >= 13) & (GRACE['month'] <= 176)) - CLIMATE['data'] = regress_model(GRACE['date'][ii], GRACE['data'][ii], - CLIMATE['date'], ORDER=2, CYCLES=[0.25,0.5,1.0,2.0,4.0,5.0,161.0/365.25], - RELATIVE=GRACE['date'][0]) + (ii,) = np.nonzero((GRACE['month'] >= 13) & (GRACE['month'] <= 176)) + CLIMATE['data'] = regress_model( + GRACE['date'][ii], + GRACE['data'][ii], + CLIMATE['date'], + ORDER=2, + CYCLES=[0.25, 0.5, 1.0, 2.0, 4.0, 5.0, 161.0 / 365.25], + RELATIVE=GRACE['date'][0], + ) # plot items - if (l == 2): - plot_items = [GRACE,CLIMATE,GSFC_C20,GFZ_C20] - elif (l == 3): - plot_items = [GRACE,CLIMATE,GSFC_C30,GFZ_C30] + if l == 2: + plot_items = [GRACE, CLIMATE, GSFC_C20, GFZ_C20] + elif l == 3: + plot_items = [GRACE, CLIMATE, GSFC_C30, GFZ_C30] else: - GSFC = gravtk.convert_weekly(G55['time'], G55['data'], - DATE=GRACE['date'], NEIGHBORS=28) - plot_items = [GRACE,CLIMATE,GSFC] - for i,v in enumerate(plot_items): - ii = [i for i,m in enumerate(v['month']) if m in common_months] + GSFC = gravtk.convert_weekly( + G55['time'], G55['data'], DATE=GRACE['date'], NEIGHBORS=28 + ) + plot_items = [GRACE, CLIMATE, GSFC] + for i, v in enumerate(plot_items): + ii = [i for i, m in enumerate(v['month']) if m in common_months] # create a time series with nans for missing months - tdec = np.full_like(month,np.nan,dtype=np.float64) - cmwe = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): + tdec = np.full_like(month, np.nan, dtype=np.float64) + cmwe = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): valid = np.count_nonzero(v['month'] == m) if valid: - mm, = np.nonzero(CLIMATE['month'] == m) + (mm,) = np.nonzero(CLIMATE['month'] == m) tdec[d] = CLIMATE['date'][mm] - mm, = np.nonzero(v['month'] == m) + (mm,) = np.nonzero(v['month'] == m) # tdec[d] = v['date'][mm] # cm w.e.: centimeters water equivalent dfactor = factors.cmwe[l] - cmwe[d] = dfactor*(v['data'][mm]-v['data'][ii].mean()) + cmwe[d] = dfactor * (v['data'][mm] - v['data'][ii].mean()) # plot all dates - ax.plot(tdec, cmwe, zorder=plot_zorder[i], - color=plot_colors[i], label=plot_labels[i]) - ax2[l].plot(tdec, cmwe, zorder=plot_zorder[i], - color=plot_colors[i], label=plot_labels[i]) + ax.plot( + tdec, + cmwe, + zorder=plot_zorder[i], + color=plot_colors[i], + label=plot_labels[i], + ) + ax2[l].plot( + tdec, + cmwe, + zorder=plot_zorder[i], + color=plot_colors[i], + label=plot_labels[i], + ) # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9,day=3,hour=12,minute=12) - ax.axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) - ax2[l].axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax.axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) + ax2[l].axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(GRACE['month'] == 186) - kk, = np.flatnonzero(GRACE['month'] == 198) + (jj,) = np.flatnonzero(GRACE['month'] == 186) + (kk,) = np.flatnonzero(GRACE['month'] == 198) # ax.axvline(GRACE['time'][jj],color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) # ax.axvline(GRACE['time'][kk],color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) - vs = ax.axvspan(GRACE['date'][jj],GRACE['date'][kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) - vs = ax2[l].axvspan(GRACE['date'][jj],GRACE['date'][kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) + vs = ax.axvspan( + GRACE['date'][jj], + GRACE['date'][kk], + color='0.5', + ls='dashed', + alpha=0.15, + ) + vs._dashes = (6, 3) + vs = ax2[l].axvspan( + GRACE['date'][jj], + GRACE['date'][kk], + color='0.5', + ls='dashed', + alpha=0.15, + ) + vs._dashes = (6, 3) # set ticks - xmax = 2002 + (END_MON + 1.0)/12.0 + xmax = 2002 + (END_MON + 1.0) / 12.0 # set limits for first plot - major_ticks = np.arange(2014,xmax,2) + major_ticks = np.arange(2014, xmax, 2) minor_ticks = sorted(set(np.arange(2002, xmax, 1)) - set(major_ticks)) ax.xaxis.set_ticks(minor_ticks, minor=True) - ax.set_xlim(2013,xmax) + ax.set_xlim(2013, xmax) # set limits for second plot - major_ticks = np.arange(2002,xmax,2) + major_ticks = np.arange(2002, xmax, 2) minor_ticks = sorted(set(np.arange(2002, xmax, 1)) - set(major_ticks)) ax2[l].xaxis.set_ticks(major_ticks) ax2[l].xaxis.set_ticks(minor_ticks, minor=True) - ax2[l].set_xlim(2001.75,xmax) + ax2[l].set_xlim(2001.75, xmax) # add labels ax.set_ylabel('C{0:d}0 [cm]'.format(l)) ax2[l].set_ylabel('C{0:d}0 [cm]'.format(l)) @@ -206,34 +262,42 @@ def plot_SLR_zonals(base_dir, PROC, DREL, START_MON, END_MON, MISSING): ax.xaxis.get_major_formatter().set_useOffset(False) ax2[l].xaxis.get_major_formatter().set_useOffset(False) # add labels to the first plot - at = matplotlib.offsetbox.AnchoredText(fig_text[l], - prop=dict(size=14,weight='bold'), - pad=0, frameon=False, loc=2) + at = matplotlib.offsetbox.AnchoredText( + fig_text[l], + prop=dict(size=14, weight='bold'), + pad=0, + frameon=False, + loc=2, + ) ax.add_artist(at) # add labels to the second plot - at = matplotlib.offsetbox.AnchoredText(fig_text[l], - prop=dict(size=14,weight='bold'), - pad=0, frameon=False, loc=2) + at = matplotlib.offsetbox.AnchoredText( + fig_text[l], + prop=dict(size=14, weight='bold'), + pad=0, + frameon=False, + loc=2, + ) ax2[l].add_artist(at) # set y limits for each axis in the first plot - ax1[2].set_ylim(-5.6,4.6) - ax1[3].set_ylim(-2.6,2.1) - ax1[4].set_ylim(-2.1,1.6) + ax1[2].set_ylim(-5.6, 4.6) + ax1[3].set_ylim(-2.6, 2.1) + ax1[4].set_ylim(-2.1, 1.6) # ax1[4].yaxis.set_ticks([-1,0,1]) - ax1[5].set_ylim(-2.1,2.1) + ax1[5].set_ylim(-2.1, 2.1) # set y limits for each axis in the second plot - ax2[2].set_ylim(-5.1,4.6) - ax2[3].set_ylim(-2.6,2.1) - ax2[4].set_ylim(-2.1,2.1) + ax2[2].set_ylim(-5.1, 4.6) + ax2[3].set_ylim(-2.6, 2.1) + ax2[4].set_ylim(-2.1, 2.1) # ax2[4].yaxis.set_ticks([-1,0,1]) - ax2[5].set_ylim(-2.1,3.6) + ax2[5].set_ylim(-2.1, 3.6) # add label and legend ax1[5].set_xlabel('Time [Yr]') ax2[5].set_xlabel('Time [Yr]') # add legend to the first plot - lgd = ax1[2].legend(loc=3,frameon=True,ncol=2) + lgd = ax1[2].legend(loc=3, frameon=True, ncol=2) # set width, color and style of lines lgd.get_frame().set_boxstyle('square,pad=0.01') lgd.get_frame().set_edgecolor('white') @@ -241,7 +305,7 @@ def plot_SLR_zonals(base_dir, PROC, DREL, START_MON, END_MON, MISSING): for line in lgd.get_lines(): line.set_linewidth(6) # add legend to the second plot - lgd = ax2[2].legend(loc=3,frameon=True,ncol=2) + lgd = ax2[2].legend(loc=3, frameon=True, ncol=2) # set width, color and style of lines lgd.get_frame().set_boxstyle('square,pad=0.1') lgd.get_frame().set_edgecolor('white') @@ -250,11 +314,15 @@ def plot_SLR_zonals(base_dir, PROC, DREL, START_MON, END_MON, MISSING): line.set_linewidth(6) # adjust the plot and save to file - f1.subplots_adjust(left=0.08,right=0.97,bottom=0.05,top=0.99,hspace=0.06) + f1.subplots_adjust( + left=0.08, right=0.97, bottom=0.05, top=0.99, hspace=0.06 + ) figurefile = filepath.joinpath('fs02ad.pdf') f1.savefig(figurefile, format='pdf') # adjust the plot and save to file - f2.subplots_adjust(left=0.08,right=0.97,bottom=0.05,top=0.99,hspace=0.06) + f2.subplots_adjust( + left=0.08, right=0.97, bottom=0.05, top=0.99, hspace=0.06 + ) figurefile = filepath.joinpath('fs02ad_all.pdf') f2.savefig(figurefile, format='pdf') # close everything @@ -267,60 +335,82 @@ def plot_SLR_zonals(base_dir, PROC, DREL, START_MON, END_MON, MISSING): # cm w.e. degree dependent factor for centimeters water equivalent dfactor = factors.cmwe[l] # GRACE data for zonal harmonic - GRACE['data'] = grace_Ylms.clm[l,0,:].copy() + GRACE['data'] = grace_Ylms.clm[l, 0, :].copy() # CSR 5x5 data for zonal harmonic - C55['data'] = C55['clm'][l,0,:].copy() + C55['data'] = C55['clm'][l, 0, :].copy() # calculate GRACE climatology for "good months" CLIMATE = {} - YY = np.arange(2002+1./24.,2022,1./12.).astype(np.int64) + YY = np.arange(2002 + 1.0 / 24.0, 2022, 1.0 / 12.0).astype(np.int64) CLIMATE['month'] = 1 + np.arange(len(YY)) - MM = 1 + ((CLIMATE['month']-1) % 12) + MM = 1 + ((CLIMATE['month'] - 1) % 12) CLIMATE['date'] = gravtk.time.convert_calendar_decimal(YY, MM) - ii, = np.nonzero((GRACE['month'] >= 13) & (GRACE['month'] <= 176)) - CLIMATE['data'] = regress_model(GRACE['date'][ii], GRACE['data'][ii], - CLIMATE['date'], ORDER=2, CYCLES=[0.25,0.5,1.0,2.0,4.0,5.0], - RELATIVE=GRACE['date'][0]) + (ii,) = np.nonzero((GRACE['month'] >= 13) & (GRACE['month'] <= 176)) + CLIMATE['data'] = regress_model( + GRACE['date'][ii], + GRACE['data'][ii], + CLIMATE['date'], + ORDER=2, + CYCLES=[0.25, 0.5, 1.0, 2.0, 4.0, 5.0], + RELATIVE=GRACE['date'][0], + ) # create output plot - fig, ax = plt.subplots(num=3, figsize=(6.5,4)) - plot_colors = ['mediumseagreen','0.5','darkorchid','darkorange'] - plot_labels = ['CSR GRACE/GRACE-FO','CSR GRACE Climatology', - 'GSFC TN-14 SLR','CSR LARES SLR'] - plot_zorder = [0,3,1,2] - for i,v in enumerate([GRACE,CLIMATE,GSFC_C30,LARES_C30]): - ii = [i for i,m in enumerate(v['month']) if m in common_months] + fig, ax = plt.subplots(num=3, figsize=(6.5, 4)) + plot_colors = ['mediumseagreen', '0.5', 'darkorchid', 'darkorange'] + plot_labels = [ + 'CSR GRACE/GRACE-FO', + 'CSR GRACE Climatology', + 'GSFC TN-14 SLR', + 'CSR LARES SLR', + ] + plot_zorder = [0, 3, 1, 2] + for i, v in enumerate([GRACE, CLIMATE, GSFC_C30, LARES_C30]): + ii = [i for i, m in enumerate(v['month']) if m in common_months] # create a time series with nans for missing months - tdec = np.full_like(month,np.nan,dtype=np.float64) - cmwe = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): + tdec = np.full_like(month, np.nan, dtype=np.float64) + cmwe = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): valid = np.count_nonzero(v['month'] == m) if valid: - mm, = np.nonzero(CLIMATE['month'] == m) + (mm,) = np.nonzero(CLIMATE['month'] == m) tdec[d] = CLIMATE['date'][mm] - mm, = np.nonzero(v['month'] == m) + (mm,) = np.nonzero(v['month'] == m) # tdec[d] = v['date'][mm] # cm w.e.: centimeters water equivalent - cmwe[d] = dfactor*(v['data'][mm]-v['data'][ii].mean()) + cmwe[d] = dfactor * (v['data'][mm] - v['data'][ii].mean()) # plot all dates - ax.plot(tdec, cmwe, zorder=plot_zorder[i], - color=plot_colors[i], label=plot_labels[i]) + ax.plot( + tdec, + cmwe, + zorder=plot_zorder[i], + color=plot_colors[i], + label=plot_labels[i], + ) # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9,day=3,hour=12,minute=12) - ax.axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax.axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(GRACE['month'] == 186) - kk, = np.flatnonzero(GRACE['month'] == 198) + (jj,) = np.flatnonzero(GRACE['month'] == 186) + (kk,) = np.flatnonzero(GRACE['month'] == 198) # ax.axvline(GRACE['time'][jj],color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) # ax.axvline(GRACE['time'][kk],color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) - vs = ax.axvspan(GRACE['date'][jj],GRACE['date'][kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) + vs = ax.axvspan( + GRACE['date'][jj], + GRACE['date'][kk], + color='0.5', + ls='dashed', + alpha=0.15, + ) + vs._dashes = (6, 3) # set limits - major_ticks = np.arange(2010,2022,2) + major_ticks = np.arange(2010, 2022, 2) minor_ticks = sorted(set(np.arange(2002, 2022, 1)) - set(major_ticks)) ax.xaxis.set_ticks(minor_ticks, minor=True) - ax.set_xlim(2010,2021.5) - ax.set_ylim(-2.6,1.6) + ax.set_xlim(2010, 2021.5) + ax.set_ylim(-2.6, 1.6) # add labels ax.set_xlabel('Time [Yr]') ax.set_ylabel('C{0:d}0 [cm]'.format(l)) @@ -330,7 +420,7 @@ def plot_SLR_zonals(base_dir, PROC, DREL, START_MON, END_MON, MISSING): # set axis ticker ax.xaxis.get_major_formatter().set_useOffset(False) # add legend - lgd = ax.legend(loc=3,frameon=True,ncol=2) + lgd = ax.legend(loc=3, frameon=True, ncol=2) # set width, color and style of lines lgd.get_frame().set_boxstyle('square,pad=0.1') lgd.get_frame().set_edgecolor('white') @@ -338,13 +428,14 @@ def plot_SLR_zonals(base_dir, PROC, DREL, START_MON, END_MON, MISSING): for line in lgd.get_lines(): line.set_linewidth(6) # adjust the plot and show - fig.subplots_adjust(left=0.1,right=0.97,bottom=0.1,top=0.97) + fig.subplots_adjust(left=0.1, right=0.97, bottom=0.1, top=0.97) figurefile = filepath.joinpath('fs02b.pdf') plt.savefig(figurefile, format='pdf') plt.cla() plt.clf() plt.close() + # PURPOSE: calculate a regression model for extrapolating values def regress_model(t_in, d_in, t_out, ORDER=2, CYCLES=None, RELATIVE=None): # remove singleton dimensions @@ -352,33 +443,34 @@ def regress_model(t_in, d_in, t_out, ORDER=2, CYCLES=None, RELATIVE=None): d_in = np.squeeze(d_in) t_out = np.squeeze(t_out) # check dimensions of output - if (np.ndim(t_out) == 0): + if np.ndim(t_out) == 0: t_out = np.array([t_out]) # CREATING DESIGN MATRIX FOR REGRESSION DMAT = [] MMAT = [] # add polynomial orders (0=constant, 1=linear, 2=quadratic) - for o in range(ORDER+1): - DMAT.append((t_in-RELATIVE)**o) - MMAT.append((t_out-RELATIVE)**o) + for o in range(ORDER + 1): + DMAT.append((t_in - RELATIVE) ** o) + MMAT.append((t_out - RELATIVE) ** o) # add cyclical terms (0.5=semi-annual, 1=annual) for c in CYCLES: - if (c == (161.0/365.25)): + if c == (161.0 / 365.25): # terms for S2 tidal aliasing during GRACE and GRACE-FO periods DMAT.extend(gravtk.time_series.aliasing_terms(t_in)) MMAT.extend(gravtk.time_series.aliasing_terms(t_out)) # remove the original S2 tidal aliasing term from CYCLES list CYCLES.remove(c) else: - DMAT.append(np.sin(2.0*np.pi*t_in/np.float64(c))) - DMAT.append(np.cos(2.0*np.pi*t_in/np.float64(c))) - MMAT.append(np.sin(2.0*np.pi*t_out/np.float64(c))) - MMAT.append(np.cos(2.0*np.pi*t_out/np.float64(c))) + DMAT.append(np.sin(2.0 * np.pi * t_in / np.float64(c))) + DMAT.append(np.cos(2.0 * np.pi * t_in / np.float64(c))) + MMAT.append(np.sin(2.0 * np.pi * t_out / np.float64(c))) + MMAT.append(np.cos(2.0 * np.pi * t_out / np.float64(c))) # Calculating Least-Squares Coefficients # Standard Least-Squares fitting (the [0] denotes coefficients output) beta_mat = np.linalg.lstsq(np.transpose(DMAT), d_in, rcond=-1)[0] # return modeled time-series - return np.dot(np.transpose(MMAT),beta_mat) + return np.dot(np.transpose(MMAT), beta_mat) + # PURPOSE: create argument parser def arguments(): @@ -388,44 +480,101 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-C', - metavar='PROC', type=str, - default=None, choices=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO data processing center') + parser.add_argument( + '--center', + '-C', + metavar='PROC', + type=str, + default=None, + choices=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO data processing center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, - default='RL06', choices=['RL04','RL05','RL06'], - help='GRACE/GRACE-FO data release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + choices=['RL04', 'RL05', 'RL06'], + help='GRACE/GRACE-FO data release', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series') - parser.add_argument('--end','-E', - type=int, default=231, - help='Ending GRACE/GRACE-FO month for time series') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167,172, - 177,178,182,200,201] - parser.add_argument('--missing','-M', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months in time series') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=231, + help='Ending GRACE/GRACE-FO month for time series', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-M', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months in time series', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run program with parameters - plot_SLR_zonals(args.directory, args.center, args.release, - args.start, args.end, args.missing) + plot_SLR_zonals( + args.directory, + args.center, + args.release, + args.start, + args.end, + args.missing, + ) + # run main program if __name__ == '__main__': diff --git a/scripts/plot_mascon_SLF_combined.py b/scripts/plot_mascon_SLF_combined.py index b9e9e83a..f7cdffdd 100644 --- a/scripts/plot_mascon_SLF_combined.py +++ b/scripts/plot_mascon_SLF_combined.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_mascon_SLF_combined.py Written by Tyler Sutterley (05/2023) @@ -26,6 +26,7 @@ Updated 01/2021: updated for new open-source processing scheme Written 11/2019 """ + from __future__ import print_function import inspect @@ -39,26 +40,28 @@ try: import matplotlib import matplotlib.pyplot as plt + matplotlib.rcParams['mathtext.default'] = 'regular' matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] import matplotlib.offsetbox except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import scipy.stats import scipy.special except ModuleNotFoundError: - warnings.warn("scipy not available", ImportWarning) + warnings.warn('scipy not available', ImportWarning) # current file path for the child programs filename = inspect.getframeinfo(inspect.currentframe()).filename filepath = pathlib.Path(filename).absolute().parent + # PURPOSE: plot and calculate effects of SLF on mascon time series -def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): +def plot_mascon_SLF_combined(base_dir, PROC, DREL, START_MON, END_MON, MISSING): # directory setup - mascon_dir = base_dir.joinpath('GRACE','mascons') + mascon_dir = base_dir.joinpath('GRACE', 'mascons') # GIA and REGION GIA = {} gia_files = {} @@ -66,47 +69,77 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): REGION = {} # Simpson (2009), Peltier (2015,2018), A (ICE6G), Caron (2018) - GIA['N'] = ['SM09','ICE6G','ICE6G-D','AW13-ICE6G','Caron'] + GIA['N'] = ['SM09', 'ICE6G', 'ICE6G-D', 'AW13-ICE6G', 'Caron'] # Ivins (2013), Whitehouse (2012), Peltier (2015,2018), A (ICE6G/IJ05), Caron (2018) - GIA['S'] = ['IJ05-R2','W12a','ICE6G','ICE6G-D','AW13-ICE6G','ascii','Caron'] + GIA['S'] = [ + 'IJ05-R2', + 'W12a', + 'ICE6G', + 'ICE6G-D', + 'AW13-ICE6G', + 'ascii', + 'Caron', + ] # GIA files for each modeling group gia_files['AW13-ICE6G'] = [] - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_.1_10.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_1._10.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_.1_1.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_1._1.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_GA.txt']) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_.1_10.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_1._10.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_.1_1.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_1._1.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_GA.txt'] + ) gia_files['ascii'] = [] - gia_files['ascii'].append(['GIA','AW13','IJ05-R2','IJ05_R2_115_.2_1.5_ICE6G.txt']) - gia_files['ascii'].append(['GIA','AW13','IJ05-R2','IJ05_R2_65_.2_1.5_ICE6G.txt']) + gia_files['ascii'].append( + ['GIA', 'AW13', 'IJ05-R2', 'IJ05_R2_115_.2_1.5_ICE6G.txt'] + ) + gia_files['ascii'].append( + ['GIA', 'AW13', 'IJ05-R2', 'IJ05_R2_65_.2_1.5_ICE6G.txt'] + ) gia_files['Caron'] = [] - gia_files['Caron'].append(['GIA','Caron','expStokes_GIA.txt']) + gia_files['Caron'].append(['GIA', 'Caron', 'expStokes_GIA.txt']) gia_files['ICE6G-D'] = [] - gia_files['ICE6G-D'].append(['GIA','ICE6G','VersionD','Stokes_trend_High_Res.txt']) - gia_files['ICE6G-D'].append(['GIA','ICE6G','VersionD','Stokes_trend_VM5a_O512.txt']) + gia_files['ICE6G-D'].append( + ['GIA', 'ICE6G', 'VersionD', 'Stokes_trend_High_Res.txt'] + ) + gia_files['ICE6G-D'].append( + ['GIA', 'ICE6G', 'VersionD', 'Stokes_trend_VM5a_O512.txt'] + ) gia_files['ICE6G'] = [] - gia_files['ICE6G'].append(['GIA','ICE6G','VM5','Stokes_G_Rot_60_I6_A_VM5a']) - gia_files['ICE6G'].append(['GIA','ICE6G','VM5','Stokes_G_Rot_60_I6_A_VM5b']) + gia_files['ICE6G'].append( + ['GIA', 'ICE6G', 'VM5', 'Stokes_G_Rot_60_I6_A_VM5a'] + ) + gia_files['ICE6G'].append( + ['GIA', 'ICE6G', 'VM5', 'Stokes_G_Rot_60_I6_A_VM5b'] + ) gia_files['IJ05-R2'] = [] - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_1.5_L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_2._L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_3.2_L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_4._L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_65_.2_1.5_L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_1.5_L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_2._L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_3.2_L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_4._L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_65_.2_1.5_L120']) gia_files['SM09'] = [] - gia_files['SM09'].append(['GIA','SM09','grate_120p11.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_120p51.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_120p53.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_120p81.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p32.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p510.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p55.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p58.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p85.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p11.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p51.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p53.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p81.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p32.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p510.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p55.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p58.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p85.clm']) gia_files['W12a'] = [] - gia_files['W12a'].append(['GIA','W12a','grate_B.clm']) - gia_files['W12a'].append(['GIA','W12a','grate_L.clm']) - gia_files['W12a'].append(['GIA','W12a','grate_U.clm']) + gia_files['W12a'].append(['GIA', 'W12a', 'grate_B.clm']) + gia_files['W12a'].append(['GIA', 'W12a', 'grate_L.clm']) + gia_files['W12a'].append(['GIA', 'W12a', 'grate_U.clm']) # mean title for each GIA string gia_mean_str['IJ05-R2'] = 'Ivins_R2_mean' gia_mean_str['W12a'] = 'W12a_mean' @@ -119,49 +152,71 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): # regions REGION['N'] = 'GIS' REGION['S'] = 'AIS' - REMOVE = dict(S='AIS',N='ARC') + REMOVE = dict(S='AIS', N='ARC') # Land-Sea Mask (with Antarctica from Rignot et al., 2017) # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - LSMASK = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - landsea = gravtk.spatial().from_netCDF4(LSMASK, - date=False, varname='LSMASK') + LSMASK = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + landsea = gravtk.spatial().from_netCDF4( + LSMASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - land_function = np.zeros((nth,nphi),dtype=np.float64) + nth, nphi = landsea.shape + land_function = np.zeros((nth, nphi), dtype=np.float64) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + land_function[indx, indy] = 1.0 # calculate ocean function from land function ocean_function = 1.0 - land_function # convert ocean function into a series of spherical harmonics # (Note that LMAX=0 in the call to gen_harmonics) - Ylms = gravtk.gen_harmonics(ocean_function.T, landsea.lon, landsea.lat, - LMAX=0, PLM=np.ones((1,1,nth))) + Ylms = gravtk.gen_harmonics( + ocean_function.T, + landsea.lon, + landsea.lat, + LMAX=0, + PLM=np.ones((1, 1, nth)), + ) # total area of ocean calculated by integrating the ocean function # Average Radius of the Earth [mm] - rad_e = 10.0*gravtk.units().rad_e - ocean_area = 4.0*np.pi*(rad_e**2)*Ylms.clm[0,0] + rad_e = 10.0 * gravtk.units().rad_e + ocean_area = 4.0 * np.pi * (rad_e**2) * Ylms.clm[0, 0] # Setting output error alpha with confidence interval CONF = 0.95 alpha = 1.0 - CONF # GRACE data release and months - GAP = [187,188,189,190,191,192,193,194,195,196,197,] - months = sorted(set(np.arange(START_MON,END_MON+1))-set(MISSING)-set(GAP)) + GAP = [ + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + ] + months = sorted( + set(np.arange(START_MON, END_MON + 1)) - set(MISSING) - set(GAP) + ) nmon = len(months) # start and end GRACE months for correction data - OBP_START,OBP_END = (4,254) - ATM_START,ATM_END = (4,251) - GLDAS_START,GLDAS_END = (4,254) + OBP_START, OBP_END = (4, 254) + ATM_START, ATM_END = (4, 251) + GLDAS_START, GLDAS_END = (4, 254) # input leakage file - leakage_file = mascon_dir.joinpath('HEX_TMB_LEAKAGE_SPH_CAP_MSCNS_L60', - 'HEX_TMB_LEAKAGE_HOLE_SPH_CAP_RAD1.5_L60_r250km_OCN.txt') + leakage_file = mascon_dir.joinpath( + 'HEX_TMB_LEAKAGE_SPH_CAP_MSCNS_L60', + 'HEX_TMB_LEAKAGE_HOLE_SPH_CAP_RAD1.5_L60_r250km_OCN.txt', + ) regional_leakage = {} - with open(leakage_file,'r') as f: + with open(leakage_file, 'r') as f: file_contents = f.read().splitlines() for line in file_contents: line_contents = line.split() @@ -186,52 +241,89 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): ax1 = {} ax2 = {} ax3 = {} - fig, ((ax1['GIS'],ax3['GIS']),(ax1['WAIS'],ax3['WAIS']), - (ax1['EAIS'],ax3['EAIS']),(ax1['APIS'],ax3['APIS'])) = \ - plt.subplots(num=1,nrows=4,ncols=2,sharex=True,figsize=(9,10)) + ( + fig, + ( + (ax1['GIS'], ax3['GIS']), + (ax1['WAIS'], ax3['WAIS']), + (ax1['EAIS'], ax3['EAIS']), + (ax1['APIS'], ax3['APIS']), + ), + ) = plt.subplots(num=1, nrows=4, ncols=2, sharex=True, figsize=(9, 10)) # create a set of months - month = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + month = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) # figure labels - FLAG = ['HEX','HEX'] - plot_colors = ['darkorchid','mediumseagreen'] - plot_title = ['No Sea Level Correction','Sea Level Fingerprint'] - iter_label = ['No SL','SLF'] + FLAG = ['HEX', 'HEX'] + plot_colors = ['darkorchid', 'mediumseagreen'] + plot_title = ['No Sea Level Correction', 'Sea Level Fingerprint'] + iter_label = ['No SL', 'SLF'] fig_text = dict(GIS='a)', WAIS='b)', EAIS='c)', APIS='d)') - ylimits = dict(GIS=[-5500,1000],WAIS=[-3000,600],EAIS=[-400,1500],APIS=[-550,200]) - hem = dict(GIS='N',WAIS='S',EAIS='S',APIS='S') - ypad = dict(GIS=4,WAIS=4,EAIS=9.5,APIS=9.5) - ysea = dict(GIS=[14,-2,-2],WAIS=[8,-1,-1],EAIS=[1,-4,-1],APIS=[1,0,-1]) - seapad = dict(GIS=4,WAIS=4,EAIS=4,APIS=9.5) - for reg,ax in ax1.items(): + ylimits = dict( + GIS=[-5500, 1000], + WAIS=[-3000, 600], + EAIS=[-400, 1500], + APIS=[-550, 200], + ) + hem = dict(GIS='N', WAIS='S', EAIS='S', APIS='S') + ypad = dict(GIS=4, WAIS=4, EAIS=9.5, APIS=9.5) + ysea = dict( + GIS=[14, -2, -2], WAIS=[8, -1, -1], EAIS=[1, -4, -1], APIS=[1, 0, -1] + ) + seapad = dict(GIS=4, WAIS=4, EAIS=4, APIS=9.5) + for reg, ax in ax1.items(): # hemisphere flag for region h = hem[reg] # set ice sheet only for AIS and GIS - SLF = ['','_SLF3','_SLF6'] if (reg == 'GIS') else ['','_SLF3','_SLF5'] + SLF = ( + ['', '_SLF3', '_SLF6'] if (reg == 'GIS') else ['', '_SLF3', '_SLF5'] + ) # leakage fraction for regions leakage_fraction = regional_leakage[reg] # read ocean bottom pressure leakage file - subdir = sd.format('AOD1B',DREL,'',LMAX,OBP_START,OBP_END) - OBP_file = ff.format('ECCO-GAD_OBP_Residuals',reg,'','',ocean_str,LMAX,gw_str,ds_str) - OBP_input = np.loadtxt(mascon_dir.joinpath(subdir,OBP_file))[:nmon,:] + subdir = sd.format('AOD1B', DREL, '', LMAX, OBP_START, OBP_END) + OBP_file = ff.format( + 'ECCO-GAD_OBP_Residuals', + reg, + '', + '', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + OBP_input = np.loadtxt(mascon_dir.joinpath(subdir, OBP_file))[:nmon, :] # read atmospheric pressure leakage file - subdir = sd.format('AOD1B',DREL,'',LMAX,ATM_START,ATM_END) + subdir = sd.format('AOD1B', DREL, '', LMAX, ATM_START, ATM_END) # ATM_file = ff.format('ATM-GAA_Residuals',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) # ATM_file = ff.format('ATM_Differences',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) - ATM_file = ff.format('ATM_Differences',reg,'','',ocean_str,LMAX,gw_str,ds_str) - ATM_input = np.loadtxt(mascon_dir.joinpath(subdir,ATM_file))[:nmon,:] + ATM_file = ff.format( + 'ATM_Differences', reg, '', '', ocean_str, LMAX, gw_str, ds_str + ) + ATM_input = np.loadtxt(mascon_dir.joinpath(subdir, ATM_file))[:nmon, :] # read GLDAS terrestrial water RMS file - subdir = sd.format('GLDAS','TWC_V2.1_RMS','',LMAX,GLDAS_START,GLDAS_END) - TWC_file = ff.format('GLDAS_TWC_RMS',reg,'','RAD1.5_','',LMAX,gw_str,ds_str) - TWC_input = np.loadtxt(base_dir.joinpath('GLDAS',subdir,TWC_file))[:nmon,:] - isvalid, = np.nonzero(np.isfinite(TWC_input[:,2]) & - (TWC_input[:,0] >= START_MON) & (TWC_input[:,0] <= END_MON)) - TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid,2]**2)/len(isvalid)) + subdir = sd.format( + 'GLDAS', 'TWC_V2.1_RMS', '', LMAX, GLDAS_START, GLDAS_END + ) + TWC_file = ff.format( + 'GLDAS_TWC_RMS', reg, '', 'RAD1.5_', '', LMAX, gw_str, ds_str + ) + TWC_input = np.loadtxt(base_dir.joinpath('GLDAS', subdir, TWC_file))[ + :nmon, : + ] + (isvalid,) = np.nonzero( + np.isfinite(TWC_input[:, 2]) + & (TWC_input[:, 0] >= START_MON) + & (TWC_input[:, 0] <= END_MON) + ) + TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid, 2] ** 2) / len(isvalid)) # input estimated SLF monte carlo variance file and calculate RMS - subdir = sd.format(PROC,DREL,'_MC',LMAX,START_MON,END_MON) - SLF_file = ff.format('MC',reg,'','RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - SLF_input = np.loadtxt(mascon_dir.joinpath(subdir,SLF_file)) - SLF_RMS = np.sqrt(np.sum(SLF_input[:,1]**2)/len(SLF_input)) + subdir = sd.format(PROC, DREL, '_MC', LMAX, START_MON, END_MON) + SLF_file = ff.format( + 'MC', reg, '', 'RAD1.5_', ocean_str, LMAX, gw_str, ds_str + ) + SLF_input = np.loadtxt(mascon_dir.joinpath(subdir, SLF_file)) + SLF_RMS = np.sqrt(np.sum(SLF_input[:, 1] ** 2) / len(SLF_input)) # calculate mean of RMS over multiple reanalyses OBP_RMS = 0.0 ATM_RMS = 0.0 @@ -239,16 +331,16 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): # ivalid, = np.nonzero(np.isfinite(OBP_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(OBP_input[:,j+3])) # OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(OBP_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(OBP_input[:,2])) - OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(OBP_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(OBP_input[:, 2])) + OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid, 2] ** 2) / valid_count) # for j in range(4): # ivalid, = np.nonzero(np.isfinite(ATM_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(ATM_input[:,j+3])) # ATM_RMS += np.sqrt(np.sum(OBP_input[ATM_input,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(ATM_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(ATM_input[:,2])) - ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(ATM_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(ATM_input[:, 2])) + ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid, 2] ** 2) / valid_count) # # divide by the number of reanalyses # OBP_RMS /= 2.0 # ATM_RMS /= 4.0 @@ -257,84 +349,135 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): # number of rheologies to iterate nRheology = len(gia_files[g]) # iterate through solutions - mon = np.zeros((nmon),dtype=np.int64) - tdec = np.zeros((nmon),dtype=np.float64) - mass = np.zeros((nmon,nRheology),dtype=np.float64) - satellite_error = np.zeros((nmon),dtype=np.float64) - for i,F in enumerate(FLAG): + mon = np.zeros((nmon), dtype=np.int64) + tdec = np.zeros((nmon), dtype=np.float64) + mass = np.zeros((nmon, nRheology), dtype=np.float64) + satellite_error = np.zeros((nmon), dtype=np.float64) + for i, F in enumerate(FLAG): # subdirectory - subdir = sd.format(PROC,DREL,SLF[i],LMAX,START_MON,END_MON) + subdir = sd.format(PROC, DREL, SLF[i], LMAX, START_MON, END_MON) # read each GIA model - for k,GIA_FILE in enumerate(gia_files[g]): - gia_Ylms = gravtk.read_GIA_model(base_dir.joinpath(*GIA_FILE), GIA=g) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_Ylms = gravtk.read_GIA_model( + base_dir.joinpath(*GIA_FILE), GIA=g + ) gia_str = gia_Ylms['title'] - input_file = ff.format(gia_str,reg,'',atm_str,ocean_str,LMAX,gw_str,ds_str) - dinput = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) - mon[:] = dinput[:nmon,0].astype(np.int64) - tdec[:] = dinput[:nmon,1] - mass[:,k] = dinput[:nmon,2] - satellite_error[:] += dinput[:nmon,3]**2 + input_file = ff.format( + gia_str, reg, '', atm_str, ocean_str, LMAX, gw_str, ds_str + ) + dinput = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) + mon[:] = dinput[:nmon, 0].astype(np.int64) + tdec[:] = dinput[:nmon, 1] + mass[:, k] = dinput[:nmon, 2] + satellite_error[:] += dinput[:nmon, 3] ** 2 # calculate mean GIA-corrected mass change (for all Earth rheologies) gia_corrected_mean = np.mean(mass, axis=1) # GRACE satellite error component, ocean leakage (ECCO-GAD), # atmosphere leakage (ATM-GAA) and GLDAS TWC - grace_error = np.sqrt(np.sum(satellite_error/nRheology + SLF_RMS**2 + - OBP_RMS**2 + ATM_RMS**2 + TWC_RMS**2)/nmon) + grace_error = np.sqrt( + np.sum( + satellite_error / nRheology + + SLF_RMS**2 + + OBP_RMS**2 + + ATM_RMS**2 + + TWC_RMS**2 + ) + / nmon + ) # calculate variance off of mean for calculating GIA uncertainty gia_corrected_variance = np.zeros((nmon)) gia_corrected_minmax = np.zeros((nmon)) # calculate GIA uncertainty as "worst-case" not RMS - for k,GIA_FILE in enumerate(gia_files[g]): - gia_corrected_variance+=np.abs(mass[:,k]-gia_corrected_mean) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_corrected_variance += np.abs( + mass[:, k] - gia_corrected_mean + ) # calculate GIA uncertainty as "worst-case" min max error for t in range(nmon): - gia_corrected_minmax[t]=np.abs(np.max(mass[t,:])-np.min(mass[t,:])) + gia_corrected_minmax[t] = np.abs( + np.max(mass[t, :]) - np.min(mass[t, :]) + ) # calculate uncertainty in mean GIA - gia_corrected_error = gia_corrected_variance/(np.float64(nRheology)-1.0) - gia_corrected_error = gia_corrected_error*np.sign(tdec-tdec.mean()) - gia_corrected_minmax = gia_corrected_minmax*np.sign(tdec-tdec.mean()) - gia_error_rate = (gia_corrected_error[-1]-gia_corrected_error[0])/(tdec[-1]-tdec[0]) - gia_minmax_rate = (gia_corrected_minmax[-1]-gia_corrected_minmax[0])/(tdec[-1]-tdec[0]) + gia_corrected_error = gia_corrected_variance / ( + np.float64(nRheology) - 1.0 + ) + gia_corrected_error = gia_corrected_error * np.sign( + tdec - tdec.mean() + ) + gia_corrected_minmax = gia_corrected_minmax * np.sign( + tdec - tdec.mean() + ) + gia_error_rate = ( + gia_corrected_error[-1] - gia_corrected_error[0] + ) / (tdec[-1] - tdec[0]) + gia_minmax_rate = ( + gia_corrected_minmax[-1] - gia_corrected_minmax[0] + ) / (tdec[-1] - tdec[0]) # calculate GIA errors at confidence interval # t.ppf parallels tinv in matlab - tstar = scipy.stats.t.ppf(1.0-(alpha/2.0),nRheology-1.0) if g in ('IJ05-R2','SM09') else 2.0 - gia_corrected_conf = tstar*np.abs(gia_error_rate) + tstar = ( + scipy.stats.t.ppf(1.0 - (alpha / 2.0), nRheology - 1.0) + if g in ('IJ05-R2', 'SM09') + else 2.0 + ) + gia_corrected_conf = tstar * np.abs(gia_error_rate) # add to plot with colors and label plot_label = plot_title[i] # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - mnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): + tnan = np.full_like(month, np.nan, dtype=np.float64) + mnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): valid = np.count_nonzero(mon == m) if valid: - mm, = np.nonzero(mon == m) + (mm,) = np.nonzero(mon == m) tnan[d] = tdec[mm] - mnan[d] = gia_corrected_mean[mm]-gia_corrected_mean[0] + mnan[d] = gia_corrected_mean[mm] - gia_corrected_mean[0] # plot all dates - ax.plot(tnan, mnan, color=plot_colors[i], label=plot_label, zorder=2) + ax.plot( + tnan, mnan, color=plot_colors[i], label=plot_label, zorder=2 + ) # fill between monthly errors - ax.fill_between(tnan, mnan-grace_error, y2=mnan+grace_error, - color=plot_colors[i], alpha=0.5, zorder=1) + ax.fill_between( + tnan, + mnan - grace_error, + y2=mnan + grace_error, + color=plot_colors[i], + alpha=0.5, + zorder=1, + ) # converting gigatonnes to milligrams then to mm sea level - mm_sealevel = -1e18*(gia_corrected_mean-gia_corrected_mean[0])/ocean_area - mm_sealevel_error = 1e18*np.sqrt(grace_error**2 + - (tstar*gia_corrected_error[-1])**2)/ocean_area + mm_sealevel = ( + -1e18 + * (gia_corrected_mean - gia_corrected_mean[0]) + / ocean_area + ) + mm_sealevel_error = ( + 1e18 + * np.sqrt( + grace_error**2 + (tstar * gia_corrected_error[-1]) ** 2 + ) + / ocean_area + ) # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9, - day=3,hour=12,minute=12) - ax.axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax.axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(mon == 186) - kk, = np.flatnonzero(mon == 198) + (jj,) = np.flatnonzero(mon == 186) + (kk,) = np.flatnonzero(mon == 198) # ax.axvline(tdec[jj],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) # ax.axvline(tdec[kk],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) - vs = ax.axvspan(tdec[jj],tdec[kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) + vs = ax.axvspan( + tdec[jj], tdec[kk], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (6, 3) # add labels - textprops = dict(size=14,weight='bold') - at = matplotlib.offsetbox.AnchoredText(fig_text[reg], - prop=textprops, pad=0, frameon=False, loc=2) + textprops = dict(size=14, weight='bold') + at = matplotlib.offsetbox.AnchoredText( + fig_text[reg], prop=textprops, pad=0, frameon=False, loc=2 + ) ax.add_artist(at) lab = ax1[reg].set_ylabel('Mass [Gt]', labelpad=ypad[reg]) # set ticks @@ -352,19 +495,28 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): ax2[reg] = ax.twinx() # add plot to hidden mm sea level axis ax2[reg].plot(tdec, mm_sealevel, visible=False) - seaticks = np.arange(ysea[reg][0],ysea[reg][1]+ysea[reg][2],ysea[reg][2]) + seaticks = np.arange( + ysea[reg][0], ysea[reg][1] + ysea[reg][2], ysea[reg][2] + ) ax2[reg].set_yticks(seaticks) - ax2[reg].set_ylim(-1e18*ylimits[reg][0]/ocean_area,-1e18*ylimits[reg][1]/ocean_area) - ax2[reg].tick_params(axis='y',colors='black',which='both',direction='in') + ax2[reg].set_ylim( + -1e18 * ylimits[reg][0] / ocean_area, + -1e18 * ylimits[reg][1] / ocean_area, + ) + ax2[reg].tick_params( + axis='y', colors='black', which='both', direction='in' + ) # formatted ticks on sea level axis - ax2[reg].set_ylabel('Equivalent Sea Level [mm]',labelpad=seapad[reg],color='black') + ax2[reg].set_ylabel( + 'Equivalent Sea Level [mm]', labelpad=seapad[reg], color='black' + ) for tl in ax2[reg].get_yticklabels(): tl.set_color('black') # add x label ax1['APIS'].set_xlabel('Time [Yr]') # add legend - lgd = ax1['APIS'].legend(loc=3,frameon=False) + lgd = ax1['APIS'].legend(loc=3, frameon=False) # lgd = ax1['APIS'].legend(loc=3,frameon=False,handletextpad=-0.2,handlelength=0) # set width, color and style of lines # lgd.get_frame().set_boxstyle('square,pad=0.1') @@ -377,40 +529,61 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): # text.set_color(plot_colors[i]) # text.set_weight('bold') - # flags for creating plots - FLAG = ['HEX','HEX'] - SLF = ['','_SLF3'] - plot_colors = ['black','mediumseagreen'] - plot_titles = ['No SL Correction','Iteration 3'] + FLAG = ['HEX', 'HEX'] + SLF = ['', '_SLF3'] + plot_colors = ['black', 'mediumseagreen'] + plot_titles = ['No SL Correction', 'Iteration 3'] fig_text = dict(GIS='e)', WAIS='f)', EAIS='g)', APIS='h)') # y limits for plot - ylimits = [-90,170,40] - for reg,ax in ax3.items(): + ylimits = [-90, 170, 40] + for reg, ax in ax3.items(): # hemisphere flag for region h = hem[reg] # read ocean bottom pressure leakage file - subdir = sd.format('AOD1B',DREL,'',LMAX,OBP_START,OBP_END) - OBP_file = ff.format('ECCO-GAD_OBP_Residuals',reg,'','',ocean_str,LMAX,gw_str,ds_str) - OBP_input = np.loadtxt(mascon_dir.joinpath(subdir,OBP_file))[:nmon,:] + subdir = sd.format('AOD1B', DREL, '', LMAX, OBP_START, OBP_END) + OBP_file = ff.format( + 'ECCO-GAD_OBP_Residuals', + reg, + '', + '', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + OBP_input = np.loadtxt(mascon_dir.joinpath(subdir, OBP_file))[:nmon, :] # read atmospheric pressure leakage file - subdir = sd.format('AOD1B',DREL,'',LMAX,ATM_START,ATM_END) + subdir = sd.format('AOD1B', DREL, '', LMAX, ATM_START, ATM_END) # ATM_file = ff.format('ATM-GAA_Residuals',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) # ATM_file = ff.format('ATM_Differences',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) - ATM_file = ff.format('ATM_Differences',reg,'','',ocean_str,LMAX,gw_str,ds_str) - ATM_input = np.loadtxt(mascon_dir.joinpath(subdir,ATM_file))[:nmon,:] + ATM_file = ff.format( + 'ATM_Differences', reg, '', '', ocean_str, LMAX, gw_str, ds_str + ) + ATM_input = np.loadtxt(mascon_dir.joinpath(subdir, ATM_file))[:nmon, :] # read GLDAS terrestrial water RMS file - subdir = sd.format('GLDAS','TWC_V2.1_RMS','',LMAX,GLDAS_START,GLDAS_END) - TWC_file = ff.format('GLDAS_TWC_RMS',reg,'','RAD1.5_','',LMAX,gw_str,ds_str) - TWC_input = np.loadtxt(base_dir.joinpath('GLDAS',subdir,TWC_file))[:nmon,:] - isvalid, = np.nonzero(np.isfinite(TWC_input[:,2]) & - (TWC_input[:,0] >= START_MON) & (TWC_input[:,0] <= END_MON)) - TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid,2]**2)/len(isvalid)) + subdir = sd.format( + 'GLDAS', 'TWC_V2.1_RMS', '', LMAX, GLDAS_START, GLDAS_END + ) + TWC_file = ff.format( + 'GLDAS_TWC_RMS', reg, '', 'RAD1.5_', '', LMAX, gw_str, ds_str + ) + TWC_input = np.loadtxt(base_dir.joinpath('GLDAS', subdir, TWC_file))[ + :nmon, : + ] + (isvalid,) = np.nonzero( + np.isfinite(TWC_input[:, 2]) + & (TWC_input[:, 0] >= START_MON) + & (TWC_input[:, 0] <= END_MON) + ) + TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid, 2] ** 2) / len(isvalid)) # input estimated SLF monte carlo variance file and calculate RMS - subdir = sd.format(PROC,DREL,'_MC',LMAX,START_MON,END_MON) - SLF_file = ff.format('MC',reg,'','RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - SLF_input = np.loadtxt(mascon_dir.joinpath(subdir,SLF_file)) - SLF_RMS = np.sqrt(np.sum(SLF_input[:,1]**2)/len(SLF_input)) + subdir = sd.format(PROC, DREL, '_MC', LMAX, START_MON, END_MON) + SLF_file = ff.format( + 'MC', reg, '', 'RAD1.5_', ocean_str, LMAX, gw_str, ds_str + ) + SLF_input = np.loadtxt(mascon_dir.joinpath(subdir, SLF_file)) + SLF_RMS = np.sqrt(np.sum(SLF_input[:, 1] ** 2) / len(SLF_input)) # calculate mean of RMS over multiple reanalyses OBP_RMS = 0.0 ATM_RMS = 0.0 @@ -418,16 +591,16 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): # ivalid, = np.nonzero(np.isfinite(OBP_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(OBP_input[:,j+3])) # OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(OBP_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(OBP_input[:,2])) - OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(OBP_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(OBP_input[:, 2])) + OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid, 2] ** 2) / valid_count) # for j in range(4): # ivalid, = np.nonzero(np.isfinite(ATM_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(ATM_input[:,j+3])) # ATM_RMS += np.sqrt(np.sum(OBP_input[ATM_input,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(ATM_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(ATM_input[:,2])) - ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(ATM_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(ATM_input[:, 2])) + ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid, 2] ** 2) / valid_count) # # divide by the number of reanalyses # OBP_RMS /= 2.0 # ATM_RMS /= 4.0 @@ -436,41 +609,55 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): # number of rheologies to iterate nRheology = len(gia_files[g]) # iterate through solutions - mon = np.zeros((nmon),dtype=np.int64) - tdec = np.zeros((nmon),dtype=np.float64) - mass = np.zeros((nmon,nRheology),dtype=np.float64) - mass_m1 = np.zeros((nmon),dtype=np.float64) - satellite_error = np.zeros((nmon),dtype=np.float64) - for i,F in enumerate(FLAG): + mon = np.zeros((nmon), dtype=np.int64) + tdec = np.zeros((nmon), dtype=np.float64) + mass = np.zeros((nmon, nRheology), dtype=np.float64) + mass_m1 = np.zeros((nmon), dtype=np.float64) + satellite_error = np.zeros((nmon), dtype=np.float64) + for i, F in enumerate(FLAG): # subdirectory - subdir = sd.format(PROC,DREL,SLF[i],LMAX,START_MON,END_MON) + subdir = sd.format(PROC, DREL, SLF[i], LMAX, START_MON, END_MON) # read each GIA model - for k,GIA_FILE in enumerate(gia_files[g]): - gia_Ylms = gravtk.read_GIA_model(base_dir.joinpath(*GIA_FILE), GIA=g) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_Ylms = gravtk.read_GIA_model( + base_dir.joinpath(*GIA_FILE), GIA=g + ) gia_str = gia_Ylms['title'] - input_file = ff.format(gia_str,reg,'',atm_str,ocean_str,LMAX,gw_str,ds_str) - dinput = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) - mon[:] = dinput[:nmon,0] - tdec[:] = dinput[:nmon,1] - mass[:,k] = dinput[:nmon,2] - satellite_error[:] += dinput[:nmon,3]**2 + input_file = ff.format( + gia_str, reg, '', atm_str, ocean_str, LMAX, gw_str, ds_str + ) + dinput = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) + mon[:] = dinput[:nmon, 0] + tdec[:] = dinput[:nmon, 1] + mass[:, k] = dinput[:nmon, 2] + satellite_error[:] += dinput[:nmon, 3] ** 2 # calculate mean GIA-corrected mass change (for all Earth rheologies) gia_corrected_mean = np.mean(mass, axis=1) # GRACE satellite error component, ocean leakage (ECCO-GAD), # atmosphere leakage (ATM-GAA) and GLDAS TWC - grace_error = np.sqrt(np.sum(satellite_error/nRheology + SLF_RMS**2 + - OBP_RMS**2 + ATM_RMS**2 + TWC_RMS**2)/nmon) - if (i > 0): + grace_error = np.sqrt( + np.sum( + satellite_error / nRheology + + SLF_RMS**2 + + OBP_RMS**2 + + ATM_RMS**2 + + TWC_RMS**2 + ) + / nmon + ) + if i > 0: # add to plot with colors and label - plot_label = u'{0} \u2013 {1}'.format(plot_titles[i],plot_titles[i-1]) + plot_label = '{0} \u2013 {1}'.format( + plot_titles[i], plot_titles[i - 1] + ) residual = gia_corrected_mean - gia_corrected_mean[0] - mass_m1 # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - rnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): + tnan = np.full_like(month, np.nan, dtype=np.float64) + rnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): valid = np.count_nonzero(mon == m) if valid: - mm, = np.nonzero(mon == m) + (mm,) = np.nonzero(mon == m) tnan[d] = tdec[mm] rnan[d] = residual[mm] # plot all dates @@ -480,22 +667,26 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): error_m1 = np.copy(grace_error) # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9, - day=3,hour=12,minute=12) - ax.axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax.axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(mon == 186) - kk, = np.flatnonzero(mon == 198) + (jj,) = np.flatnonzero(mon == 186) + (kk,) = np.flatnonzero(mon == 198) # ax.axvline(tdec[jj],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) # ax.axvline(tdec[kk],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) - vs = ax.axvspan(tdec[jj],tdec[kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) + vs = ax.axvspan( + tdec[jj], tdec[kk], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (6, 3) # add horizontal line at 0 - ax.axhline(0.0, color='black', ls='dashed', dashes=(11,5), lw=0.5) + ax.axhline(0.0, color='black', ls='dashed', dashes=(11, 5), lw=0.5) # add labels - textprops = dict(size=14,weight='bold') - at = matplotlib.offsetbox.AnchoredText(fig_text[reg], - prop=textprops, pad=0, frameon=False, loc=2) + textprops = dict(size=14, weight='bold') + at = matplotlib.offsetbox.AnchoredText( + fig_text[reg], prop=textprops, pad=0, frameon=False, loc=2 + ) ax.add_artist(at) ax.set_ylabel('Mass Difference [Gt]') # set ticks @@ -504,7 +695,9 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): minor_ticks = sorted(set(np.arange(2002, 2022, 1)) - set(major_ticks)) ax.xaxis.set_ticks(minor_ticks, minor=True) axlim = ax.set_xlim(2002, 2022.25) - data_ticks = np.arange(ylimits[1]-10,ylimits[0]-ylimits[2],-ylimits[2]) + data_ticks = np.arange( + ylimits[1] - 10, ylimits[0] - ylimits[2], -ylimits[2] + ) ax.yaxis.set_ticks(data_ticks[::-1]) axlim = ax.set_ylim(ylimits[:2]) ax.get_xaxis().set_tick_params(which='both', direction='in') @@ -515,7 +708,7 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): # add x labels ax3['APIS'].set_xlabel('Time [Yr]') # add legend - lgd = ax3['APIS'].legend(loc=3,frameon=False) + lgd = ax3['APIS'].legend(loc=3, frameon=False) # lgd = ax3['APIS'].legend(loc=3,frameon=False,handletextpad=-0.2,handlelength=0) # set width, color and style of lines # lgd.get_frame().set_boxstyle('square,pad=0.1') @@ -529,56 +722,115 @@ def plot_mascon_SLF_combined(base_dir,PROC,DREL,START_MON,END_MON,MISSING): # text.set_weight('bold') # adjust plot to figure dimensions - fig.subplots_adjust(left=0.08,right=0.99,bottom=0.04,top=0.99, - hspace=0.08,wspace=0.28) - figurefile = filepath.joinpath('fig5ah_{0}_{1}.pdf'.format(PROC,DREL)) + fig.subplots_adjust( + left=0.08, right=0.99, bottom=0.04, top=0.99, hspace=0.08, wspace=0.28 + ) + figurefile = filepath.joinpath('fig5ah_{0}_{1}.pdf'.format(PROC, DREL)) plt.savefig(figurefile, format='pdf') plt.cla() plt.clf() plt.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser() # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, default=None, - choices=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + default=None, + choices=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series') - parser.add_argument('--end','-E', - type=int, default=230, - help='Ending GRACE/GRACE-FO month for time series') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167,172, - 177,178,182,200,201] - parser.add_argument('--missing','-M', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months in time series') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=230, + help='Ending GRACE/GRACE-FO month for time series', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-M', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months in time series', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run program for parameters - plot_mascon_SLF_combined(args.directory,args.center,args.release, - args.start,args.end,args.missing) + plot_mascon_SLF_combined( + args.directory, + args.center, + args.release, + args.start, + args.end, + args.missing, + ) + # run main program if __name__ == '__main__': diff --git a/scripts/plot_mascon_SLF_iterations.py b/scripts/plot_mascon_SLF_iterations.py index 65b4004b..980f7cb7 100644 --- a/scripts/plot_mascon_SLF_iterations.py +++ b/scripts/plot_mascon_SLF_iterations.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_mascon_SLF_iterations.py Written by Tyler Sutterley (05/2023) @@ -54,6 +54,7 @@ create a plot showing the effect of the iterations for supplement Written 08/2017 """ + from __future__ import print_function import inspect @@ -67,26 +68,30 @@ try: import matplotlib import matplotlib.pyplot as plt + matplotlib.rcParams['mathtext.default'] = 'regular' matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] import matplotlib.offsetbox except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import scipy.stats import scipy.special except ModuleNotFoundError: - warnings.warn("scipy not available", ImportWarning) + warnings.warn('scipy not available', ImportWarning) # current file path for the child programs filename = inspect.getframeinfo(inspect.currentframe()).filename filepath = pathlib.Path(filename).absolute().parent + # PURPOSE: plot and calculate effects of SLF on mascon time series -def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCENARIOS=False): +def plot_mascon_SLF_iterations( + base_dir, PROC, DREL, START_MON, END_MON, MISSING, SCENARIOS=False +): # directory setup - mascon_dir = base_dir.joinpath('GRACE','mascons') + mascon_dir = base_dir.joinpath('GRACE', 'mascons') # GIA and REGION GIA = {} gia_files = {} @@ -94,47 +99,77 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN REGION = {} # Simpson (2009), Peltier (2015,2018), A (ICE6G), Caron (2018) - GIA['N'] = ['SM09','ICE6G','ICE6G-D','AW13-ICE6G','Caron'] + GIA['N'] = ['SM09', 'ICE6G', 'ICE6G-D', 'AW13-ICE6G', 'Caron'] # Ivins (2013), Whitehouse (2012), Peltier (2015,2018), A (ICE6G/IJ05), Caron (2018) - GIA['S'] = ['IJ05-R2','W12a','ICE6G','ICE6G-D','AW13-ICE6G','ascii','Caron'] + GIA['S'] = [ + 'IJ05-R2', + 'W12a', + 'ICE6G', + 'ICE6G-D', + 'AW13-ICE6G', + 'ascii', + 'Caron', + ] # GIA files for each modeling group gia_files['AW13-ICE6G'] = [] - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_.1_10.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_1._10.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_.1_1.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_1._1.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_GA.txt']) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_.1_10.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_1._10.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_.1_1.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_1._1.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_GA.txt'] + ) gia_files['ascii'] = [] - gia_files['ascii'].append(['GIA','AW13','IJ05-R2','IJ05_R2_115_.2_1.5_ICE6G.txt']) - gia_files['ascii'].append(['GIA','AW13','IJ05-R2','IJ05_R2_65_.2_1.5_ICE6G.txt']) + gia_files['ascii'].append( + ['GIA', 'AW13', 'IJ05-R2', 'IJ05_R2_115_.2_1.5_ICE6G.txt'] + ) + gia_files['ascii'].append( + ['GIA', 'AW13', 'IJ05-R2', 'IJ05_R2_65_.2_1.5_ICE6G.txt'] + ) gia_files['Caron'] = [] - gia_files['Caron'].append(['GIA','Caron','expStokes_GIA.txt']) + gia_files['Caron'].append(['GIA', 'Caron', 'expStokes_GIA.txt']) gia_files['ICE6G-D'] = [] - gia_files['ICE6G-D'].append(['GIA','ICE6G','VersionD','Stokes_trend_High_Res.txt']) - gia_files['ICE6G-D'].append(['GIA','ICE6G','VersionD','Stokes_trend_VM5a_O512.txt']) + gia_files['ICE6G-D'].append( + ['GIA', 'ICE6G', 'VersionD', 'Stokes_trend_High_Res.txt'] + ) + gia_files['ICE6G-D'].append( + ['GIA', 'ICE6G', 'VersionD', 'Stokes_trend_VM5a_O512.txt'] + ) gia_files['ICE6G'] = [] - gia_files['ICE6G'].append(['GIA','ICE6G','VM5','Stokes_G_Rot_60_I6_A_VM5a']) - gia_files['ICE6G'].append(['GIA','ICE6G','VM5','Stokes_G_Rot_60_I6_A_VM5b']) + gia_files['ICE6G'].append( + ['GIA', 'ICE6G', 'VM5', 'Stokes_G_Rot_60_I6_A_VM5a'] + ) + gia_files['ICE6G'].append( + ['GIA', 'ICE6G', 'VM5', 'Stokes_G_Rot_60_I6_A_VM5b'] + ) gia_files['IJ05-R2'] = [] - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_1.5_L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_2._L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_3.2_L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_4._L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_65_.2_1.5_L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_1.5_L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_2._L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_3.2_L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_4._L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_65_.2_1.5_L120']) gia_files['SM09'] = [] - gia_files['SM09'].append(['GIA','SM09','grate_120p11.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_120p51.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_120p53.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_120p81.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p32.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p510.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p55.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p58.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p85.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p11.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p51.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p53.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p81.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p32.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p510.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p55.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p58.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p85.clm']) gia_files['W12a'] = [] - gia_files['W12a'].append(['GIA','W12a','grate_B.clm']) - gia_files['W12a'].append(['GIA','W12a','grate_L.clm']) - gia_files['W12a'].append(['GIA','W12a','grate_U.clm']) + gia_files['W12a'].append(['GIA', 'W12a', 'grate_B.clm']) + gia_files['W12a'].append(['GIA', 'W12a', 'grate_L.clm']) + gia_files['W12a'].append(['GIA', 'W12a', 'grate_U.clm']) # mean title for each GIA string gia_mean_str['IJ05-R2'] = 'Ivins_R2_mean' gia_mean_str['W12a'] = 'W12a_mean' @@ -147,7 +182,7 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN # regions REGION['N'] = 'GIS' REGION['S'] = 'AIS' - REMOVE = dict(S='AIS',N='ARC') + REMOVE = dict(S='AIS', N='ARC') # figure labels fig_text = dict(N='b)', S='a)', GIS='a)', WAIS='b)', EAIS='c)', APIS='d)') @@ -155,25 +190,31 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN # Land-Sea Mask (with Antarctica from Rignot et al., 2017) # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - LSMASK = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - landsea = gravtk.spatial().from_netCDF4(LSMASK, - date=False, varname='LSMASK') + LSMASK = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + landsea = gravtk.spatial().from_netCDF4( + LSMASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - land_function = np.zeros((nth,nphi),dtype=np.float64) + nth, nphi = landsea.shape + land_function = np.zeros((nth, nphi), dtype=np.float64) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + land_function[indx, indy] = 1.0 # calculate ocean function from land function ocean_function = 1.0 - land_function # convert ocean function into a series of spherical harmonics # (Note that LMAX=0 in the call to gen_harmonics) - Ylms = gravtk.gen_harmonics(ocean_function.T, landsea.lon, landsea.lat, - LMAX=0, PLM=np.ones((1,1,nth))) + Ylms = gravtk.gen_harmonics( + ocean_function.T, + landsea.lon, + landsea.lat, + LMAX=0, + PLM=np.ones((1, 1, nth)), + ) # total area of ocean calculated by integrating the ocean function # Average Radius of the Earth [mm] - rad_e = 10.0*gravtk.units().rad_e - ocean_area = 4.0*np.pi*(rad_e**2)*Ylms.clm[0,0] + rad_e = 10.0 * gravtk.units().rad_e + ocean_area = 4.0 * np.pi * (rad_e**2) * Ylms.clm[0, 0] # Setting output error alpha with confidence interval CONF = 0.95 @@ -182,33 +223,49 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN RUNS = 20000 # GRACE data release and months - GAP = [187,188,189,190,191,192,193,194,195,196,197,] - months = sorted(set(np.arange(START_MON,END_MON+1))-set(MISSING)-set(GAP)) + GAP = [ + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + ] + months = sorted( + set(np.arange(START_MON, END_MON + 1)) - set(MISSING) - set(GAP) + ) nmon = len(months) # start and end GRACE months for correction data - OBP_START,OBP_END = (4,254) - ATM_START,ATM_END = (4,251) - GLDAS_START,GLDAS_END = (4,254) + OBP_START, OBP_END = (4, 254) + ATM_START, ATM_END = (4, 251) + GLDAS_START, GLDAS_END = (4, 254) # input leakage file - leakage_file = mascon_dir.joinpath('HEX_TMB_LEAKAGE_SPH_CAP_MSCNS_L60', - 'HEX_TMB_LEAKAGE_HOLE_SPH_CAP_RAD1.5_L60_r250km_OCN.txt') + leakage_file = mascon_dir.joinpath( + 'HEX_TMB_LEAKAGE_SPH_CAP_MSCNS_L60', + 'HEX_TMB_LEAKAGE_HOLE_SPH_CAP_RAD1.5_L60_r250km_OCN.txt', + ) regional_leakage = {} - with open(leakage_file,'r') as f: + with open(leakage_file, 'r') as f: file_contents = f.read().splitlines() for line in file_contents: line_contents = line.split() regional_leakage[line_contents[0]] = np.float64(line_contents[1]) # flags for calculating trends to populate tables - FLAG = ['HEX','HEX'] - SLF = ['','_SLF3'] - iter_label = ['No SL','SLF'] + FLAG = ['HEX', 'HEX'] + SLF = ['', '_SLF3'] + iter_label = ['No SL', 'SLF'] # if running with all scenarios if SCENARIOS: - FLAG.extend(['HEX','HEX','HEX']) - SLF.extend(['_SLF4','_SLF5','_SLF6']) - iter_label.extend(['ISSLF','AISSLF','GISSLF']) + FLAG.extend(['HEX', 'HEX', 'HEX']) + SLF.extend(['_SLF4', '_SLF5', '_SLF6']) + iter_label.extend(['ISSLF', 'AISSLF', 'GISSLF']) # atmospheric ECMWF "jump" flag and ocean redistribution flag atm_str = 'wATM_' if (DREL == 'RL05') else '' ocean_str = 'OCN_' @@ -226,50 +283,109 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN # run for multiple regions to get trends for the table regions = [] - AIS_regions = ['AIS','WAIS','EAIS','APIS','INTERIOR','QML','IIpp','CpDc','DDpi','FpG','GH2','GH3'] - GIS_regions = ['GIS','NW','NN','NE','SW','SE'] - GIC_regions = ['CBI','CDE','ICL','SVB','FJL','SZEM','NZEM','ALK','DEN','PNW','PAT'] - HEM = ['S']*len(AIS_regions) + ['N']*len(GIS_regions) + ['N']*len(GIC_regions) - remove = ['AIS']*len(AIS_regions) + ['ARC']*len(GIS_regions) + ['ARC']*len(GIC_regions) + AIS_regions = [ + 'AIS', + 'WAIS', + 'EAIS', + 'APIS', + 'INTERIOR', + 'QML', + 'IIpp', + 'CpDc', + 'DDpi', + 'FpG', + 'GH2', + 'GH3', + ] + GIS_regions = ['GIS', 'NW', 'NN', 'NE', 'SW', 'SE'] + GIC_regions = [ + 'CBI', + 'CDE', + 'ICL', + 'SVB', + 'FJL', + 'SZEM', + 'NZEM', + 'ALK', + 'DEN', + 'PNW', + 'PAT', + ] + HEM = ( + ['S'] * len(AIS_regions) + + ['N'] * len(GIS_regions) + + ['N'] * len(GIC_regions) + ) + remove = ( + ['AIS'] * len(AIS_regions) + + ['ARC'] * len(GIS_regions) + + ['ARC'] * len(GIC_regions) + ) regions.extend(AIS_regions) regions.extend(GIS_regions) regions.extend(GIC_regions) # file with output table - tablefile = filepath.joinpath('table_1_{0}_{1}_obp_atm_twc.txt'.format(PROC,DREL)) + tablefile = filepath.joinpath( + 'table_1_{0}_{1}_obp_atm_twc.txt'.format(PROC, DREL) + ) fid = tablefile.open(mode='w', encoding='utf8') # fid = sys.stdout - for h,reg,rem in zip(HEM,regions,remove): + for h, reg, rem in zip(HEM, regions, remove): # leakage fraction for regions if reg in regional_leakage.keys(): leakage_fraction = regional_leakage[reg] else: leakage_fraction = 0.0 # read ocean bottom pressure leakage file - subdir = sd.format('AOD1B',DREL,'',LMAX,OBP_START,OBP_END) - OBP_file = ff.format('ECCO-GAD_OBP_Residuals',reg,'','',ocean_str,LMAX,gw_str,ds_str) - OBP_input = np.loadtxt(mascon_dir.joinpath(subdir,OBP_file))[:nmon,:] + subdir = sd.format('AOD1B', DREL, '', LMAX, OBP_START, OBP_END) + OBP_file = ff.format( + 'ECCO-GAD_OBP_Residuals', + reg, + '', + '', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + OBP_input = np.loadtxt(mascon_dir.joinpath(subdir, OBP_file))[:nmon, :] # read atmospheric pressure leakage file - subdir = sd.format('AOD1B',DREL,'',LMAX,ATM_START,ATM_END) + subdir = sd.format('AOD1B', DREL, '', LMAX, ATM_START, ATM_END) # ATM_file = ff.format('ATM-GAA_Residuals',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) # ATM_file = ff.format('ATM_Differences',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) - ATM_file = ff.format('ATM_Differences',reg,'','',ocean_str,LMAX,gw_str,ds_str) - ATM_input = np.loadtxt(mascon_dir.joinpath(subdir,ATM_file))[:nmon,:] + ATM_file = ff.format( + 'ATM_Differences', reg, '', '', ocean_str, LMAX, gw_str, ds_str + ) + ATM_input = np.loadtxt(mascon_dir.joinpath(subdir, ATM_file))[:nmon, :] # read GLDAS terrestrial water RMS file - subdir = sd.format('GLDAS','TWC_V2.1_RMS','',LMAX,GLDAS_START,GLDAS_END) - TWC_file = ff.format('GLDAS_TWC_RMS',reg,'','RAD1.5_','',LMAX,gw_str,ds_str) - TWC_input = np.loadtxt(base_dir.joinpath('GLDAS',subdir,TWC_file))[:nmon,:] - isvalid, = np.nonzero(np.isfinite(TWC_input[:,2]) & - (TWC_input[:,0] >= START_MON) & (TWC_input[:,0] <= END_MON)) - TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid,2]**2)/len(isvalid)) + subdir = sd.format( + 'GLDAS', 'TWC_V2.1_RMS', '', LMAX, GLDAS_START, GLDAS_END + ) + TWC_file = ff.format( + 'GLDAS_TWC_RMS', reg, '', 'RAD1.5_', '', LMAX, gw_str, ds_str + ) + TWC_input = np.loadtxt(base_dir.joinpath('GLDAS', subdir, TWC_file))[ + :nmon, : + ] + (isvalid,) = np.nonzero( + np.isfinite(TWC_input[:, 2]) + & (TWC_input[:, 0] >= START_MON) + & (TWC_input[:, 0] <= END_MON) + ) + TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid, 2] ** 2) / len(isvalid)) # input estimated SLF monte carlo variance file and calculate RMS - subdir = sd.format(PROC,DREL,'_MC',LMAX,START_MON,END_MON) - SLF_file = ff.format('MC',reg,'','RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - SLF_input = np.loadtxt(mascon_dir.joinpath(subdir,SLF_file)) - SLF_RMS = np.sqrt(np.sum(SLF_input[:,1]**2)/len(SLF_input)) + subdir = sd.format(PROC, DREL, '_MC', LMAX, START_MON, END_MON) + SLF_file = ff.format( + 'MC', reg, '', 'RAD1.5_', ocean_str, LMAX, gw_str, ds_str + ) + SLF_input = np.loadtxt(mascon_dir.joinpath(subdir, SLF_file)) + SLF_RMS = np.sqrt(np.sum(SLF_input[:, 1] ** 2) / len(SLF_input)) # read GIA uncertainty from Caron et al. (2018) - subdir = sd.format(PROC,DREL,'_SLF3',LMAX,START_MON,END_MON) - input_file = ff.format('Caron_Error',reg,'',atm_str,ocean_str,LMAX,gw_str,ds_str) - CARON_RMS = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) + subdir = sd.format(PROC, DREL, '_SLF3', LMAX, START_MON, END_MON) + input_file = ff.format( + 'Caron_Error', reg, '', atm_str, ocean_str, LMAX, gw_str, ds_str + ) + CARON_RMS = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) # calculate mean of RMS over multiple reanalyses OBP_RMS = 0.0 ATM_RMS = 0.0 @@ -277,216 +393,435 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN # ivalid, = np.nonzero(np.isfinite(OBP_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(OBP_input[:,j+3])) # OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(OBP_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(OBP_input[:,2])) - OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(OBP_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(OBP_input[:, 2])) + OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid, 2] ** 2) / valid_count) # for j in range(4): # ivalid, = np.nonzero(np.isfinite(ATM_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(ATM_input[:,j+3])) # ATM_RMS += np.sqrt(np.sum(OBP_input[ATM_input,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(ATM_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(ATM_input[:,2])) - ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(ATM_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(ATM_input[:, 2])) + ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid, 2] ** 2) / valid_count) # # divide by the number of reanalyses # OBP_RMS /= 2.0 # ATM_RMS /= 4.0 # iterate through GIA solutions - for j,g in enumerate(GIA[h]): + for j, g in enumerate(GIA[h]): print('{0} {1}'.format(reg, gia_mean_str[g]), file=fid) - mon = np.zeros((nmon),dtype=np.int64) - tdec = np.zeros((nmon),dtype=np.float64) + mon = np.zeros((nmon), dtype=np.int64) + tdec = np.zeros((nmon), dtype=np.float64) # number of rheologies to iterate nRheology = len(gia_files[g]) - mass = np.zeros((nmon,nRheology),dtype=np.float64) - complement = np.zeros((nmon),dtype=np.float64) - satellite_error = np.zeros((nmon),dtype=np.float64) - bx1 = np.zeros_like(FLAG,dtype=np.float64) - ex1 = np.zeros_like(FLAG,dtype=np.float64) - bx2 = np.zeros_like(FLAG,dtype=np.float64) - ex2 = np.zeros_like(FLAG,dtype=np.float64) + mass = np.zeros((nmon, nRheology), dtype=np.float64) + complement = np.zeros((nmon), dtype=np.float64) + satellite_error = np.zeros((nmon), dtype=np.float64) + bx1 = np.zeros_like(FLAG, dtype=np.float64) + ex1 = np.zeros_like(FLAG, dtype=np.float64) + bx2 = np.zeros_like(FLAG, dtype=np.float64) + ex2 = np.zeros_like(FLAG, dtype=np.float64) # iterate through solutions - for i,F in enumerate(FLAG): + for i, F in enumerate(FLAG): # subdirectory - subdir = sd.format(PROC,DREL,SLF[i],LMAX,START_MON,END_MON) + subdir = sd.format(PROC, DREL, SLF[i], LMAX, START_MON, END_MON) # read each GIA model - for k,GIA_FILE in enumerate(gia_files[g]): - gia_Ylms = gravtk.read_GIA_model(base_dir.joinpath(*GIA_FILE), GIA=g) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_Ylms = gravtk.read_GIA_model( + base_dir.joinpath(*GIA_FILE), GIA=g + ) gia_str = gia_Ylms['title'] - input_file = ff.format(gia_str,reg,'',atm_str,ocean_str,LMAX,gw_str,ds_str) - dinput = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) - mon[:] = dinput[:nmon,0].astype(np.int64) - tdec[:] = dinput[:nmon,1] - mass[:,k] = dinput[:nmon,2] - satellite_error[:] += dinput[:nmon,3]**2 + input_file = ff.format( + gia_str, + reg, + '', + atm_str, + ocean_str, + LMAX, + gw_str, + ds_str, + ) + dinput = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) + mon[:] = dinput[:nmon, 0].astype(np.int64) + tdec[:] = dinput[:nmon, 1] + mass[:, k] = dinput[:nmon, 2] + satellite_error[:] += dinput[:nmon, 3] ** 2 # read remove file to calculate complement - input_file = ff.format(gia_str,rem,'',atm_str,ocean_str,LMAX,gw_str,ds_str) - rinput = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) - complement += (rinput[:nmon,2] - mass[:,k]) + input_file = ff.format( + gia_str, + rem, + '', + atm_str, + ocean_str, + LMAX, + gw_str, + ds_str, + ) + rinput = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) + complement += rinput[:nmon, 2] - mass[:, k] # calculate mean GIA-corrected mass change (for all Earth rheologies) gia_corrected_mean = np.mean(mass, axis=1) # GRACE satellite error component with SLF uncertainty - grace_error = np.sqrt(satellite_error/np.float64(nRheology)) + grace_error = np.sqrt(satellite_error / np.float64(nRheology)) # calculate variance off of mean for calculating GIA uncertainty gia_corrected_variance = np.zeros((nmon)) gia_corrected_minmax = np.zeros((nmon)) # calculate complement time series of region - complement = complement/np.float64(nRheology) + complement = complement / np.float64(nRheology) # calculate GIA uncertainty as "worst-case" not RMS - for k,GIA_FILE in enumerate(gia_files[g]): - gia_corrected_variance+=np.abs(mass[:,k]-gia_corrected_mean) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_corrected_variance += np.abs( + mass[:, k] - gia_corrected_mean + ) # calculate GIA uncertainty as "worst-case" min max error for t in range(nmon): - gia_corrected_minmax[t]=np.abs(np.max(mass[t,:])-np.min(mass[t,:])) + gia_corrected_minmax[t] = np.abs( + np.max(mass[t, :]) - np.min(mass[t, :]) + ) # if using Caron et al. (2018) use RMS of covariance errors - if (g == 'Caron'): + if g == 'Caron': # calculate uncertainty in mass drift - gia_corrected_error = CARON_RMS*(tdec-tdec.mean()) - gia_corrected_minmax = CARON_RMS*(tdec-tdec.mean()) + gia_corrected_error = CARON_RMS * (tdec - tdec.mean()) + gia_corrected_minmax = CARON_RMS * (tdec - tdec.mean()) gia_error_rate = np.copy(CARON_RMS) gia_minmax_rate = np.copy(CARON_RMS) tstar = 1.0 else: # calculate uncertainty in mean GIA - gia_corrected_error = gia_corrected_variance/(np.float64(nRheology)-1.0) - gia_corrected_error = gia_corrected_error*np.sign(tdec-tdec.mean()) - gia_corrected_minmax = gia_corrected_minmax*np.sign(tdec-tdec.mean())/2.0 - gia_error_rate = (gia_corrected_error[-1]-gia_corrected_error[0])/(tdec[-1]-tdec[0]) - gia_minmax_rate = (gia_corrected_minmax[-1]-gia_corrected_minmax[0])/(tdec[-1]-tdec[0]) + gia_corrected_error = gia_corrected_variance / ( + np.float64(nRheology) - 1.0 + ) + gia_corrected_error = gia_corrected_error * np.sign( + tdec - tdec.mean() + ) + gia_corrected_minmax = ( + gia_corrected_minmax * np.sign(tdec - tdec.mean()) / 2.0 + ) + gia_error_rate = ( + gia_corrected_error[-1] - gia_corrected_error[0] + ) / (tdec[-1] - tdec[0]) + gia_minmax_rate = ( + gia_corrected_minmax[-1] - gia_corrected_minmax[0] + ) / (tdec[-1] - tdec[0]) # calculate GIA errors at confidence interval # t.ppf parallels tinv in matlab - tstar = scipy.stats.t.ppf(1.0-(alpha/2.0),nRheology-1.0) if g in ('IJ05-R2','SM09') else 2.0 + tstar = ( + scipy.stats.t.ppf(1.0 - (alpha / 2.0), nRheology - 1.0) + if g in ('IJ05-R2', 'SM09') + else 2.0 + ) # GIA errors at confidence interval - gia_corrected_conf = tstar*np.abs(gia_error_rate) + gia_corrected_conf = tstar * np.abs(gia_error_rate) # build terms for S2 tidal aliasing TERMS = gravtk.time_series.aliasing_terms(tdec) # fit a linear model for trend and acceleration - bfit = gravtk.time_series.regress(tdec, gia_corrected_mean, - ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS) - ofit = gravtk.time_series.regress(tdec, gia_corrected_mean, - DATA_ERR=OBP_RMS, ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS) - afit = gravtk.time_series.regress(tdec, gia_corrected_mean, - DATA_ERR=ATM_RMS, ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS) - tfit = gravtk.time_series.regress(tdec, gia_corrected_mean, - DATA_ERR=TWC_RMS, ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS) - cfit = gravtk.time_series.regress(tdec, complement, - ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS) + bfit = gravtk.time_series.regress( + tdec, + gia_corrected_mean, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + ) + ofit = gravtk.time_series.regress( + tdec, + gia_corrected_mean, + DATA_ERR=OBP_RMS, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + ) + afit = gravtk.time_series.regress( + tdec, + gia_corrected_mean, + DATA_ERR=ATM_RMS, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + ) + tfit = gravtk.time_series.regress( + tdec, + gia_corrected_mean, + DATA_ERR=TWC_RMS, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + ) + cfit = gravtk.time_series.regress( + tdec, complement, ORDER=2, CYCLES=[0.5, 1.0], TERMS=TERMS + ) # run a monte carlo regression for trend and acceleration combined_error = np.sqrt(grace_error**2 + SLF_RMS**2) - mc = monte_carlo_regress(tdec, gia_corrected_mean, combined_error, - tstar*(gia_corrected_error-gia_corrected_error[0]), - leakage_fraction*complement, OBP_RMS, ATM_RMS, TWC_RMS, - ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS, RUNS=RUNS, - RMS=False, CONF=CONF) + mc = monte_carlo_regress( + tdec, + gia_corrected_mean, + combined_error, + tstar * (gia_corrected_error - gia_corrected_error[0]), + leakage_fraction * complement, + OBP_RMS, + ATM_RMS, + TWC_RMS, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + RUNS=RUNS, + RMS=False, + CONF=CONF, + ) # print trend (x1) coefficients to file - bx1[i],cx1 = bfit['beta'][1],cfit['beta'][1] - ex1[i] = bfit['error'][1] + gia_corrected_conf + \ - np.abs(leakage_fraction*cx1) + np.abs(ofit['error'][1]) + \ - np.abs(afit['error'][1]) + np.abs(tfit['error'][1]) + bx1[i], cx1 = bfit['beta'][1], cfit['beta'][1] + ex1[i] = ( + bfit['error'][1] + + gia_corrected_conf + + np.abs(leakage_fraction * cx1) + + np.abs(ofit['error'][1]) + + np.abs(afit['error'][1]) + + np.abs(tfit['error'][1]) + ) # print acceleration (x2) coefficients to file - bx2[i],cx2 = 2.0*bfit['beta'][2],2.0*cfit['beta'][2] - ex2[i] = 2.0*bfit['error'][2] + np.abs(leakage_fraction*cx2) + \ - np.abs(2.0*ofit['error'][2]) + np.abs(2.0*afit['error'][2]) + \ - np.abs(2.0*tfit['error'][2]) - args = (iter_label[i],bx1[i],ex1[i],bx2[i],ex2[i]) - print('{0}\tx1={1:f}+/-{2:f}\tx2={3:f}+/-{4:f}'.format(*args),file=fid) + bx2[i], cx2 = 2.0 * bfit['beta'][2], 2.0 * cfit['beta'][2] + ex2[i] = ( + 2.0 * bfit['error'][2] + + np.abs(leakage_fraction * cx2) + + np.abs(2.0 * ofit['error'][2]) + + np.abs(2.0 * afit['error'][2]) + + np.abs(2.0 * tfit['error'][2]) + ) + args = (iter_label[i], bx1[i], ex1[i], bx2[i], ex2[i]) + print( + '{0}\tx1={1:f}+/-{2:f}\tx2={3:f}+/-{4:f}'.format(*args), + file=fid, + ) # print monte carlo trend and acceleration coefficients to file - a=(mc['beta'][1],mc['error'][1],2.*mc['beta'][2],2.*mc['error'][2]) - print('\tmx1={0:f}+/-{1:f}\tmx2={2:f}+/-{3:f}'.format(*a),file=fid) + a = ( + mc['beta'][1], + mc['error'][1], + 2.0 * mc['beta'][2], + 2.0 * mc['error'][2], + ) + print( + '\tmx1={0:f}+/-{1:f}\tmx2={2:f}+/-{3:f}'.format(*a), + file=fid, + ) # converting gigatonnes to milligrams then to mm sea level - mm_sealevel = -1e18*(gia_corrected_mean-gia_corrected_mean[0])/ocean_area - cumulative_gia = tstar*(gia_corrected_error[-1]-gia_corrected_error[0]) - mm_sealevel_error = 1e18*np.sqrt(np.sum(satellite_error/nRheology + - SLF_RMS**2 + OBP_RMS**2 + ATM_RMS**2 + TWC_RMS**2)/nmon + - cumulative_gia**2)/ocean_area - args = (mm_sealevel[-1],mm_sealevel_error) - print('\ttotal sea level: {0:f}+/-{1:f} mm'.format(*args),file=fid) + mm_sealevel = ( + -1e18 + * (gia_corrected_mean - gia_corrected_mean[0]) + / ocean_area + ) + cumulative_gia = tstar * ( + gia_corrected_error[-1] - gia_corrected_error[0] + ) + mm_sealevel_error = ( + 1e18 + * np.sqrt( + np.sum( + satellite_error / nRheology + + SLF_RMS**2 + + OBP_RMS**2 + + ATM_RMS**2 + + TWC_RMS**2 + ) + / nmon + + cumulative_gia**2 + ) + / ocean_area + ) + args = (mm_sealevel[-1], mm_sealevel_error) + print( + '\ttotal sea level: {0:f}+/-{1:f} mm'.format(*args), + file=fid, + ) # if running with all scenarios if SCENARIOS: # print percent differences - dx1 = 100.0*np.abs((bx1[1]-bx1[0])/bx1[0]) - dx2 = 100.0*np.abs((bx2[1]-bx2[0])/bx2[0]) - print('(2-1)%\tx1={0:f}\t\t\tx2:{1:f}'.format(dx1,dx2),file=fid) - dx1 = 100.0*np.abs((bx1[2]-bx1[1])/bx1[1]) - dx2 = 100.0*np.abs((bx2[2]-bx2[1])/bx2[1]) - print('(3-2)%\tx1={0:f}\t\t\tx2:{1:f}'.format(dx1,dx2),file=fid) + dx1 = 100.0 * np.abs((bx1[1] - bx1[0]) / bx1[0]) + dx2 = 100.0 * np.abs((bx2[1] - bx2[0]) / bx2[0]) + print( + '(2-1)%\tx1={0:f}\t\t\tx2:{1:f}'.format(dx1, dx2), file=fid + ) + dx1 = 100.0 * np.abs((bx1[2] - bx1[1]) / bx1[1]) + dx2 = 100.0 * np.abs((bx2[2] - bx2[1]) / bx2[1]) + print( + '(3-2)%\tx1={0:f}\t\t\tx2:{1:f}'.format(dx1, dx2), file=fid + ) # calculate uncertainties associated with geophysical corrections - emc = monte_carlo_regress(tdec, np.ones((nmon)), grace_error, - 0.0, 0.0, 0.0, 0.0, 0.0, ORDER=2, CYCLES=[0.5,1.0], - TERMS=TERMS, RUNS=RUNS, RMS=False, CONF=CONF) - smc = monte_carlo_regress(tdec, np.ones((nmon)), SLF_RMS, - 0.0, 0.0, 0.0, 0.0, 0.0, ORDER=2, CYCLES=[0.5,1.0], - TERMS=TERMS, RUNS=RUNS, RMS=False, CONF=CONF) - lmc = monte_carlo_regress(tdec, np.ones((nmon)), 0.0, 0.0, - leakage_fraction*complement, 0.0, 0.0, 0.0, - ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS, RUNS=RUNS, - RMS=False, CONF=CONF) - omc = monte_carlo_regress(tdec, np.ones((nmon)), 0.0, 0.0, 0.0, - OBP_RMS, 0.0, 0.0, ORDER=2, CYCLES=[0.5,1.0], - TERMS=TERMS, RUNS=RUNS, RMS=False, CONF=CONF) - amc = monte_carlo_regress(tdec, np.ones((nmon)), 0.0, 0.0, 0.0, - 0.0, ATM_RMS, 0.0, ORDER=2, CYCLES=[0.5,1.0], - TERMS=TERMS, RUNS=RUNS, RMS=False, CONF=CONF) - tmc = monte_carlo_regress(tdec, np.ones((nmon)), 0.0, 0.0, 0.0, - 0.0, 0.0, TWC_RMS, ORDER=2, CYCLES=[0.5,1.0], - TERMS=TERMS, RUNS=RUNS, RMS=False, CONF=CONF) + emc = monte_carlo_regress( + tdec, + np.ones((nmon)), + grace_error, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + RUNS=RUNS, + RMS=False, + CONF=CONF, + ) + smc = monte_carlo_regress( + tdec, + np.ones((nmon)), + SLF_RMS, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + RUNS=RUNS, + RMS=False, + CONF=CONF, + ) + lmc = monte_carlo_regress( + tdec, + np.ones((nmon)), + 0.0, + 0.0, + leakage_fraction * complement, + 0.0, + 0.0, + 0.0, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + RUNS=RUNS, + RMS=False, + CONF=CONF, + ) + omc = monte_carlo_regress( + tdec, + np.ones((nmon)), + 0.0, + 0.0, + 0.0, + OBP_RMS, + 0.0, + 0.0, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + RUNS=RUNS, + RMS=False, + CONF=CONF, + ) + amc = monte_carlo_regress( + tdec, + np.ones((nmon)), + 0.0, + 0.0, + 0.0, + 0.0, + ATM_RMS, + 0.0, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + RUNS=RUNS, + RMS=False, + CONF=CONF, + ) + tmc = monte_carlo_regress( + tdec, + np.ones((nmon)), + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + TWC_RMS, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + RUNS=RUNS, + RMS=False, + CONF=CONF, + ) # print uncertainties associated with geophysical corrections - args = (emc['error'][1],2.0*emc['error'][2]) - print('grace={0:f}\tacc={1:f}'.format(*args),file=fid) - args = (smc['error'][1],2.0*smc['error'][2]) - print('slf={0:f}\tacc={1:f}'.format(*args),file=fid) - args = (omc['error'][1],2.0*omc['error'][2]) - print('obp={0:f}\tacc={1:f}'.format(*args),file=fid) - args = (amc['error'][1],2.0*amc['error'][2]) - print('atm={0:f}\tacc={1:f}'.format(*args),file=fid) - args = (tmc['error'][1],2.0*tmc['error'][2]) - print('tws={0:f}\tacc={1:f}'.format(*args),file=fid) - args = (lmc['error'][1],2.0*lmc['error'][2]) - print('leak={0:f}\tacc={1:f}'.format(*args),file=fid) - print('gia={0:f}\n'.format(gia_corrected_conf),file=fid) + args = (emc['error'][1], 2.0 * emc['error'][2]) + print('grace={0:f}\tacc={1:f}'.format(*args), file=fid) + args = (smc['error'][1], 2.0 * smc['error'][2]) + print('slf={0:f}\tacc={1:f}'.format(*args), file=fid) + args = (omc['error'][1], 2.0 * omc['error'][2]) + print('obp={0:f}\tacc={1:f}'.format(*args), file=fid) + args = (amc['error'][1], 2.0 * amc['error'][2]) + print('atm={0:f}\tacc={1:f}'.format(*args), file=fid) + args = (tmc['error'][1], 2.0 * tmc['error'][2]) + print('tws={0:f}\tacc={1:f}'.format(*args), file=fid) + args = (lmc['error'][1], 2.0 * lmc['error'][2]) + print('leak={0:f}\tacc={1:f}'.format(*args), file=fid) + print('gia={0:f}\n'.format(gia_corrected_conf), file=fid) # flags for creating plots - FLAG = ['HEX','HEX'] + FLAG = ['HEX', 'HEX'] mm_total = np.zeros((2)) mm_error = np.zeros((2)) - plot_colors = ['darkorchid','mediumseagreen'] - plot_title = ['No Sea Level Correction','Sea Level Fingerprint'] - iter_label = ['No SL','SLF'] + plot_colors = ['darkorchid', 'mediumseagreen'] + plot_title = ['No Sea Level Correction', 'Sea Level Fingerprint'] + iter_label = ['No SL', 'SLF'] # create a set of months - month = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + month = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) # create figure axis ax = {} ax1 = {} - fig, (ax['S'],ax['N']) = plt.subplots(num=1,ncols=2,figsize=(8.5,4)) + fig, (ax['S'], ax['N']) = plt.subplots(num=1, ncols=2, figsize=(8.5, 4)) # y limits for plot - ylimits = [-5200,1200,500] - for h,reg in REGION.items(): + ylimits = [-5200, 1200, 500] + for h, reg in REGION.items(): # set ice sheet only for AIS and GIS - SLF = ['','_SLF3','_SLF6'] if (reg == 'GIS') else ['','_SLF3','_SLF5'] + SLF = ( + ['', '_SLF3', '_SLF6'] if (reg == 'GIS') else ['', '_SLF3', '_SLF5'] + ) # leakage fraction for regions leakage_fraction = regional_leakage[reg] # read ocean bottom pressure leakage file - subdir = sd.format('AOD1B',DREL,'',LMAX,OBP_START,OBP_END) - OBP_file = ff.format('ECCO-GAD_OBP_Residuals',reg,'','',ocean_str,LMAX,gw_str,ds_str) - OBP_input = np.loadtxt(mascon_dir.joinpath(subdir,OBP_file))[:nmon,:] + subdir = sd.format('AOD1B', DREL, '', LMAX, OBP_START, OBP_END) + OBP_file = ff.format( + 'ECCO-GAD_OBP_Residuals', + reg, + '', + '', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + OBP_input = np.loadtxt(mascon_dir.joinpath(subdir, OBP_file))[:nmon, :] # read atmospheric pressure leakage file - subdir = sd.format('AOD1B',DREL,'',LMAX,ATM_START,ATM_END) + subdir = sd.format('AOD1B', DREL, '', LMAX, ATM_START, ATM_END) # ATM_file = ff.format('ATM-GAA_Residuals',reg,'_3D','',ocean_str,LMAX,gw_str) # ATM_file = ff.format('ATM_Differences',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) - ATM_file = ff.format('ATM_Differences',reg,'','',ocean_str,LMAX,gw_str,ds_str) - ATM_input = np.loadtxt(mascon_dir.joinpath(subdir,ATM_file))[:nmon,:] + ATM_file = ff.format( + 'ATM_Differences', reg, '', '', ocean_str, LMAX, gw_str, ds_str + ) + ATM_input = np.loadtxt(mascon_dir.joinpath(subdir, ATM_file))[:nmon, :] # read GLDAS terrestrial water RMS file - subdir = sd.format('GLDAS','TWC_V2.1_RMS','',LMAX,GLDAS_START,GLDAS_END) - TWC_file = ff.format('GLDAS_TWC_RMS',reg,'','RAD1.5_','',LMAX,gw_str,ds_str) - TWC_input = np.loadtxt(base_dir.joinpath('GLDAS',subdir,TWC_file))[:nmon,:] - isvalid, = np.nonzero(np.isfinite(TWC_input[:,2]) & - (TWC_input[:,0] >= START_MON) & (TWC_input[:,0] <= END_MON)) - TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid,2]**2)/len(isvalid)) + subdir = sd.format( + 'GLDAS', 'TWC_V2.1_RMS', '', LMAX, GLDAS_START, GLDAS_END + ) + TWC_file = ff.format( + 'GLDAS_TWC_RMS', reg, '', 'RAD1.5_', '', LMAX, gw_str, ds_str + ) + TWC_input = np.loadtxt(base_dir.joinpath('GLDAS', subdir, TWC_file))[ + :nmon, : + ] + (isvalid,) = np.nonzero( + np.isfinite(TWC_input[:, 2]) + & (TWC_input[:, 0] >= START_MON) + & (TWC_input[:, 0] <= END_MON) + ) + TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid, 2] ** 2) / len(isvalid)) # input estimated SLF monte carlo variance file and calculate RMS - subdir = sd.format(PROC,DREL,'_MC',LMAX,START_MON,END_MON) - SLF_file = ff.format('MC',reg,'','RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - SLF_input = np.loadtxt(mascon_dir.joinpath(subdir,SLF_file)) - SLF_RMS = np.sqrt(np.sum(SLF_input[:,1]**2)/len(SLF_input)) + subdir = sd.format(PROC, DREL, '_MC', LMAX, START_MON, END_MON) + SLF_file = ff.format( + 'MC', reg, '', 'RAD1.5_', ocean_str, LMAX, gw_str, ds_str + ) + SLF_input = np.loadtxt(mascon_dir.joinpath(subdir, SLF_file)) + SLF_RMS = np.sqrt(np.sum(SLF_input[:, 1] ** 2) / len(SLF_input)) # calculate mean of RMS over multiple reanalyses OBP_RMS = 0.0 ATM_RMS = 0.0 @@ -494,16 +829,16 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN # ivalid, = np.nonzero(np.isfinite(OBP_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(OBP_input[:,j+3])) # OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(OBP_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(OBP_input[:,2])) - OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(OBP_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(OBP_input[:, 2])) + OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid, 2] ** 2) / valid_count) # for j in range(4): # ivalid, = np.nonzero(np.isfinite(ATM_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(ATM_input[:,j+3])) # ATM_RMS += np.sqrt(np.sum(OBP_input[ATM_input,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(ATM_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(ATM_input[:,2])) - ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(ATM_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(ATM_input[:, 2])) + ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid, 2] ** 2) / valid_count) # # divide by the number of reanalyses # OBP_RMS /= 2.0 # ATM_RMS /= 4.0 @@ -512,131 +847,225 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN # number of rheologies to iterate nRheology = len(gia_files[g]) # iterate through solutions - mon = np.zeros((nmon),dtype=np.int64) - tdec = np.zeros((nmon),dtype=np.float64) - mass = np.zeros((nmon,nRheology),dtype=np.float64) - satellite_error = np.zeros((nmon),dtype=np.float64) - complement = np.zeros((nmon),dtype=np.float64) - bx1 = np.zeros_like(FLAG,dtype=np.float64) - ex1 = np.zeros_like(FLAG,dtype=np.float64) - bx2 = np.zeros_like(FLAG,dtype=np.float64) - ex2 = np.zeros_like(FLAG,dtype=np.float64) - for i,F in enumerate(FLAG): + mon = np.zeros((nmon), dtype=np.int64) + tdec = np.zeros((nmon), dtype=np.float64) + mass = np.zeros((nmon, nRheology), dtype=np.float64) + satellite_error = np.zeros((nmon), dtype=np.float64) + complement = np.zeros((nmon), dtype=np.float64) + bx1 = np.zeros_like(FLAG, dtype=np.float64) + ex1 = np.zeros_like(FLAG, dtype=np.float64) + bx2 = np.zeros_like(FLAG, dtype=np.float64) + ex2 = np.zeros_like(FLAG, dtype=np.float64) + for i, F in enumerate(FLAG): # subdirectory - subdir = sd.format(PROC,DREL,SLF[i],LMAX,START_MON,END_MON) + subdir = sd.format(PROC, DREL, SLF[i], LMAX, START_MON, END_MON) # read each GIA model - for k,GIA_FILE in enumerate(gia_files[g]): - gia_Ylms = gravtk.read_GIA_model(base_dir.joinpath(*GIA_FILE), GIA=g) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_Ylms = gravtk.read_GIA_model( + base_dir.joinpath(*GIA_FILE), GIA=g + ) gia_str = gia_Ylms['title'] - input_file = ff.format(gia_str,reg,'',atm_str,ocean_str,LMAX,gw_str,ds_str) - dinput = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) - mon[:] = dinput[:nmon,0].astype(np.int64) - tdec[:] = dinput[:nmon,1] - mass[:,k] = dinput[:nmon,2] - satellite_error[:] += dinput[:nmon,3]**2 + input_file = ff.format( + gia_str, reg, '', atm_str, ocean_str, LMAX, gw_str, ds_str + ) + dinput = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) + mon[:] = dinput[:nmon, 0].astype(np.int64) + tdec[:] = dinput[:nmon, 1] + mass[:, k] = dinput[:nmon, 2] + satellite_error[:] += dinput[:nmon, 3] ** 2 # read remove file to calculate complement - input_file = ff.format(gia_str,REMOVE[h],'',atm_str,ocean_str,LMAX,gw_str,ds_str) - rinput = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) - complement += (rinput[:nmon,2] - mass[:,k]) + input_file = ff.format( + gia_str, + REMOVE[h], + '', + atm_str, + ocean_str, + LMAX, + gw_str, + ds_str, + ) + rinput = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) + complement += rinput[:nmon, 2] - mass[:, k] # ax[h].plot(tdec,mass[:,k]-mass[0,k]) # calculate mean GIA-corrected mass change (for all Earth rheologies) gia_corrected_mean = np.mean(mass, axis=1) # GRACE satellite error component, ocean leakage (ECCO-GAD), # atmosphere leakage (ATM-GAA) and GLDAS TWC - grace_error = np.sqrt(np.sum(satellite_error/nRheology + SLF_RMS**2 + - OBP_RMS**2 + ATM_RMS**2 + TWC_RMS**2)/nmon) + grace_error = np.sqrt( + np.sum( + satellite_error / nRheology + + SLF_RMS**2 + + OBP_RMS**2 + + ATM_RMS**2 + + TWC_RMS**2 + ) + / nmon + ) # calculate variance off of mean for calculating GIA uncertainty gia_corrected_variance = np.zeros((nmon)) gia_corrected_minmax = np.zeros((nmon)) # calculate GIA uncertainty as "worst-case" not RMS - for k,GIA_FILE in enumerate(gia_files[g]): - gia_corrected_variance+=np.abs(mass[:,k]-gia_corrected_mean) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_corrected_variance += np.abs( + mass[:, k] - gia_corrected_mean + ) # calculate GIA uncertainty as "worst-case" min max error for t in range(nmon): - gia_corrected_minmax[t]=np.abs(np.max(mass[t,:])-np.min(mass[t,:])) + gia_corrected_minmax[t] = np.abs( + np.max(mass[t, :]) - np.min(mass[t, :]) + ) # calculate uncertainty in mean GIA - gia_corrected_error = gia_corrected_variance/(np.float64(nRheology)-1.0) - gia_corrected_error = gia_corrected_error*np.sign(tdec-tdec.mean()) - gia_corrected_minmax = gia_corrected_minmax*np.sign(tdec-tdec.mean()) - gia_error_rate = (gia_corrected_error[-1]-gia_corrected_error[0])/(tdec[-1]-tdec[0]) - gia_minmax_rate = (gia_corrected_minmax[-1]-gia_corrected_minmax[0])/(tdec[-1]-tdec[0]) + gia_corrected_error = gia_corrected_variance / ( + np.float64(nRheology) - 1.0 + ) + gia_corrected_error = gia_corrected_error * np.sign( + tdec - tdec.mean() + ) + gia_corrected_minmax = gia_corrected_minmax * np.sign( + tdec - tdec.mean() + ) + gia_error_rate = ( + gia_corrected_error[-1] - gia_corrected_error[0] + ) / (tdec[-1] - tdec[0]) + gia_minmax_rate = ( + gia_corrected_minmax[-1] - gia_corrected_minmax[0] + ) / (tdec[-1] - tdec[0]) # calculate GIA errors at confidence interval # t.ppf parallels tinv in matlab - tstar = scipy.stats.t.ppf(1.0-(alpha/2.0),nRheology-1.0) if g in ('IJ05-R2','SM09') else 2.0 + tstar = ( + scipy.stats.t.ppf(1.0 - (alpha / 2.0), nRheology - 1.0) + if g in ('IJ05-R2', 'SM09') + else 2.0 + ) # tstar = 2.0 - gia_corrected_conf = tstar*np.abs(gia_error_rate) + gia_corrected_conf = tstar * np.abs(gia_error_rate) # build terms for S2 tidal aliasing TERMS = gravtk.time_series.aliasing_terms(tdec) # fit a linear model for trend and acceleration - bfit = gravtk.time_series.regress(tdec, gia_corrected_mean, - ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS) - ofit = gravtk.time_series.regress(tdec, gia_corrected_mean, DATA_ERR=OBP_RMS, - ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS) - afit = gravtk.time_series.regress(tdec, gia_corrected_mean, DATA_ERR=ATM_RMS, - ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS) - tfit = gravtk.time_series.regress(tdec, gia_corrected_mean, DATA_ERR=TWC_RMS, - ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS) - cfit = gravtk.time_series.regress(tdec, complement, - ORDER=2, CYCLES=[0.5,1.0], TERMS=TERMS) - bx1[i],cx1 = bfit['beta'][1], cfit['beta'][1] - ex1[i] = bfit['error'][1] + gia_corrected_conf + \ - np.abs(leakage_fraction*cx1) + np.abs(ofit['error'][1]) + \ - np.abs(afit['error'][1]) + np.abs(tfit['error'][1]) - bx2[i],cx2 = 2.0*bfit['beta'][2],2.0*cfit['beta'][2] - ex2[i] = 2.0*bfit['error'][2] + np.abs(leakage_fraction*cx2) + \ - np.abs(2.0*ofit['error'][2]) + np.abs(2.0*afit['error'][2]) + \ - np.abs(2.0*tfit['error'][2]) - args = (i,bx1[i],ex1[i],bx2[i],ex2[i]) + bfit = gravtk.time_series.regress( + tdec, + gia_corrected_mean, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + ) + ofit = gravtk.time_series.regress( + tdec, + gia_corrected_mean, + DATA_ERR=OBP_RMS, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + ) + afit = gravtk.time_series.regress( + tdec, + gia_corrected_mean, + DATA_ERR=ATM_RMS, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + ) + tfit = gravtk.time_series.regress( + tdec, + gia_corrected_mean, + DATA_ERR=TWC_RMS, + ORDER=2, + CYCLES=[0.5, 1.0], + TERMS=TERMS, + ) + cfit = gravtk.time_series.regress( + tdec, complement, ORDER=2, CYCLES=[0.5, 1.0], TERMS=TERMS + ) + bx1[i], cx1 = bfit['beta'][1], cfit['beta'][1] + ex1[i] = ( + bfit['error'][1] + + gia_corrected_conf + + np.abs(leakage_fraction * cx1) + + np.abs(ofit['error'][1]) + + np.abs(afit['error'][1]) + + np.abs(tfit['error'][1]) + ) + bx2[i], cx2 = 2.0 * bfit['beta'][2], 2.0 * cfit['beta'][2] + ex2[i] = ( + 2.0 * bfit['error'][2] + + np.abs(leakage_fraction * cx2) + + np.abs(2.0 * ofit['error'][2]) + + np.abs(2.0 * afit['error'][2]) + + np.abs(2.0 * tfit['error'][2]) + ) + args = (i, bx1[i], ex1[i], bx2[i], ex2[i]) # add to plot with colors and label # \u00B1 is the unicode symbol for plus-minus # args = (iter_label[i],bx1[i],ex1[i]) # plot_label = u'{0}: {1:0.1f}\u00B1{2:0.1f} Gt/yr'.format(*args) plot_label = plot_title[i] # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - mnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): + tnan = np.full_like(month, np.nan, dtype=np.float64) + mnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): valid = np.count_nonzero(mon == m) if valid: - mm, = np.nonzero(mon == m) + (mm,) = np.nonzero(mon == m) tnan[d] = tdec[mm] - mnan[d] = gia_corrected_mean[mm]-gia_corrected_mean[0] + mnan[d] = gia_corrected_mean[mm] - gia_corrected_mean[0] # plot all dates - ax[h].plot(tnan, mnan, color=plot_colors[i], label=plot_label, zorder=2) + ax[h].plot( + tnan, mnan, color=plot_colors[i], label=plot_label, zorder=2 + ) # fill between monthly errors - ax[h].fill_between(tnan, mnan-grace_error, y2=mnan+grace_error, - color=plot_colors[i], alpha=0.5, zorder=1) + ax[h].fill_between( + tnan, + mnan - grace_error, + y2=mnan + grace_error, + color=plot_colors[i], + alpha=0.5, + zorder=1, + ) # converting gigatonnes to milligrams then to mm sea level - mm_sealevel = -1e18*(gia_corrected_mean-gia_corrected_mean[0])/ocean_area - cumulative_gia = tstar*(gia_corrected_error[-1]-gia_corrected_error[0]) - mm_sealevel_error = 1e18*np.sqrt(grace_error**2 + cumulative_gia**2)/ocean_area - args = (reg,iter_label[i],mm_sealevel[-1],mm_sealevel_error) - print('{0} {1}: {2:f}+/-{3:f} mm'.format(*args),file=fid) + mm_sealevel = ( + -1e18 + * (gia_corrected_mean - gia_corrected_mean[0]) + / ocean_area + ) + cumulative_gia = tstar * ( + gia_corrected_error[-1] - gia_corrected_error[0] + ) + mm_sealevel_error = ( + 1e18 * np.sqrt(grace_error**2 + cumulative_gia**2) / ocean_area + ) + args = (reg, iter_label[i], mm_sealevel[-1], mm_sealevel_error) + print('{0} {1}: {2:f}+/-{3:f} mm'.format(*args), file=fid) # add to totals (error in quadrature) mm_total[i] += mm_sealevel[-1] mm_error[i] += mm_sealevel_error**2 # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9, - day=3,hour=12,minute=12) - ax[h].axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax[h].axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(mon == 186) - kk, = np.flatnonzero(mon == 198) + (jj,) = np.flatnonzero(mon == 186) + (kk,) = np.flatnonzero(mon == 198) # ax[h].axvline(tdec[jj],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) # ax[h].axvline(tdec[kk],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) - vs = ax[h].axvspan(tdec[jj],tdec[kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) + vs = ax[h].axvspan( + tdec[jj], tdec[kk], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (6, 3) # add labels - textprops = dict(size=14,weight='bold') - at = matplotlib.offsetbox.AnchoredText(fig_text[h], - prop=textprops, pad=0, frameon=False, loc=2) + textprops = dict(size=14, weight='bold') + at = matplotlib.offsetbox.AnchoredText( + fig_text[h], prop=textprops, pad=0, frameon=False, loc=2 + ) ax[h].add_artist(at) - data_ticks = np.arange(np.floor(tdec[0]),np.ceil(tdec[-1])+1,2) + data_ticks = np.arange(np.floor(tdec[0]), np.ceil(tdec[-1]) + 1, 2) ax[h].xaxis.set_ticks(data_ticks) - data_ticks = np.arange(ylimits[1]-200,ylimits[0]-ylimits[2],-ylimits[2]) + data_ticks = np.arange( + ylimits[1] - 200, ylimits[0] - ylimits[2], -ylimits[2] + ) ax[h].yaxis.set_ticks(data_ticks[::-1]) - ax[h].set_xticks(np.arange(np.floor(tdec[0]),np.ceil(tdec[-1]),2)) + ax[h].set_xticks(np.arange(np.floor(tdec[0]), np.ceil(tdec[-1]), 2)) # axlim = ax[h].set_xlim([np.floor(tdec[0]), np.ceil(tdec[-1])]) # axlim = ax[h].set_xlim([np.floor(tdec[0]), np.ceil(2.0*tdec[-1])/2.0]) axlim = ax[h].set_xlim(2002, 2021.5) @@ -649,13 +1078,17 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN ax1[h] = ax[h].twinx() # add plot to hidden mm sea level axis ax1[h].plot(tdec, mm_sealevel, visible=False) - ax1[h].set_ylim(-1e18*np.array(ylimits[0:2])/ocean_area) - #ax1[h].yaxis.set_ticks(np.arange(12,-4,-1)) - ax1[h].tick_params(axis='y',colors='black',which='both',direction='in') + ax1[h].set_ylim(-1e18 * np.array(ylimits[0:2]) / ocean_area) + # ax1[h].yaxis.set_ticks(np.arange(12,-4,-1)) + ax1[h].tick_params( + axis='y', colors='black', which='both', direction='in' + ) # formatted ticks on N axis ax1['N'].yaxis.get_major_formatter().set_useOffset(False) - ax1['N'].set_ylabel('Equivalent Sea Level Contribution [mm]',labelpad=10,color='black') + ax1['N'].set_ylabel( + 'Equivalent Sea Level Contribution [mm]', labelpad=10, color='black' + ) for tl in ax1['N'].get_yticklabels(): tl.set_color('black') # hidden ticks @@ -663,7 +1096,7 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN ax1['S'].yaxis.set_ticklabels([]) # add legend - lgd = ax['S'].legend(loc=3,frameon=False) + lgd = ax['S'].legend(loc=3, frameon=False) # lgd = ax['S'].legend(loc=3,frameon=False,handletextpad=-0.2,handlelength=0) # set width, color and style of lines # lgd.get_frame().set_boxstyle('square,pad=0.1') @@ -681,55 +1114,85 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN ax['S'].set_xlabel('Time [Yr]') ax['S'].set_ylabel('Mass [Gt]') # adjust plot to figure dimensions - fig.subplots_adjust(left=0.1, right=0.93, bottom=0.10, top=0.98, wspace=0.06) - figurefile = filepath.joinpath('fig5ab_{0}_{1}.pdf'.format(PROC,DREL)) + fig.subplots_adjust( + left=0.1, right=0.93, bottom=0.10, top=0.98, wspace=0.06 + ) + figurefile = filepath.joinpath('fig5ab_{0}_{1}.pdf'.format(PROC, DREL)) plt.savefig(figurefile, format='pdf') plt.cla() plt.clf() plt.close() # print sea level totals - for i,F in enumerate(FLAG): - args = ('Total',iter_label[i],mm_total[i],np.sqrt(mm_error[i])) - print('{0} {1}: {2:f}+/-{3:f} mm'.format(*args),file=fid) - + for i, F in enumerate(FLAG): + args = ('Total', iter_label[i], mm_total[i], np.sqrt(mm_error[i])) + print('{0} {1}: {2:f}+/-{3:f} mm'.format(*args), file=fid) # flags for creating plots - FLAG = ['HEX','HEX','HEX','HEX'] - SLF = ['','_SLF1','_SLF2','_SLF3'] - plot_colors = ['black','darkorchid','darkorange','mediumseagreen'] - plot_titles = ['No SL Correction','Iteration 1','Iteration 2','Iteration 3'] + FLAG = ['HEX', 'HEX', 'HEX', 'HEX'] + SLF = ['', '_SLF1', '_SLF2', '_SLF3'] + plot_colors = ['black', 'darkorchid', 'darkorange', 'mediumseagreen'] + plot_titles = [ + 'No SL Correction', + 'Iteration 1', + 'Iteration 2', + 'Iteration 3', + ] # create figure axis ax = {} - fig, (ax['S'],ax['N']) = plt.subplots(num=1,ncols=2,sharey=True,figsize=(8,3.5)) + fig, (ax['S'], ax['N']) = plt.subplots( + num=1, ncols=2, sharey=True, figsize=(8, 3.5) + ) # y limits for plot - ylimits = [-90,190,20] - for h,reg in REGION.items(): + ylimits = [-90, 190, 20] + for h, reg in REGION.items(): # leakage fraction for regions leakage_fraction = regional_leakage[reg] # read ocean bottom pressure leakage file - subdir = sd.format('AOD1B',DREL,'',LMAX,OBP_START,OBP_END) - OBP_file = ff.format('ECCO-GAD_OBP_Residuals',reg,'','',ocean_str,LMAX,gw_str,ds_str) - OBP_input = np.loadtxt(mascon_dir.joinpath(subdir,OBP_file))[:nmon,:] + subdir = sd.format('AOD1B', DREL, '', LMAX, OBP_START, OBP_END) + OBP_file = ff.format( + 'ECCO-GAD_OBP_Residuals', + reg, + '', + '', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + OBP_input = np.loadtxt(mascon_dir.joinpath(subdir, OBP_file))[:nmon, :] # read atmospheric pressure leakage file - subdir = sd.format('AOD1B',DREL,'',LMAX,ATM_START,ATM_END) + subdir = sd.format('AOD1B', DREL, '', LMAX, ATM_START, ATM_END) # ATM_file = ff.format('ATM-GAA_Residuals',reg,'_3D','',ocean_str,LMAX,gw_str) # ATM_file = ff.format('ATM_Differences',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) - ATM_file = ff.format('ATM_Differences',reg,'','',ocean_str,LMAX,gw_str,ds_str) - ATM_input = np.loadtxt(mascon_dir.joinpath(subdir,ATM_file))[:nmon,:] + ATM_file = ff.format( + 'ATM_Differences', reg, '', '', ocean_str, LMAX, gw_str, ds_str + ) + ATM_input = np.loadtxt(mascon_dir.joinpath(subdir, ATM_file))[:nmon, :] # read GLDAS terrestrial water RMS file - subdir = sd.format('GLDAS','TWC_V2.1_RMS','',LMAX,GLDAS_START,GLDAS_END) - TWC_file = ff.format('GLDAS_TWC_RMS',reg,'','RAD1.5_','',LMAX,gw_str,ds_str) - TWC_input = np.loadtxt(base_dir.joinpath('GLDAS',subdir,TWC_file))[:nmon,:] - isvalid, = np.nonzero(np.isfinite(TWC_input[:,2]) & - (TWC_input[:,0] >= START_MON) & (TWC_input[:,0] <= END_MON)) - TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid,2]**2)/len(isvalid)) + subdir = sd.format( + 'GLDAS', 'TWC_V2.1_RMS', '', LMAX, GLDAS_START, GLDAS_END + ) + TWC_file = ff.format( + 'GLDAS_TWC_RMS', reg, '', 'RAD1.5_', '', LMAX, gw_str, ds_str + ) + TWC_input = np.loadtxt(base_dir.joinpath('GLDAS', subdir, TWC_file))[ + :nmon, : + ] + (isvalid,) = np.nonzero( + np.isfinite(TWC_input[:, 2]) + & (TWC_input[:, 0] >= START_MON) + & (TWC_input[:, 0] <= END_MON) + ) + TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid, 2] ** 2) / len(isvalid)) # input estimated SLF monte carlo variance file and calculate RMS - subdir = sd.format(PROC,DREL,'_MC',LMAX,START_MON,END_MON) - SLF_file = ff.format('MC',reg,'','RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - SLF_input = np.loadtxt(mascon_dir.joinpath(subdir,SLF_file)) - SLF_RMS = np.sqrt(np.sum(SLF_input[:,1]**2)/len(SLF_input)) + subdir = sd.format(PROC, DREL, '_MC', LMAX, START_MON, END_MON) + SLF_file = ff.format( + 'MC', reg, '', 'RAD1.5_', ocean_str, LMAX, gw_str, ds_str + ) + SLF_input = np.loadtxt(mascon_dir.joinpath(subdir, SLF_file)) + SLF_RMS = np.sqrt(np.sum(SLF_input[:, 1] ** 2) / len(SLF_input)) # calculate mean of RMS over multiple reanalyses OBP_RMS = 0.0 ATM_RMS = 0.0 @@ -737,16 +1200,16 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN # ivalid, = np.nonzero(np.isfinite(OBP_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(OBP_input[:,j+3])) # OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(OBP_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(OBP_input[:,2])) - OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(OBP_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(OBP_input[:, 2])) + OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid, 2] ** 2) / valid_count) # for j in range(4): # ivalid, = np.nonzero(np.isfinite(ATM_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(ATM_input[:,j+3])) # ATM_RMS += np.sqrt(np.sum(OBP_input[ATM_input,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(ATM_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(ATM_input[:,2])) - ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(ATM_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(ATM_input[:, 2])) + ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid, 2] ** 2) / valid_count) # # divide by the number of reanalyses # OBP_RMS /= 2.0 # ATM_RMS /= 4.0 @@ -755,41 +1218,55 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN # number of rheologies to iterate nRheology = len(gia_files[g]) # iterate through solutions - mon = np.zeros((nmon),dtype=np.int64) - tdec = np.zeros((nmon),dtype=np.float64) - mass = np.zeros((nmon,nRheology),dtype=np.float64) - mass_m1 = np.zeros((nmon),dtype=np.float64) - satellite_error = np.zeros((nmon),dtype=np.float64) - for i,F in enumerate(FLAG): + mon = np.zeros((nmon), dtype=np.int64) + tdec = np.zeros((nmon), dtype=np.float64) + mass = np.zeros((nmon, nRheology), dtype=np.float64) + mass_m1 = np.zeros((nmon), dtype=np.float64) + satellite_error = np.zeros((nmon), dtype=np.float64) + for i, F in enumerate(FLAG): # subdirectory - subdir = sd.format(PROC,DREL,SLF[i],LMAX,START_MON,END_MON) + subdir = sd.format(PROC, DREL, SLF[i], LMAX, START_MON, END_MON) # read each GIA model - for k,GIA_FILE in enumerate(gia_files[g]): - gia_Ylms = gravtk.read_GIA_model(base_dir.joinpath(*GIA_FILE), GIA=g) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_Ylms = gravtk.read_GIA_model( + base_dir.joinpath(*GIA_FILE), GIA=g + ) gia_str = gia_Ylms['title'] - input_file = ff.format(gia_str,reg,'',atm_str,ocean_str,LMAX,gw_str,ds_str) - dinput = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) - mon[:] = dinput[:nmon,0] - tdec[:] = dinput[:nmon,1] - mass[:,k] = dinput[:nmon,2] - satellite_error[:] += dinput[:nmon,3]**2 + input_file = ff.format( + gia_str, reg, '', atm_str, ocean_str, LMAX, gw_str, ds_str + ) + dinput = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) + mon[:] = dinput[:nmon, 0] + tdec[:] = dinput[:nmon, 1] + mass[:, k] = dinput[:nmon, 2] + satellite_error[:] += dinput[:nmon, 3] ** 2 # calculate mean GIA-corrected mass change (for all Earth rheologies) gia_corrected_mean = np.mean(mass, axis=1) # GRACE satellite error component, ocean leakage (ECCO-GAD), # atmosphere leakage (ATM-GAA) and GLDAS TWC - grace_error = np.sqrt(np.sum(satellite_error/nRheology + SLF_RMS**2 + - OBP_RMS**2 + ATM_RMS**2 + TWC_RMS**2)/nmon) - if (i > 0): + grace_error = np.sqrt( + np.sum( + satellite_error / nRheology + + SLF_RMS**2 + + OBP_RMS**2 + + ATM_RMS**2 + + TWC_RMS**2 + ) + / nmon + ) + if i > 0: # add to plot with colors and label - plot_label = u'{0} \u2013 {1}'.format(plot_titles[i],plot_titles[i-1]) + plot_label = '{0} \u2013 {1}'.format( + plot_titles[i], plot_titles[i - 1] + ) residual = gia_corrected_mean - gia_corrected_mean[0] - mass_m1 # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - rnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): + tnan = np.full_like(month, np.nan, dtype=np.float64) + rnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): valid = np.count_nonzero(mon == m) if valid: - mm, = np.nonzero(mon == m) + (mm,) = np.nonzero(mon == m) tnan[d] = tdec[mm] rnan[d] = residual[mm] # plot all dates @@ -799,28 +1276,34 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN error_m1 = np.copy(grace_error) # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9, - day=3,hour=12,minute=12) - ax[h].axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax[h].axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(mon == 186) - kk, = np.flatnonzero(mon == 198) + (jj,) = np.flatnonzero(mon == 186) + (kk,) = np.flatnonzero(mon == 198) # ax[h].axvline(tdec[jj],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) # ax[h].axvline(tdec[kk],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) - vs = ax[h].axvspan(tdec[jj],tdec[kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) + vs = ax[h].axvspan( + tdec[jj], tdec[kk], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (6, 3) # add horizontal line at 0 - ax[h].axhline(0.0, color='black', ls='dashed', dashes=(11,5), lw=0.5) + ax[h].axhline(0.0, color='black', ls='dashed', dashes=(11, 5), lw=0.5) # add labels - textprops = dict(size=14,weight='bold') - at = matplotlib.offsetbox.AnchoredText(fig_text[h], - prop=textprops, pad=0, frameon=False, loc=2) + textprops = dict(size=14, weight='bold') + at = matplotlib.offsetbox.AnchoredText( + fig_text[h], prop=textprops, pad=0, frameon=False, loc=2 + ) ax[h].add_artist(at) - data_ticks = np.arange(np.floor(tdec[0]),np.ceil(tdec[-1])+1,2) + data_ticks = np.arange(np.floor(tdec[0]), np.ceil(tdec[-1]) + 1, 2) ax[h].xaxis.set_ticks(data_ticks) - data_ticks = np.arange(ylimits[1]-10,ylimits[0]-ylimits[2],-ylimits[2]) + data_ticks = np.arange( + ylimits[1] - 10, ylimits[0] - ylimits[2], -ylimits[2] + ) ax[h].yaxis.set_ticks(data_ticks[::-1]) - ax[h].set_xticks(np.arange(np.floor(tdec[0]),np.ceil(tdec[-1]),2)) + ax[h].set_xticks(np.arange(np.floor(tdec[0]), np.ceil(tdec[-1]), 2)) # axlim = ax[h].set_xlim([np.floor(tdec[0]), np.ceil(tdec[-1])]) # axlim = ax[h].set_xlim([np.floor(tdec[0]), np.ceil(2.0*tdec[-1])/2.0]) axlim = ax[h].set_xlim(2002, 2021.5) @@ -831,7 +1314,7 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN ax[h].xaxis.get_major_formatter().set_useOffset(False) # add legend - lgd = ax['S'].legend(loc=3,frameon=False) + lgd = ax['S'].legend(loc=3, frameon=False) # lgd = ax['S'].legend(loc=3,frameon=False,handletextpad=-0.2,handlelength=0) # set width, color and style of lines # lgd.get_frame().set_boxstyle('square,pad=0.1') @@ -849,8 +1332,10 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN ax['S'].set_xlabel('Time [Yr]') ax['S'].set_ylabel('Mass Difference [Gt]') # adjust plot to figure dimensions - fig.subplots_adjust(left=0.08,right=0.98,bottom=0.11,top=0.98,wspace=0.06) - figurefile = pathlib.Path('fig6ab_{0}_{1}.pdf'.format(PROC,DREL)) + fig.subplots_adjust( + left=0.08, right=0.98, bottom=0.11, top=0.98, wspace=0.06 + ) + figurefile = pathlib.Path('fig6ab_{0}_{1}.pdf'.format(PROC, DREL)) plt.savefig(figurefile, format='pdf') plt.cla() plt.clf() @@ -859,11 +1344,26 @@ def plot_mascon_SLF_iterations(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SCEN # close the output table fid.close() -# PURPOSE: run a monte carlo type error analysis with each error component -def monte_carlo_regress(t_in, d_in, satellite_error, gia_error, leakage_model, - ocean_error, atm_error, twc_error, RUNS=10000, ORDER=0, CYCLES=[], - TERMS=[], STDEV=0, CONF=0, RMS=True, RELATIVE=Ellipsis): +# PURPOSE: run a monte carlo type error analysis with each error component +def monte_carlo_regress( + t_in, + d_in, + satellite_error, + gia_error, + leakage_model, + ocean_error, + atm_error, + twc_error, + RUNS=10000, + ORDER=0, + CYCLES=[], + TERMS=[], + STDEV=0, + CONF=0, + RMS=True, + RELATIVE=Ellipsis, +): # remove singleton dimensions t_in = np.squeeze(t_in) d_in = np.squeeze(d_in) @@ -874,19 +1374,19 @@ def monte_carlo_regress(t_in, d_in, satellite_error, gia_error, leakage_model, t_rel = t_in[RELATIVE].mean() elif isinstance(RELATIVE, (float, int, np.float64, np.int_)): t_rel = np.copy(RELATIVE) - elif (RELATIVE == Ellipsis): + elif RELATIVE == Ellipsis: t_rel = t_in[RELATIVE].mean() # create design matrix based on polynomial order and harmonics # with any additional fit terms DMAT = [] # add polynomial orders (0=constant, 1=linear, 2=quadratic) - for o in range(ORDER+1): - DMAT.append((t_in-t_rel)**o) + for o in range(ORDER + 1): + DMAT.append((t_in - t_rel) ** o) # add cyclical terms (0.5=semi-annual, 1=annual) for c in CYCLES: - DMAT.append(np.sin(2.0*np.pi*t_in/np.float64(c))) - DMAT.append(np.cos(2.0*np.pi*t_in/np.float64(c))) + DMAT.append(np.sin(2.0 * np.pi * t_in / np.float64(c))) + DMAT.append(np.cos(2.0 * np.pi * t_in / np.float64(c))) # add additional terms to the design matrix for t in TERMS: DMAT.append(t) @@ -894,10 +1394,10 @@ def monte_carlo_regress(t_in, d_in, satellite_error, gia_error, leakage_model, DMAT = np.transpose(DMAT) # output beta_err range (standard deviation or confidence interval) - if (STDEV != 0): + if STDEV != 0: # Setting output error alpha with standard deviation - alpha = 1.0 - scipy.special.erf(STDEV/np.sqrt(2.0)) - elif (CONF != 0): + alpha = 1.0 - scipy.special.erf(STDEV / np.sqrt(2.0)) + elif CONF != 0: # Setting output error alpha with confidence interval alpha = 1.0 - CONF else: @@ -910,21 +1410,21 @@ def monte_carlo_regress(t_in, d_in, satellite_error, gia_error, leakage_model, nu = nmax - n_terms # Student T-Distribution with D.O.F. nu # t.ppf parallels tinv in matlab - tstar = scipy.stats.t.ppf(1.0-(alpha/2.0),nu) + tstar = scipy.stats.t.ppf(1.0 - (alpha / 2.0), nu) # initiating output variables from MC run - beta_mat = np.zeros((RUNS,n_terms))# regressed variable - beta_hat = np.zeros((n_terms))# regressed variable with max probability - beta_conf = np.zeros((n_terms,2))# confidence interval - beta_pdf = np.zeros((RUNS,n_terms))# regressed variable - st_err = np.zeros((RUNS,n_terms))# standard error - beta_err = np.zeros((RUNS,n_terms))# error to specified std or confidence + beta_mat = np.zeros((RUNS, n_terms)) # regressed variable + beta_hat = np.zeros((n_terms)) # regressed variable with max probability + beta_conf = np.zeros((n_terms, 2)) # confidence interval + beta_pdf = np.zeros((RUNS, n_terms)) # regressed variable + st_err = np.zeros((RUNS, n_terms)) # standard error + beta_err = np.zeros((RUNS, n_terms)) # error to specified std or confidence # Calculating Least-Squares Coefficients # Least-Squares fitting # Covariance Matrix # Multiplying the design matrix by itself - Hinv = np.linalg.inv(np.dot(np.transpose(DMAT),DMAT)) + Hinv = np.linalg.inv(np.dot(np.transpose(DMAT), DMAT)) # Taking the diagonal components of the cov matrix hdiag = np.diag(Hinv) @@ -937,30 +1437,43 @@ def monte_carlo_regress(t_in, d_in, satellite_error, gia_error, leakage_model, # the random variable will make error between +/- the data error # assuming data error is uniformly distributed as a worst case # error is between +/- data error - gia_error_rand = (1.0 - 2.0*random_value[0])*gia_error - leakage_error_rand = (1.0 - 2.0*random_value[1])*leakage_model - satellite_error_rand = (1.0 - 2.0*np.random.rand(nmax))*satellite_error - ocean_error_rand = (1.0 - 2.0*np.random.rand(nmax))*ocean_error - atmosphere_error_rand = (1.0 - 2.0*np.random.rand(nmax))*atm_error - total_water_error_rand = (1.0 - 2.0*np.random.rand(nmax))*twc_error + gia_error_rand = (1.0 - 2.0 * random_value[0]) * gia_error + leakage_error_rand = (1.0 - 2.0 * random_value[1]) * leakage_model + satellite_error_rand = ( + 1.0 - 2.0 * np.random.rand(nmax) + ) * satellite_error + ocean_error_rand = (1.0 - 2.0 * np.random.rand(nmax)) * ocean_error + atmosphere_error_rand = (1.0 - 2.0 * np.random.rand(nmax)) * atm_error + total_water_error_rand = (1.0 - 2.0 * np.random.rand(nmax)) * twc_error # adding random assessments of each measurement error to data points - data_simul[:] = d_in + gia_error_rand + leakage_error_rand + \ - satellite_error_rand + ocean_error_rand + atmosphere_error_rand + \ - total_water_error_rand + data_simul[:] = ( + d_in + + gia_error_rand + + leakage_error_rand + + satellite_error_rand + + ocean_error_rand + + atmosphere_error_rand + + total_water_error_rand + ) # Standard Least-Squares fitting (the [0] denotes coefficients output) - beta_mat[i,:] = np.linalg.lstsq(DMAT,data_simul,rcond=-1)[0] + beta_mat[i, :] = np.linalg.lstsq(DMAT, data_simul, rcond=-1)[0] # MSE = (1/nu)*sum((Y-X*B)**2) # Mean square error (real data - model)^2/DOF - MSE = np.dot(np.transpose(data_simul - np.dot(DMAT,beta_mat[i,:])), - (data_simul - np.dot(DMAT,beta_mat[i,:])))/nu + MSE = ( + np.dot( + np.transpose(data_simul - np.dot(DMAT, beta_mat[i, :])), + (data_simul - np.dot(DMAT, beta_mat[i, :])), + ) + / nu + ) # Standard Error (1 STD) - st_err[i,:] = np.sqrt(MSE*hdiag) + st_err[i, :] = np.sqrt(MSE * hdiag) # beta_err is the error for each coefficient # beta_err = t(nu,1-alpha/2)*standard error - beta_err[i,:] = tstar*st_err[i,:] + beta_err[i, :] = tstar * st_err[i, :] # Calculating the mean of the regressed terms # the mean trend shouldn't be much different than the standard regression @@ -970,48 +1483,65 @@ def monte_carlo_regress(t_in, d_in, satellite_error, gia_error, leakage_model, # Will add to the mean of the errors from the regressions beta_var = np.zeros((n_terms)) for n in range(n_terms): - beta_var[n] = np.dot(np.transpose(beta_mat[:,n] - beta_mean[n]), \ - beta_mat[:,n] - beta_mean[n])/RUNS + beta_var[n] = ( + np.dot( + np.transpose(beta_mat[:, n] - beta_mean[n]), + beta_mat[:, n] - beta_mean[n], + ) + / RUNS + ) # probability distribution of the regression - p_beta = np.exp(-0.5*(beta_mat[:,n]-beta_mean[n])**2)/np.sqrt(2.0*np.pi) + p_beta = np.exp(-0.5 * (beta_mat[:, n] - beta_mean[n]) ** 2) / np.sqrt( + 2.0 * np.pi + ) # normalize the pdf of beta - beta_pdf[:,n] = p_beta/np.sum(p_beta) + beta_pdf[:, n] = p_beta / np.sum(p_beta) # beta_hat is the beta with highest probability # calculated by finding the max of the probability distribution - indices = np.argmax(beta_pdf[:,n]) - beta_hat[n] = beta_mat[:,n][indices] + indices = np.argmax(beta_pdf[:, n]) + beta_hat[n] = beta_mat[:, n][indices] # sorting the probabilities in descending order - im = np.argsort(beta_pdf[:,n])[::-1] + im = np.argsort(beta_pdf[:, n])[::-1] # taking the cumulative sum of the sorted probabilities # will find the error (to specified confidence) - cum_beta_pdf = np.cumsum(beta_pdf[im,n]) + cum_beta_pdf = np.cumsum(beta_pdf[im, n]) # minimum beta - beta_min = np.interp(1.0-alpha,cum_beta_pdf,beta_mat[im,n]) + beta_min = np.interp(1.0 - alpha, cum_beta_pdf, beta_mat[im, n]) # confidence interval # beta_hat - beta_min = error - beta_conf[n,0] = beta_hat[n]-np.abs(beta_min-beta_hat[n]) - beta_conf[n,1] = beta_hat[n]+np.abs(beta_min-beta_hat[n]) + beta_conf[n, 0] = beta_hat[n] - np.abs(beta_min - beta_hat[n]) + beta_conf[n, 1] = beta_hat[n] + np.abs(beta_min - beta_hat[n]) # Propagating RMS errors (RMS is default, worst case can be specified) # Also will output the variance of the regressed coefficient (as beta_var) # Student T-Distribution with D.O.F. (RUNS-1): removing 1 for mean # t.ppf parallels tinv in matlab - tstar_mean = scipy.stats.t.ppf(1.0-(alpha/2.0),RUNS-1.0) + tstar_mean = scipy.stats.t.ppf(1.0 - (alpha / 2.0), RUNS - 1.0) if RMS: # mean_std is the standard error - mean_std = np.sqrt(np.sum(st_err**2.0,axis=0)/RUNS + beta_var) + mean_std = np.sqrt(np.sum(st_err**2.0, axis=0) / RUNS + beta_var) # mean_err = t(nu,1-alpha/2)*standard error - mean_err=np.sqrt(np.sum(beta_err**2.0,axis=0)/RUNS+tstar_mean*beta_var) + mean_err = np.sqrt( + np.sum(beta_err**2.0, axis=0) / RUNS + tstar_mean * beta_var + ) else: # mean_std is the standard error - mean_std = np.mean(st_err,axis=0) + np.sqrt(beta_var) + mean_std = np.mean(st_err, axis=0) + np.sqrt(beta_var) # error at specified standard deviation or confidence interval # mean_err = t(nu,1-alpha/2)*standard error - mean_err = np.mean(beta_err,axis=0) + tstar_mean*np.sqrt(beta_var) + mean_err = np.mean(beta_err, axis=0) + tstar_mean * np.sqrt(beta_var) - return {'matrix':beta_mat, 'beta':beta_mean, 'error':mean_err, \ - 'std_err':mean_std, 'variance':beta_var, 'cov_mat':Hinv, \ - 'pdf':beta_pdf, 'hat':beta_hat, 'confidence':beta_conf} + return { + 'matrix': beta_mat, + 'beta': beta_mean, + 'error': mean_err, + 'std_err': mean_std, + 'variance': beta_var, + 'cov_mat': Hinv, + 'pdf': beta_pdf, + 'hat': beta_hat, + 'confidence': beta_conf, + } # PURPOSE: create argument parser @@ -1019,46 +1549,108 @@ def arguments(): parser = argparse.ArgumentParser() # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, default=None, - choices=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + default=None, + choices=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series') - parser.add_argument('--end','-E', - type=int, default=230, - help='Ending GRACE/GRACE-FO month for time series') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167,172, - 177,178,182,200,201] - parser.add_argument('--missing','-M', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months in time series') - parser.add_argument('--scenarios','-s', - default=False, action='store_true', - help='Run with all sea level scenarios') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=230, + help='Ending GRACE/GRACE-FO month for time series', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-M', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months in time series', + ) + parser.add_argument( + '--scenarios', + '-s', + default=False, + action='store_true', + help='Run with all sea level scenarios', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run program for parameters - plot_mascon_SLF_iterations(args.directory,args.center,args.release, - args.start,args.end,args.missing,SCENARIOS=args.scenarios) + plot_mascon_SLF_iterations( + args.directory, + args.center, + args.release, + args.start, + args.end, + args.missing, + SCENARIOS=args.scenarios, + ) + # run main program if __name__ == '__main__': diff --git a/scripts/plot_mascon_SLF_timeseries.py b/scripts/plot_mascon_SLF_timeseries.py index 3d0883cf..ee921a61 100644 --- a/scripts/plot_mascon_SLF_timeseries.py +++ b/scripts/plot_mascon_SLF_timeseries.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_mascon_SLF_timeseries.py Written by Tyler Sutterley (05/2023) @@ -32,6 +32,7 @@ Updated 01/2021: updated for new open-source processing scheme Written 11/2019 """ + from __future__ import print_function import inspect @@ -45,85 +46,133 @@ try: import matplotlib import matplotlib.pyplot as plt + matplotlib.rcParams['mathtext.default'] = 'regular' matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] import matplotlib.offsetbox except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import scipy.stats import scipy.special except ModuleNotFoundError: - warnings.warn("scipy not available", ImportWarning) + warnings.warn('scipy not available', ImportWarning) # current file path for the child programs filename = inspect.getframeinfo(inspect.currentframe()).filename filepath = pathlib.Path(filename).absolute().parent + # PURPOSE: plot mascon time series from Velicogna et al. (2014) -def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB=False): +def plot_mascon_SLF_timeseries( + base_dir, PROC, DREL, START_MON, END_MON, MISSING, SMB=False +): # directory setup - mascon_dir = base_dir.joinpath('GRACE','mascons') + mascon_dir = base_dir.joinpath('GRACE', 'mascons') # GIA parameters GIA = {} gia_files = {} # Simpson (2009), Peltier (2015,2018), A (ICE6G), Caron (2018) - GIA['N'] = ['SM09','ICE6G','ICE6G-D','AW13-ICE6G','Caron'] + GIA['N'] = ['SM09', 'ICE6G', 'ICE6G-D', 'AW13-ICE6G', 'Caron'] # Ivins (2013), Whitehouse (2012), Peltier (2015,2018), A (ICE6G/IJ05), Caron (2018) - GIA['S'] = ['IJ05-R2','W12a','ICE6G','ICE6G-D','AW13-ICE6G','ascii','Caron'] + GIA['S'] = [ + 'IJ05-R2', + 'W12a', + 'ICE6G', + 'ICE6G-D', + 'AW13-ICE6G', + 'ascii', + 'Caron', + ] # GIA files for each modeling group gia_files['AW13-ICE6G'] = [] - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_.1_10.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_1._10.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_.1_1.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_1._1.']) - gia_files['AW13-ICE6G'].append(['GIA','AW13','ICE6G','stokes.ice6g_GA.txt']) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_.1_10.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_1._10.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_.1_1.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_1._1.'] + ) + gia_files['AW13-ICE6G'].append( + ['GIA', 'AW13', 'ICE6G', 'stokes.ice6g_GA.txt'] + ) gia_files['ascii'] = [] - gia_files['ascii'].append(['GIA','AW13','IJ05-R2','IJ05_R2_115_.2_1.5_ICE6G.txt']) - gia_files['ascii'].append(['GIA','AW13','IJ05-R2','IJ05_R2_65_.2_1.5_ICE6G.txt']) + gia_files['ascii'].append( + ['GIA', 'AW13', 'IJ05-R2', 'IJ05_R2_115_.2_1.5_ICE6G.txt'] + ) + gia_files['ascii'].append( + ['GIA', 'AW13', 'IJ05-R2', 'IJ05_R2_65_.2_1.5_ICE6G.txt'] + ) gia_files['Caron'] = [] - gia_files['Caron'].append(['GIA','Caron','expStokes_GIA.txt']) + gia_files['Caron'].append(['GIA', 'Caron', 'expStokes_GIA.txt']) gia_files['ICE6G-D'] = [] - gia_files['ICE6G-D'].append(['GIA','ICE6G','VersionD','Stokes_trend_High_Res.txt']) - gia_files['ICE6G-D'].append(['GIA','ICE6G','VersionD','Stokes_trend_VM5a_O512.txt']) + gia_files['ICE6G-D'].append( + ['GIA', 'ICE6G', 'VersionD', 'Stokes_trend_High_Res.txt'] + ) + gia_files['ICE6G-D'].append( + ['GIA', 'ICE6G', 'VersionD', 'Stokes_trend_VM5a_O512.txt'] + ) gia_files['ICE6G'] = [] - gia_files['ICE6G'].append(['GIA','ICE6G','VM5','Stokes_G_Rot_60_I6_A_VM5a']) - gia_files['ICE6G'].append(['GIA','ICE6G','VM5','Stokes_G_Rot_60_I6_A_VM5b']) + gia_files['ICE6G'].append( + ['GIA', 'ICE6G', 'VM5', 'Stokes_G_Rot_60_I6_A_VM5a'] + ) + gia_files['ICE6G'].append( + ['GIA', 'ICE6G', 'VM5', 'Stokes_G_Rot_60_I6_A_VM5b'] + ) gia_files['IJ05-R2'] = [] - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_1.5_L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_2._L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_3.2_L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_115_.2_4._L120']) - gia_files['IJ05-R2'].append(['GIA','IJ05-R2','Stokes.R2_65_.2_1.5_L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_1.5_L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_2._L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_3.2_L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_115_.2_4._L120']) + gia_files['IJ05-R2'].append(['GIA', 'IJ05-R2', 'Stokes.R2_65_.2_1.5_L120']) gia_files['SM09'] = [] - gia_files['SM09'].append(['GIA','SM09','grate_120p11.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_120p51.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_120p53.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_120p81.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p32.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p510.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p55.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p58.clm']) - gia_files['SM09'].append(['GIA','SM09','grate_96p85.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p11.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p51.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p53.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_120p81.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p32.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p510.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p55.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p58.clm']) + gia_files['SM09'].append(['GIA', 'SM09', 'grate_96p85.clm']) gia_files['W12a'] = [] - gia_files['W12a'].append(['GIA','W12a','grate_B.clm']) - gia_files['W12a'].append(['GIA','W12a','grate_L.clm']) - gia_files['W12a'].append(['GIA','W12a','grate_U.clm']) + gia_files['W12a'].append(['GIA', 'W12a', 'grate_B.clm']) + gia_files['W12a'].append(['GIA', 'W12a', 'grate_L.clm']) + gia_files['W12a'].append(['GIA', 'W12a', 'grate_U.clm']) # Setting output error alpha with confidence interval CONF = 0.95 alpha = 1.0 - CONF # GRACE data release and months - GAP = [187,188,189,190,191,192,193,194,195,196,197,] - months = sorted(set(np.arange(START_MON,END_MON+1))-set(MISSING)-set(GAP)) + GAP = [ + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + ] + months = sorted( + set(np.arange(START_MON, END_MON + 1)) - set(MISSING) - set(GAP) + ) nmon = len(months) # start and end GRACE months for correction data - OBP_START,OBP_END = (4,254) - ATM_START,ATM_END = (4,251) - GLDAS_START,GLDAS_END = (4,254) + OBP_START, OBP_END = (4, 254) + ATM_START, ATM_END = (4, 251) + GLDAS_START, GLDAS_END = (4, 254) # atmospheric ECMWF "jump" flag and ocean redistribution flag atm_str = 'wATM_' if (DREL == 'RL05') else '' @@ -136,11 +185,17 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= # destripe string ds_str = '' # create a set of months - month = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + month = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) # version flags - VERSION = ['v0',''] - plot_title = ['Version 0','Version 1'] - plot_colors = ['darkorchid','mediumseagreen','darkorange','red','dodgerblue'] + VERSION = ['v0', ''] + plot_title = ['Version 0', 'Version 1'] + plot_colors = [ + 'darkorchid', + 'mediumseagreen', + 'darkorange', + 'red', + 'dodgerblue', + ] # subdirectory and input file formats sd = 'HEX_{0}_{1}{2}_SPH_CAP_MSCNS{3}_L{4:d}_{5:03d}-{6:03d}' @@ -149,40 +204,86 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= # create figure axis for Greenland plots ax1 = {} ax2 = {} - fig, ((ax1['GIS'], ax1['NN'], ax1['NE']), (ax1['NW'], ax1['SW'], ax1['SE'])) = \ - plt.subplots(num=2, nrows=2, ncols=3, sharex=False, sharey=False, figsize=(12, 7.5)) - ylimits = {'NW':[-1200,1100,200],'NN':[-450,450,100],'NE':[-180,320,40],\ - 'SW':[-650,550,100],'SE':[-900,1200,200],'GIS':[-2800,3000,400]} - fig_text = {'GIS':'a)','NN':'b)','NE':'c)','NW':'d)','SW':'e)','SE':'f)'} - reg_text = {'GIS':'GIS','NN':'N','NE':'NE','NW':'NW','SW':'SW','SE':'SE'} + ( + fig, + ((ax1['GIS'], ax1['NN'], ax1['NE']), (ax1['NW'], ax1['SW'], ax1['SE'])), + ) = plt.subplots( + num=2, nrows=2, ncols=3, sharex=False, sharey=False, figsize=(12, 7.5) + ) + ylimits = { + 'NW': [-1200, 1100, 200], + 'NN': [-450, 450, 100], + 'NE': [-180, 320, 40], + 'SW': [-650, 550, 100], + 'SE': [-900, 1200, 200], + 'GIS': [-2800, 3000, 400], + } + fig_text = { + 'GIS': 'a)', + 'NN': 'b)', + 'NE': 'c)', + 'NW': 'd)', + 'SW': 'e)', + 'SE': 'f)', + } + reg_text = { + 'GIS': 'GIS', + 'NN': 'N', + 'NE': 'NE', + 'NW': 'NW', + 'SW': 'SW', + 'SE': 'SE', + } h = 'N' SLF = '_SLF3' - RACMO_START,RACMO_END = (4,239) - GEMB_START,GEMB_END = (4,251) - ypad = dict(GIS=4,NN=4,NE=4,NW=4,SW=4,SE=4) - for reg,ax in ax1.items(): + RACMO_START, RACMO_END = (4, 239) + GEMB_START, GEMB_END = (4, 251) + ypad = dict(GIS=4, NN=4, NE=4, NW=4, SW=4, SE=4) + for reg, ax in ax1.items(): # read ocean bottom pressure leakage file - subdir = sd.format('AOD1B',DREL,'','',LMAX,OBP_START,OBP_END) - OBP_file = ff.format('ECCO-GAD_OBP_Residuals',reg,'','',ocean_str,LMAX,gw_str,ds_str) - OBP_input = np.loadtxt(mascon_dir.joinpath(subdir,OBP_file))[:nmon,:] + subdir = sd.format('AOD1B', DREL, '', '', LMAX, OBP_START, OBP_END) + OBP_file = ff.format( + 'ECCO-GAD_OBP_Residuals', + reg, + '', + '', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + OBP_input = np.loadtxt(mascon_dir.joinpath(subdir, OBP_file))[:nmon, :] # read atmospheric pressure leakage file - subdir = sd.format('AOD1B',DREL,'','',LMAX,ATM_START,ATM_END) + subdir = sd.format('AOD1B', DREL, '', '', LMAX, ATM_START, ATM_END) # ATM_file = ff.format('ATM-GAA_Residuals',reg,'_3D','',ocean_str,LMAX,gw_str) # ATM_file = ff.format('ATM_Differences',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) - ATM_file = ff.format('ATM_Differences',reg,'','',ocean_str,LMAX,gw_str,ds_str) - ATM_input = np.loadtxt(mascon_dir.joinpath(subdir,ATM_file))[:nmon,:] + ATM_file = ff.format( + 'ATM_Differences', reg, '', '', ocean_str, LMAX, gw_str, ds_str + ) + ATM_input = np.loadtxt(mascon_dir.joinpath(subdir, ATM_file))[:nmon, :] # read GLDAS terrestrial water RMS file - subdir = sd.format('GLDAS','TWC_V2.1_RMS','','',LMAX,GLDAS_START,GLDAS_END) - TWC_file = ff.format('GLDAS_TWC_RMS',reg,'','RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - TWC_input = np.loadtxt(base_dir.joinpath('GLDAS',subdir,TWC_file))[:nmon,:] - isvalid, = np.nonzero(np.isfinite(TWC_input[:,2]) & - (TWC_input[:,0] >= START_MON) & (TWC_input[:,0] <= END_MON)) - TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid,2]**2)/len(isvalid)) + subdir = sd.format( + 'GLDAS', 'TWC_V2.1_RMS', '', '', LMAX, GLDAS_START, GLDAS_END + ) + TWC_file = ff.format( + 'GLDAS_TWC_RMS', reg, '', 'RAD1.5_', ocean_str, LMAX, gw_str, ds_str + ) + TWC_input = np.loadtxt(base_dir.joinpath('GLDAS', subdir, TWC_file))[ + :nmon, : + ] + (isvalid,) = np.nonzero( + np.isfinite(TWC_input[:, 2]) + & (TWC_input[:, 0] >= START_MON) + & (TWC_input[:, 0] <= END_MON) + ) + TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid, 2] ** 2) / len(isvalid)) # input estimated SLF monte carlo variance file and calculate RMS - subdir = sd.format(PROC,DREL,'','_MC',LMAX,START_MON,END_MON) - SLF_file = ff.format('MC',reg,'','RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - SLF_input = np.loadtxt(mascon_dir.joinpath(subdir,SLF_file)) - SLF_RMS = np.sqrt(np.sum(SLF_input[:,1]**2)/len(SLF_input)) + subdir = sd.format(PROC, DREL, '', '_MC', LMAX, START_MON, END_MON) + SLF_file = ff.format( + 'MC', reg, '', 'RAD1.5_', ocean_str, LMAX, gw_str, ds_str + ) + SLF_input = np.loadtxt(mascon_dir.joinpath(subdir, SLF_file)) + SLF_RMS = np.sqrt(np.sum(SLF_input[:, 1] ** 2) / len(SLF_input)) # calculate mean of RMS over multiple reanalyses OBP_RMS = 0.0 ATM_RMS = 0.0 @@ -190,16 +291,16 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= # ivalid, = np.nonzero(np.isfinite(OBP_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(OBP_input[:,j+3])) # OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(OBP_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(OBP_input[:,2])) - OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(OBP_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(OBP_input[:, 2])) + OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid, 2] ** 2) / valid_count) # for j in range(4): # ivalid, = np.nonzero(np.isfinite(ATM_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(ATM_input[:,j+3])) # ATM_RMS += np.sqrt(np.sum(OBP_input[ATM_input,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(ATM_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(ATM_input[:,2])) - ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(ATM_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(ATM_input[:, 2])) + ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid, 2] ** 2) / valid_count) # # divide by the number of reanalyses # OBP_RMS /= 2.0 # ATM_RMS /= 4.0 @@ -208,130 +309,237 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= # number of rheologies to iterate nRheology = len(gia_files[g]) # iterate through solutions - mon = np.zeros((nmon),dtype=np.int64) - tdec = np.zeros((nmon),dtype=np.float64) - mass = np.zeros((nmon,nRheology),dtype=np.float64) - satellite_error = np.zeros((nmon),dtype=np.float64) - for i,F in enumerate(VERSION): + mon = np.zeros((nmon), dtype=np.int64) + tdec = np.zeros((nmon), dtype=np.float64) + mass = np.zeros((nmon, nRheology), dtype=np.float64) + satellite_error = np.zeros((nmon), dtype=np.float64) + for i, F in enumerate(VERSION): # subdirectory - subdir = sd.format(PROC,DREL,F,SLF,LMAX,START_MON,END_MON) + subdir = sd.format(PROC, DREL, F, SLF, LMAX, START_MON, END_MON) # read each GIA model - for k,GIA_FILE in enumerate(gia_files[g]): - gia_Ylms = gravtk.read_GIA_model(base_dir.joinpath(*GIA_FILE), GIA=g) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_Ylms = gravtk.read_GIA_model( + base_dir.joinpath(*GIA_FILE), GIA=g + ) gia_str = gia_Ylms['title'] - input_file = ff.format(gia_str,reg,'',atm_str,ocean_str,LMAX,gw_str,ds_str) - dinput = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) - mon[:] = dinput[:nmon,0].astype(np.int64) - tdec[:] = dinput[:nmon,1] - mass[:,k] = dinput[:nmon,2] - satellite_error[:] += dinput[:nmon,3]**2 + input_file = ff.format( + gia_str, reg, '', atm_str, ocean_str, LMAX, gw_str, ds_str + ) + dinput = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) + mon[:] = dinput[:nmon, 0].astype(np.int64) + tdec[:] = dinput[:nmon, 1] + mass[:, k] = dinput[:nmon, 2] + satellite_error[:] += dinput[:nmon, 3] ** 2 # calculate mean GIA-corrected mass change (for all Earth rheologies) gia_corrected_mean = np.mean(mass, axis=1) # GRACE satellite error component, ocean leakage (ECCO-GAD), # atmosphere leakage (ATM-GAA) and GLDAS TWC - grace_error = np.sqrt(np.sum(satellite_error/nRheology + SLF_RMS**2 + - OBP_RMS**2 + ATM_RMS**2 + TWC_RMS**2)/nmon) + grace_error = np.sqrt( + np.sum( + satellite_error / nRheology + + SLF_RMS**2 + + OBP_RMS**2 + + ATM_RMS**2 + + TWC_RMS**2 + ) + / nmon + ) # calculate variance off of mean for calculating GIA uncertainty gia_corrected_variance = np.zeros((nmon)) gia_corrected_minmax = np.zeros((nmon)) # calculate GIA uncertainty as "worst-case" not RMS - for k,GIA_FILE in enumerate(gia_files[g]): - gia_corrected_variance+=np.abs(mass[:,k]-gia_corrected_mean) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_corrected_variance += np.abs( + mass[:, k] - gia_corrected_mean + ) # calculate GIA uncertainty as "worst-case" min max error for t in range(nmon): - gia_corrected_minmax[t]=np.abs(np.max(mass[t,:])-np.min(mass[t,:])) + gia_corrected_minmax[t] = np.abs( + np.max(mass[t, :]) - np.min(mass[t, :]) + ) # calculate uncertainty in mean GIA - gia_corrected_error = gia_corrected_variance/(np.float64(nRheology)-1.0) - gia_corrected_error = gia_corrected_error*np.sign(tdec-tdec.mean()) - gia_corrected_minmax = gia_corrected_minmax*np.sign(tdec-tdec.mean()) - gia_error_rate = (gia_corrected_error[-1]-gia_corrected_error[0])/(tdec[-1]-tdec[0]) - gia_minmax_rate = (gia_corrected_minmax[-1]-gia_corrected_minmax[0])/(tdec[-1]-tdec[0]) + gia_corrected_error = gia_corrected_variance / ( + np.float64(nRheology) - 1.0 + ) + gia_corrected_error = gia_corrected_error * np.sign( + tdec - tdec.mean() + ) + gia_corrected_minmax = gia_corrected_minmax * np.sign( + tdec - tdec.mean() + ) + gia_error_rate = ( + gia_corrected_error[-1] - gia_corrected_error[0] + ) / (tdec[-1] - tdec[0]) + gia_minmax_rate = ( + gia_corrected_minmax[-1] - gia_corrected_minmax[0] + ) / (tdec[-1] - tdec[0]) # calculate GIA errors at confidence interval # t.ppf parallels tinv in matlab - tstar = scipy.stats.t.ppf(1.0-(alpha/2.0),nRheology-1.0) if g in ('IJ05-R2','SM09') else 2.0 - gia_corrected_conf = tstar*np.abs(gia_error_rate) + tstar = ( + scipy.stats.t.ppf(1.0 - (alpha / 2.0), nRheology - 1.0) + if g in ('IJ05-R2', 'SM09') + else 2.0 + ) + gia_corrected_conf = tstar * np.abs(gia_error_rate) # add to plot with colors and label plot_label = plot_title[i] # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - mnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): + tnan = np.full_like(month, np.nan, dtype=np.float64) + mnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): valid = np.count_nonzero(mon == m) if valid: - mm, = np.nonzero(mon == m) + (mm,) = np.nonzero(mon == m) tnan[d] = tdec[mm] - mnan[d] = gia_corrected_mean[mm] - np.mean(gia_corrected_mean) + mnan[d] = gia_corrected_mean[mm] - np.mean( + gia_corrected_mean + ) # plot all dates - ax.plot(tnan, mnan, color=plot_colors[i], label=plot_label, zorder=3+i) + ax.plot( + tnan, mnan, color=plot_colors[i], label=plot_label, zorder=3 + i + ) # fill between monthly errors - ax.fill_between(tnan, mnan-grace_error, y2=mnan+grace_error, - color=plot_colors[i], alpha=0.35, zorder=1+i) + ax.fill_between( + tnan, + mnan - grace_error, + y2=mnan + grace_error, + color=plot_colors[i], + alpha=0.35, + zorder=1 + i, + ) # plot RACMO SMB time series for comparison - if ('RACMO' in SMB): + if 'RACMO' in SMB: # read RACMO surface mass balance file - subdir = sd.format('RACMO2.3p2','FGRN055_DS1km_v4.0_SMB_cumul','','',LMAX,RACMO_START,RACMO_END) - RACMO_file = ff.format('RACMO2.3p2','FGRN055_DS1km_v4.0_SMB_cumul_',reg,'RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - RACMO = np.loadtxt(base_dir.joinpath('RACMO','SMB1km_v4.0',subdir,RACMO_file))[:nmon,:] + subdir = sd.format( + 'RACMO2.3p2', + 'FGRN055_DS1km_v4.0_SMB_cumul', + '', + '', + LMAX, + RACMO_START, + RACMO_END, + ) + RACMO_file = ff.format( + 'RACMO2.3p2', + 'FGRN055_DS1km_v4.0_SMB_cumul_', + reg, + 'RAD1.5_', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + RACMO = np.loadtxt( + base_dir.joinpath('RACMO', 'SMB1km_v4.0', subdir, RACMO_file) + )[:nmon, :] # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - mnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): - valid = np.count_nonzero(RACMO[:,0] == m) + tnan = np.full_like(month, np.nan, dtype=np.float64) + mnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): + valid = np.count_nonzero(RACMO[:, 0] == m) if valid: - mm, = np.nonzero(RACMO[:,0] == m) - tnan[d] = RACMO[mm,1] - mnan[d] = RACMO[mm,2] - np.mean(RACMO[:,2]) + (mm,) = np.nonzero(RACMO[:, 0] == m) + tnan[d] = RACMO[mm, 1] + mnan[d] = RACMO[mm, 2] - np.mean(RACMO[:, 2]) # plot all dates - ax.plot(tnan, mnan, color=plot_colors[2], label='RACMO2.3p2', zorder=5) + ax.plot( + tnan, mnan, color=plot_colors[2], label='RACMO2.3p2', zorder=5 + ) # plot GSFC-fdm SMB time series for comparison - if ('GSFC-fdm' in SMB): + if 'GSFC-fdm' in SMB: # read MERRA-2 hybrid surface mass balance file - subdir = 'HEX_GSFC_FDM_{0}_{1}_{2}_L{3:d}'.format('v1_2_1','gris','SMB_a',LMAX) - MERRA2_file = ff.format('GSFC_FDM','v1_2_1_SMB_a_',reg,'RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - MERRA2 = np.loadtxt(base_dir.joinpath('MERRA2_hybrid','v1.2.1',subdir,MERRA2_file)) + subdir = 'HEX_GSFC_FDM_{0}_{1}_{2}_L{3:d}'.format( + 'v1_2_1', 'gris', 'SMB_a', LMAX + ) + MERRA2_file = ff.format( + 'GSFC_FDM', + 'v1_2_1_SMB_a_', + reg, + 'RAD1.5_', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + MERRA2 = np.loadtxt( + base_dir.joinpath( + 'MERRA2_hybrid', 'v1.2.1', subdir, MERRA2_file + ) + ) # plot all dates - ax.plot(MERRA2[:,1], MERRA2[:,2] - np.mean(MERRA2[:,2]), - color=plot_colors[3], label='GSFC-fdm v1.2.1', zorder=5) + ax.plot( + MERRA2[:, 1], + MERRA2[:, 2] - np.mean(MERRA2[:, 2]), + color=plot_colors[3], + label='GSFC-fdm v1.2.1', + zorder=5, + ) # plot JPL-GEMB SMB time series for comparison - if ('GEMB' in SMB): + if 'GEMB' in SMB: # read GEMB surface mass balance file - subdir = sd.format('GEMB','v1_2_Greenland_SMB_cumul','','',LMAX,GEMB_START,GEMB_END) - GEMB_file = ff.format('GEMB','v1_2_SMB_cumul_',reg,'RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - GEMB = np.loadtxt(base_dir.joinpath('GEMB','v1.2',subdir,GEMB_file)) + subdir = sd.format( + 'GEMB', + 'v1_2_Greenland_SMB_cumul', + '', + '', + LMAX, + GEMB_START, + GEMB_END, + ) + GEMB_file = ff.format( + 'GEMB', + 'v1_2_SMB_cumul_', + reg, + 'RAD1.5_', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + GEMB = np.loadtxt( + base_dir.joinpath('GEMB', 'v1.2', subdir, GEMB_file) + ) # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - mnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): - valid = np.count_nonzero(GEMB[:,0] == m) + tnan = np.full_like(month, np.nan, dtype=np.float64) + mnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): + valid = np.count_nonzero(GEMB[:, 0] == m) if valid: - mm, = np.nonzero(GEMB[:,0] == m) - tnan[d] = GEMB[mm,1] - mnan[d] = GEMB[mm,2] - np.mean(GEMB[:,2]) + (mm,) = np.nonzero(GEMB[:, 0] == m) + tnan[d] = GEMB[mm, 1] + mnan[d] = GEMB[mm, 2] - np.mean(GEMB[:, 2]) # plot all dates - ax.plot(tnan, mnan, color=plot_colors[4], label='GEMB v1.2', zorder=5) + ax.plot( + tnan, mnan, color=plot_colors[4], label='GEMB v1.2', zorder=5 + ) # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9, - day=3,hour=12,minute=12) - ax.axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax.axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(mon == 186) - kk, = np.flatnonzero(mon == 198) + (jj,) = np.flatnonzero(mon == 186) + (kk,) = np.flatnonzero(mon == 198) # ax.axvline(tdec[jj],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) # ax.axvline(tdec[kk],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) - vs = ax.axvspan(tdec[jj],tdec[kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) + vs = ax.axvspan( + tdec[jj], tdec[kk], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (6, 3) # add labels - textprops = dict(size=14,weight='bold') - at = matplotlib.offsetbox.AnchoredText(fig_text[reg], - prop=textprops, pad=0, frameon=False, loc=2) + textprops = dict(size=14, weight='bold') + at = matplotlib.offsetbox.AnchoredText( + fig_text[reg], prop=textprops, pad=0, frameon=False, loc=2 + ) ax.add_artist(at) - textprops = dict(size=14,weight='bold') - at = matplotlib.offsetbox.AnchoredText(reg_text[reg], - prop=textprops, pad=0, frameon=False, loc=1) + textprops = dict(size=14, weight='bold') + at = matplotlib.offsetbox.AnchoredText( + reg_text[reg], prop=textprops, pad=0, frameon=False, loc=1 + ) ax.add_artist(at) # set ticks major_ticks = np.arange(2002, 2024, 4) @@ -349,7 +557,7 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= ax.set_ylabel('Mass [Gt]', labelpad=ypad[reg]) # add legend - lgd = ax1['GIS'].legend(loc=3,frameon=False) + lgd = ax1['GIS'].legend(loc=3, frameon=False) # lgd = ax1['GIS'].legend(loc=3,frameon=False,handletextpad=-0.2,handlelength=0) # set width, color and style of lines # lgd.get_frame().set_boxstyle('square,pad=0.1') @@ -358,13 +566,15 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= for line in lgd.get_lines(): line.set_linewidth(6) # line.set_linewidth(0) - for i,text in enumerate(lgd.get_texts()): + for i, text in enumerate(lgd.get_texts()): text.set_color(plot_colors[i]) text.set_weight('bold') # adjust plot to figure dimensions - fig.subplots_adjust(left=0.0625,right=0.99,bottom=0.05,top=0.99,wspace=0.2,hspace=0.125) - figurefile = filepath.joinpath('fig3af_{0}_{1}.pdf'.format(PROC,DREL)) + fig.subplots_adjust( + left=0.0625, right=0.99, bottom=0.05, top=0.99, wspace=0.2, hspace=0.125 + ) + figurefile = filepath.joinpath('fig3af_{0}_{1}.pdf'.format(PROC, DREL)) plt.savefig(figurefile, format='pdf') plt.cla() plt.clf() @@ -373,40 +583,89 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= # create figure axis for Antarctic plots ax1 = {} ax2 = {} - fig, ((ax1['AIS'], ax1['APIS'], ax1['GH3']), (ax1['QML'], ax1['CpDc'], ax1['DDpi'])) = \ - plt.subplots(num=2, nrows=2, ncols=3, sharex=False, sharey=False, figsize=(12, 7.5)) - ylimits = {'AIS':[-1700,1600,250],'APIS':[-350,350,50],'GH3':[-1400,1400,200],\ - 'QML':[-750,850,100],'CpDc':[-400,300,50],'DDpi':[-200,250,50]} - fig_text = {'AIS':'a)','APIS':'b)','GH3':'c)','QML':'d)','CpDc':'e)','DDpi':'f)'} - reg_text = {'AIS':'AIS','APIS':'APIS','GH3':'ASE','QML':'QML','CpDc':'TMF','DDpi':'VW'} + ( + fig, + ( + (ax1['AIS'], ax1['APIS'], ax1['GH3']), + (ax1['QML'], ax1['CpDc'], ax1['DDpi']), + ), + ) = plt.subplots( + num=2, nrows=2, ncols=3, sharex=False, sharey=False, figsize=(12, 7.5) + ) + ylimits = { + 'AIS': [-1700, 1600, 250], + 'APIS': [-350, 350, 50], + 'GH3': [-1400, 1400, 200], + 'QML': [-750, 850, 100], + 'CpDc': [-400, 300, 50], + 'DDpi': [-200, 250, 50], + } + fig_text = { + 'AIS': 'a)', + 'APIS': 'b)', + 'GH3': 'c)', + 'QML': 'd)', + 'CpDc': 'e)', + 'DDpi': 'f)', + } + reg_text = { + 'AIS': 'AIS', + 'APIS': 'APIS', + 'GH3': 'ASE', + 'QML': 'QML', + 'CpDc': 'TMF', + 'DDpi': 'VW', + } h = 'S' SLF = '_SLF3' - RACMO_START,RACMO_END = (4,243) - GEMB_START,GEMB_END = (4,251) - ypad = dict(AIS=0,APIS=4,GH3=0,QML=4,CpDc=4,DDpi=4) - for reg,ax in ax1.items(): + RACMO_START, RACMO_END = (4, 243) + GEMB_START, GEMB_END = (4, 251) + ypad = dict(AIS=0, APIS=4, GH3=0, QML=4, CpDc=4, DDpi=4) + for reg, ax in ax1.items(): # read ocean bottom pressure leakage file - subdir = sd.format('AOD1B',DREL,'','',LMAX,OBP_START,OBP_END) - OBP_file = ff.format('ECCO-GAD_OBP_Residuals',reg,'','',ocean_str,LMAX,gw_str,ds_str) - OBP_input = np.loadtxt(mascon_dir.joinpath(subdir,OBP_file))[:nmon,:] + subdir = sd.format('AOD1B', DREL, '', '', LMAX, OBP_START, OBP_END) + OBP_file = ff.format( + 'ECCO-GAD_OBP_Residuals', + reg, + '', + '', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + OBP_input = np.loadtxt(mascon_dir.joinpath(subdir, OBP_file))[:nmon, :] # read atmospheric pressure leakage file - subdir = sd.format('AOD1B',DREL,'','',LMAX,ATM_START,ATM_END) + subdir = sd.format('AOD1B', DREL, '', '', LMAX, ATM_START, ATM_END) # ATM_file = ff.format('ATM-GAA_Residuals',reg,'_3D','',ocean_str,LMAX,gw_str) # ATM_file = ff.format('ATM_Differences',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) - ATM_file = ff.format('ATM_Differences',reg,'','',ocean_str,LMAX,gw_str,ds_str) - ATM_input = np.loadtxt(mascon_dir.joinpath(subdir,ATM_file))[:nmon,:] + ATM_file = ff.format( + 'ATM_Differences', reg, '', '', ocean_str, LMAX, gw_str, ds_str + ) + ATM_input = np.loadtxt(mascon_dir.joinpath(subdir, ATM_file))[:nmon, :] # read GLDAS terrestrial water RMS file - subdir = sd.format('GLDAS','TWC_V2.1_RMS','','',LMAX,GLDAS_START,GLDAS_END) - TWC_file = ff.format('GLDAS_TWC_RMS',reg,'','RAD1.5_','',LMAX,gw_str,ds_str) - TWC_input = np.loadtxt(base_dir.joinpath('GLDAS',subdir,TWC_file))[:nmon,:] - isvalid, = np.nonzero(np.isfinite(TWC_input[:,2]) & - (TWC_input[:,0] >= START_MON) & (TWC_input[:,0] <= END_MON)) - TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid,2]**2)/len(isvalid)) + subdir = sd.format( + 'GLDAS', 'TWC_V2.1_RMS', '', '', LMAX, GLDAS_START, GLDAS_END + ) + TWC_file = ff.format( + 'GLDAS_TWC_RMS', reg, '', 'RAD1.5_', '', LMAX, gw_str, ds_str + ) + TWC_input = np.loadtxt(base_dir.joinpath('GLDAS', subdir, TWC_file))[ + :nmon, : + ] + (isvalid,) = np.nonzero( + np.isfinite(TWC_input[:, 2]) + & (TWC_input[:, 0] >= START_MON) + & (TWC_input[:, 0] <= END_MON) + ) + TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid, 2] ** 2) / len(isvalid)) # input estimated SLF monte carlo variance file and calculate RMS - subdir = sd.format(PROC,DREL,'','_MC',LMAX,START_MON,END_MON) - SLF_file = ff.format('MC',reg,'','RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - SLF_input = np.loadtxt(mascon_dir.joinpath(subdir,SLF_file)) - SLF_RMS = np.sqrt(np.sum(SLF_input[:,1]**2)/len(SLF_input)) + subdir = sd.format(PROC, DREL, '', '_MC', LMAX, START_MON, END_MON) + SLF_file = ff.format( + 'MC', reg, '', 'RAD1.5_', ocean_str, LMAX, gw_str, ds_str + ) + SLF_input = np.loadtxt(mascon_dir.joinpath(subdir, SLF_file)) + SLF_RMS = np.sqrt(np.sum(SLF_input[:, 1] ** 2) / len(SLF_input)) # calculate mean of RMS over multiple reanalyses OBP_RMS = 0.0 ATM_RMS = 0.0 @@ -414,16 +673,16 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= # ivalid, = np.nonzero(np.isfinite(OBP_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(OBP_input[:,j+3])) # OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(OBP_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(OBP_input[:,2])) - OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(OBP_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(OBP_input[:, 2])) + OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid, 2] ** 2) / valid_count) # for j in range(4): # ivalid, = np.nonzero(np.isfinite(ATM_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(ATM_input[:,j+3])) # ATM_RMS += np.sqrt(np.sum(OBP_input[ATM_input,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(ATM_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(ATM_input[:,2])) - ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(ATM_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(ATM_input[:, 2])) + ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid, 2] ** 2) / valid_count) # # divide by the number of reanalyses # OBP_RMS /= 2.0 # ATM_RMS /= 4.0 @@ -432,130 +691,239 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= # number of rheologies to iterate nRheology = len(gia_files[g]) # iterate through solutions - mon = np.zeros((nmon),dtype=np.int64) - tdec = np.zeros((nmon),dtype=np.float64) - mass = np.zeros((nmon,nRheology),dtype=np.float64) - satellite_error = np.zeros((nmon),dtype=np.float64) - for i,F in enumerate(VERSION): + mon = np.zeros((nmon), dtype=np.int64) + tdec = np.zeros((nmon), dtype=np.float64) + mass = np.zeros((nmon, nRheology), dtype=np.float64) + satellite_error = np.zeros((nmon), dtype=np.float64) + for i, F in enumerate(VERSION): # subdirectory - subdir = sd.format(PROC,DREL,F,SLF,LMAX,START_MON,END_MON) + subdir = sd.format(PROC, DREL, F, SLF, LMAX, START_MON, END_MON) # read each GIA model - for k,GIA_FILE in enumerate(gia_files[g]): - gia_Ylms = gravtk.read_GIA_model(base_dir.joinpath(*GIA_FILE), GIA=g) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_Ylms = gravtk.read_GIA_model( + base_dir.joinpath(*GIA_FILE), GIA=g + ) gia_str = gia_Ylms['title'] - input_file = ff.format(gia_str,reg,'',atm_str,ocean_str,LMAX,gw_str,ds_str) - dinput = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) - mon[:] = dinput[:nmon,0].astype(np.int64) - tdec[:] = dinput[:nmon,1] - mass[:,k] = dinput[:nmon,2] - satellite_error[:] += dinput[:nmon,3]**2 + input_file = ff.format( + gia_str, reg, '', atm_str, ocean_str, LMAX, gw_str, ds_str + ) + dinput = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) + mon[:] = dinput[:nmon, 0].astype(np.int64) + tdec[:] = dinput[:nmon, 1] + mass[:, k] = dinput[:nmon, 2] + satellite_error[:] += dinput[:nmon, 3] ** 2 # calculate mean GIA-corrected mass change (for all Earth rheologies) gia_corrected_mean = np.mean(mass, axis=1) # GRACE satellite error component, ocean leakage (ECCO-GAD), # atmosphere leakage (ATM-GAA) and GLDAS TWC - grace_error = np.sqrt(np.sum(satellite_error/nRheology + SLF_RMS**2 + - OBP_RMS**2 + ATM_RMS**2 + TWC_RMS**2)/nmon) + grace_error = np.sqrt( + np.sum( + satellite_error / nRheology + + SLF_RMS**2 + + OBP_RMS**2 + + ATM_RMS**2 + + TWC_RMS**2 + ) + / nmon + ) # calculate variance off of mean for calculating GIA uncertainty gia_corrected_variance = np.zeros((nmon)) gia_corrected_minmax = np.zeros((nmon)) # calculate GIA uncertainty as "worst-case" not RMS - for k,GIA_FILE in enumerate(gia_files[g]): - gia_corrected_variance+=np.abs(mass[:,k]-gia_corrected_mean) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_corrected_variance += np.abs( + mass[:, k] - gia_corrected_mean + ) # calculate GIA uncertainty as "worst-case" min max error for t in range(nmon): - gia_corrected_minmax[t]=np.abs(np.max(mass[t,:])-np.min(mass[t,:])) + gia_corrected_minmax[t] = np.abs( + np.max(mass[t, :]) - np.min(mass[t, :]) + ) # calculate uncertainty in mean GIA - gia_corrected_error = gia_corrected_variance/(np.float64(nRheology)-1.0) - gia_corrected_error = gia_corrected_error*np.sign(tdec-tdec.mean()) - gia_corrected_minmax = gia_corrected_minmax*np.sign(tdec-tdec.mean()) - gia_error_rate = (gia_corrected_error[-1]-gia_corrected_error[0])/(tdec[-1]-tdec[0]) - gia_minmax_rate = (gia_corrected_minmax[-1]-gia_corrected_minmax[0])/(tdec[-1]-tdec[0]) + gia_corrected_error = gia_corrected_variance / ( + np.float64(nRheology) - 1.0 + ) + gia_corrected_error = gia_corrected_error * np.sign( + tdec - tdec.mean() + ) + gia_corrected_minmax = gia_corrected_minmax * np.sign( + tdec - tdec.mean() + ) + gia_error_rate = ( + gia_corrected_error[-1] - gia_corrected_error[0] + ) / (tdec[-1] - tdec[0]) + gia_minmax_rate = ( + gia_corrected_minmax[-1] - gia_corrected_minmax[0] + ) / (tdec[-1] - tdec[0]) # calculate GIA errors at confidence interval # t.ppf parallels tinv in matlab - tstar = scipy.stats.t.ppf(1.0-(alpha/2.0),nRheology-1.0) if g in ('IJ05-R2','SM09') else 2.0 - gia_corrected_conf = tstar*np.abs(gia_error_rate) + tstar = ( + scipy.stats.t.ppf(1.0 - (alpha / 2.0), nRheology - 1.0) + if g in ('IJ05-R2', 'SM09') + else 2.0 + ) + gia_corrected_conf = tstar * np.abs(gia_error_rate) # add to plot with colors and label plot_label = plot_title[i] # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - mnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): + tnan = np.full_like(month, np.nan, dtype=np.float64) + mnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): valid = np.count_nonzero(mon == m) if valid: - mm, = np.nonzero(mon == m) + (mm,) = np.nonzero(mon == m) tnan[d] = tdec[mm] - mnan[d] = gia_corrected_mean[mm] - np.mean(gia_corrected_mean) + mnan[d] = gia_corrected_mean[mm] - np.mean( + gia_corrected_mean + ) # plot all dates - ax.plot(tnan, mnan, color=plot_colors[i], label=plot_label, zorder=3+i) + ax.plot( + tnan, mnan, color=plot_colors[i], label=plot_label, zorder=3 + i + ) # fill between monthly errors - ax.fill_between(tnan, mnan-grace_error, y2=mnan+grace_error, - color=plot_colors[i], alpha=0.35, zorder=1+i) + ax.fill_between( + tnan, + mnan - grace_error, + y2=mnan + grace_error, + color=plot_colors[i], + alpha=0.35, + zorder=1 + i, + ) # plot RACMO SMB time series for comparison - if ('RACMO' in SMB): + if 'RACMO' in SMB: # read RACMO surface mass balance file - subdir = sd.format('RACMO2.3p2','ANT27_SMB_cumul','','',LMAX,RACMO_START,RACMO_END) - RACMO_file = ff.format('RACMO2.3p2','ANT27_SMB_cumul_',reg,'RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - RACMO = np.loadtxt(base_dir.joinpath('RACMO','XANT27_1979-2022',subdir,RACMO_file))[:nmon,:] + subdir = sd.format( + 'RACMO2.3p2', + 'ANT27_SMB_cumul', + '', + '', + LMAX, + RACMO_START, + RACMO_END, + ) + RACMO_file = ff.format( + 'RACMO2.3p2', + 'ANT27_SMB_cumul_', + reg, + 'RAD1.5_', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + RACMO = np.loadtxt( + base_dir.joinpath( + 'RACMO', 'XANT27_1979-2022', subdir, RACMO_file + ) + )[:nmon, :] # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - mnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): - valid = np.count_nonzero(RACMO[:,0] == m) + tnan = np.full_like(month, np.nan, dtype=np.float64) + mnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): + valid = np.count_nonzero(RACMO[:, 0] == m) if valid: - mm, = np.nonzero(RACMO[:,0] == m) - tnan[d] = RACMO[mm,1] - mnan[d] = RACMO[mm,2] - np.mean(RACMO[:,2]) + (mm,) = np.nonzero(RACMO[:, 0] == m) + tnan[d] = RACMO[mm, 1] + mnan[d] = RACMO[mm, 2] - np.mean(RACMO[:, 2]) # plot all dates - ax.plot(tnan, mnan, color=plot_colors[2], label='RACMO2.3p2', zorder=5) + ax.plot( + tnan, mnan, color=plot_colors[2], label='RACMO2.3p2', zorder=5 + ) # plot GSFC-fdm SMB time series for comparison - if ('GSFC-fdm' in SMB): + if 'GSFC-fdm' in SMB: # read MERRA-2 hybrid surface mass balance file - subdir = 'HEX_GSFC_FDM_{0}_{1}_{2}_L{3:d}'.format('v1_2_1','ais','SMB_a',LMAX) - MERRA2_file = ff.format('GSFC_FDM','v1_2_1_SMB_a_',reg,'RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - MERRA2 = np.loadtxt(base_dir.joinpath('MERRA2_hybrid','v1.2.1',subdir,MERRA2_file)) + subdir = 'HEX_GSFC_FDM_{0}_{1}_{2}_L{3:d}'.format( + 'v1_2_1', 'ais', 'SMB_a', LMAX + ) + MERRA2_file = ff.format( + 'GSFC_FDM', + 'v1_2_1_SMB_a_', + reg, + 'RAD1.5_', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + MERRA2 = np.loadtxt( + base_dir.joinpath( + 'MERRA2_hybrid', 'v1.2.1', subdir, MERRA2_file + ) + ) # plot all dates - ax.plot(MERRA2[:,1], MERRA2[:,2] - np.mean(MERRA2[:,2]), - color=plot_colors[3], label='GSFC-fdm v1.2.1', zorder=5) + ax.plot( + MERRA2[:, 1], + MERRA2[:, 2] - np.mean(MERRA2[:, 2]), + color=plot_colors[3], + label='GSFC-fdm v1.2.1', + zorder=5, + ) # plot JPL-GEMB SMB time series for comparison - if ('GEMB' in SMB): + if 'GEMB' in SMB: # read GEMB surface mass balance file - subdir = sd.format('GEMB','v1_2_Antarctica_SMB_cumul','','',LMAX,GEMB_START,GEMB_END) - GEMB_file = ff.format('GEMB','v1_2_SMB_cumul_',reg,'RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - GEMB = np.loadtxt(base_dir.joinpath('GEMB','v1.2',subdir,GEMB_file)) + subdir = sd.format( + 'GEMB', + 'v1_2_Antarctica_SMB_cumul', + '', + '', + LMAX, + GEMB_START, + GEMB_END, + ) + GEMB_file = ff.format( + 'GEMB', + 'v1_2_SMB_cumul_', + reg, + 'RAD1.5_', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + GEMB = np.loadtxt( + base_dir.joinpath('GEMB', 'v1.2', subdir, GEMB_file) + ) # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - mnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): - valid = np.count_nonzero(GEMB[:,0] == m) + tnan = np.full_like(month, np.nan, dtype=np.float64) + mnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): + valid = np.count_nonzero(GEMB[:, 0] == m) if valid: - mm, = np.nonzero(GEMB[:,0] == m) - tnan[d] = GEMB[mm,1] - mnan[d] = GEMB[mm,2] - np.mean(GEMB[:,2]) + (mm,) = np.nonzero(GEMB[:, 0] == m) + tnan[d] = GEMB[mm, 1] + mnan[d] = GEMB[mm, 2] - np.mean(GEMB[:, 2]) # plot all dates - ax.plot(tnan, mnan, color=plot_colors[4], label='GEMB v1.2', zorder=5) + ax.plot( + tnan, mnan, color=plot_colors[4], label='GEMB v1.2', zorder=5 + ) # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9, - day=3,hour=12,minute=12) - ax.axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax.axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(mon == 186) - kk, = np.flatnonzero(mon == 198) + (jj,) = np.flatnonzero(mon == 186) + (kk,) = np.flatnonzero(mon == 198) # ax.axvline(tdec[jj],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) # ax.axvline(tdec[kk],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) - vs = ax.axvspan(tdec[jj],tdec[kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) + vs = ax.axvspan( + tdec[jj], tdec[kk], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (6, 3) # add labels - textprops = dict(size=14,weight='bold') - at = matplotlib.offsetbox.AnchoredText(fig_text[reg], - prop=textprops, pad=0, frameon=False, loc=2) + textprops = dict(size=14, weight='bold') + at = matplotlib.offsetbox.AnchoredText( + fig_text[reg], prop=textprops, pad=0, frameon=False, loc=2 + ) ax.add_artist(at) - textprops = dict(size=14,weight='bold') - at = matplotlib.offsetbox.AnchoredText(reg_text[reg], - prop=textprops, pad=0, frameon=False, loc=1) + textprops = dict(size=14, weight='bold') + at = matplotlib.offsetbox.AnchoredText( + reg_text[reg], prop=textprops, pad=0, frameon=False, loc=1 + ) ax.add_artist(at) # set ticks major_ticks = np.arange(2002, 2024, 4) @@ -573,22 +941,24 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= ax.set_ylabel('Mass [Gt]', labelpad=ypad[reg]) # add legend - lgd = ax1['AIS'].legend(loc=3,frameon=False) + lgd = ax1['AIS'].legend(loc=3, frameon=False) # lgd = ax1['AIS'].legend(loc=3,frameon=False,handletextpad=-0.2,handlelength=0) # set width, color and style of lines # lgd.get_frame().set_boxstyle('square,pad=0.1') # lgd.get_frame().set_edgecolor('black') lgd.get_frame().set_alpha(1.0) for line in lgd.get_lines(): - line.set_linewidth(6) + line.set_linewidth(6) # line.set_linewidth(0) - for i,text in enumerate(lgd.get_texts()): + for i, text in enumerate(lgd.get_texts()): text.set_color(plot_colors[i]) text.set_weight('bold') # adjust plot to figure dimensions - fig.subplots_adjust(left=0.0625,right=0.99,bottom=0.05,top=0.99,wspace=0.2,hspace=0.125) - figurefile = filepath.joinpath('fig4af_{0}_{1}.pdf'.format(PROC,DREL)) + fig.subplots_adjust( + left=0.0625, right=0.99, bottom=0.05, top=0.99, wspace=0.2, hspace=0.125 + ) + figurefile = filepath.joinpath('fig4af_{0}_{1}.pdf'.format(PROC, DREL)) plt.savefig(figurefile, format='pdf') plt.cla() plt.clf() @@ -597,36 +967,60 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= # create figure axis for Antarctic plots ax1 = {} ax2 = {} - fig, (ax1['EAIS'], ax1['INTERIOR']) = plt.subplots(num=1,ncols=2,figsize=(8,3.75)) - ylimits = {'EAIS':[-1700,1600,250],'INTERIOR':[-650,650,100]} - fig_text = {'EAIS':'a)','INTERIOR':'b)'} - reg_text = {'EAIS':'EAIS','INTERIOR':'EAIS Interior'} + fig, (ax1['EAIS'], ax1['INTERIOR']) = plt.subplots( + num=1, ncols=2, figsize=(8, 3.75) + ) + ylimits = {'EAIS': [-1700, 1600, 250], 'INTERIOR': [-650, 650, 100]} + fig_text = {'EAIS': 'a)', 'INTERIOR': 'b)'} + reg_text = {'EAIS': 'EAIS', 'INTERIOR': 'EAIS Interior'} h = 'S' SLF = '_SLF3' - ypad = dict(EAIS=4,INTERIOR=4) - for reg,ax in ax1.items(): + ypad = dict(EAIS=4, INTERIOR=4) + for reg, ax in ax1.items(): # read ocean bottom pressure leakage file - subdir = sd.format('AOD1B',DREL,'','',LMAX,OBP_START,OBP_END) - OBP_file = ff.format('ECCO-GAD_OBP_Residuals',reg,'','',ocean_str,LMAX,gw_str,ds_str) - OBP_input = np.loadtxt(mascon_dir.joinpath(subdir,OBP_file))[:nmon,:] + subdir = sd.format('AOD1B', DREL, '', '', LMAX, OBP_START, OBP_END) + OBP_file = ff.format( + 'ECCO-GAD_OBP_Residuals', + reg, + '', + '', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + OBP_input = np.loadtxt(mascon_dir.joinpath(subdir, OBP_file))[:nmon, :] # read atmospheric pressure leakage file - subdir = sd.format('AOD1B',DREL,'','',LMAX,ATM_START,ATM_END) + subdir = sd.format('AOD1B', DREL, '', '', LMAX, ATM_START, ATM_END) # ATM_file = ff.format('ATM-GAA_Residuals',reg,'_3D','',ocean_str,LMAX,gw_str) # ATM_file = ff.format('ATM_Differences',reg,'_3D','',ocean_str,LMAX,gw_str,ds_str) - ATM_file = ff.format('ATM_Differences',reg,'','',ocean_str,LMAX,gw_str,ds_str) - ATM_input = np.loadtxt(mascon_dir.joinpath(subdir,ATM_file))[:nmon,:] + ATM_file = ff.format( + 'ATM_Differences', reg, '', '', ocean_str, LMAX, gw_str, ds_str + ) + ATM_input = np.loadtxt(mascon_dir.joinpath(subdir, ATM_file))[:nmon, :] # read GLDAS terrestrial water RMS file - subdir = sd.format('GLDAS','TWC_V2.1_RMS','','',LMAX,GLDAS_START,GLDAS_END) - TWC_file = ff.format('GLDAS_TWC_RMS',reg,'','RAD1.5_','',LMAX,gw_str,ds_str) - TWC_input = np.loadtxt(base_dir.joinpath('GLDAS',subdir,TWC_file))[:nmon,:] - isvalid, = np.nonzero(np.isfinite(TWC_input[:,2]) & - (TWC_input[:,0] >= START_MON) & (TWC_input[:,0] <= END_MON)) - TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid,2]**2)/len(isvalid)) + subdir = sd.format( + 'GLDAS', 'TWC_V2.1_RMS', '', '', LMAX, GLDAS_START, GLDAS_END + ) + TWC_file = ff.format( + 'GLDAS_TWC_RMS', reg, '', 'RAD1.5_', '', LMAX, gw_str, ds_str + ) + TWC_input = np.loadtxt(base_dir.joinpath('GLDAS', subdir, TWC_file))[ + :nmon, : + ] + (isvalid,) = np.nonzero( + np.isfinite(TWC_input[:, 2]) + & (TWC_input[:, 0] >= START_MON) + & (TWC_input[:, 0] <= END_MON) + ) + TWC_RMS = np.sqrt(np.sum(TWC_input[isvalid, 2] ** 2) / len(isvalid)) # input estimated SLF monte carlo variance file and calculate RMS - subdir = sd.format(PROC,DREL,'','_MC',LMAX,START_MON,END_MON) - SLF_file = ff.format('MC',reg,'','RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - SLF_input = np.loadtxt(mascon_dir.joinpath(subdir,SLF_file)) - SLF_RMS = np.sqrt(np.sum(SLF_input[:,1]**2)/len(SLF_input)) + subdir = sd.format(PROC, DREL, '', '_MC', LMAX, START_MON, END_MON) + SLF_file = ff.format( + 'MC', reg, '', 'RAD1.5_', ocean_str, LMAX, gw_str, ds_str + ) + SLF_input = np.loadtxt(mascon_dir.joinpath(subdir, SLF_file)) + SLF_RMS = np.sqrt(np.sum(SLF_input[:, 1] ** 2) / len(SLF_input)) # calculate mean of RMS over multiple reanalyses OBP_RMS = 0.0 ATM_RMS = 0.0 @@ -634,16 +1028,16 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= # ivalid, = np.nonzero(np.isfinite(OBP_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(OBP_input[:,j+3])) # OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(OBP_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(OBP_input[:,2])) - OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(OBP_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(OBP_input[:, 2])) + OBP_RMS += np.sqrt(np.sum(OBP_input[ivalid, 2] ** 2) / valid_count) # for j in range(4): # ivalid, = np.nonzero(np.isfinite(ATM_input[:,j+3])) # valid_count = np.count_nonzero(np.isfinite(ATM_input[:,j+3])) # ATM_RMS += np.sqrt(np.sum(OBP_input[ATM_input,j+3]**2)/valid_count) - ivalid, = np.nonzero(np.isfinite(ATM_input[:,2])) - valid_count = np.count_nonzero(np.isfinite(ATM_input[:,2])) - ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid,2]**2)/valid_count) + (ivalid,) = np.nonzero(np.isfinite(ATM_input[:, 2])) + valid_count = np.count_nonzero(np.isfinite(ATM_input[:, 2])) + ATM_RMS += np.sqrt(np.sum(ATM_input[ivalid, 2] ** 2) / valid_count) # # divide by the number of reanalyses # OBP_RMS /= 2.0 # ATM_RMS /= 4.0 @@ -652,130 +1046,239 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= # number of rheologies to iterate nRheology = len(gia_files[g]) # iterate through solutions - mon = np.zeros((nmon),dtype=np.int64) - tdec = np.zeros((nmon),dtype=np.float64) - mass = np.zeros((nmon,nRheology),dtype=np.float64) - satellite_error = np.zeros((nmon),dtype=np.float64) - for i,F in enumerate(VERSION): + mon = np.zeros((nmon), dtype=np.int64) + tdec = np.zeros((nmon), dtype=np.float64) + mass = np.zeros((nmon, nRheology), dtype=np.float64) + satellite_error = np.zeros((nmon), dtype=np.float64) + for i, F in enumerate(VERSION): # subdirectory - subdir = sd.format(PROC,DREL,F,SLF,LMAX,START_MON,END_MON) + subdir = sd.format(PROC, DREL, F, SLF, LMAX, START_MON, END_MON) # read each GIA model - for k,GIA_FILE in enumerate(gia_files[g]): - gia_Ylms = gravtk.read_GIA_model(base_dir.joinpath(*GIA_FILE), GIA=g) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_Ylms = gravtk.read_GIA_model( + base_dir.joinpath(*GIA_FILE), GIA=g + ) gia_str = gia_Ylms['title'] - input_file = ff.format(gia_str,reg,'',atm_str,ocean_str,LMAX,gw_str,ds_str) - dinput = np.loadtxt(mascon_dir.joinpath(subdir,input_file)) - mon[:] = dinput[:nmon,0].astype(np.int64) - tdec[:] = dinput[:nmon,1] - mass[:,k] = dinput[:nmon,2] - satellite_error[:] += dinput[:nmon,3]**2 + input_file = ff.format( + gia_str, reg, '', atm_str, ocean_str, LMAX, gw_str, ds_str + ) + dinput = np.loadtxt(mascon_dir.joinpath(subdir, input_file)) + mon[:] = dinput[:nmon, 0].astype(np.int64) + tdec[:] = dinput[:nmon, 1] + mass[:, k] = dinput[:nmon, 2] + satellite_error[:] += dinput[:nmon, 3] ** 2 # calculate mean GIA-corrected mass change (for all Earth rheologies) gia_corrected_mean = np.mean(mass, axis=1) # GRACE satellite error component, ocean leakage (ECCO-GAD), # atmosphere leakage (ATM-GAA) and GLDAS TWC - grace_error = np.sqrt(np.sum(satellite_error/nRheology + SLF_RMS**2 + - OBP_RMS**2 + ATM_RMS**2 + TWC_RMS**2)/nmon) + grace_error = np.sqrt( + np.sum( + satellite_error / nRheology + + SLF_RMS**2 + + OBP_RMS**2 + + ATM_RMS**2 + + TWC_RMS**2 + ) + / nmon + ) # calculate variance off of mean for calculating GIA uncertainty gia_corrected_variance = np.zeros((nmon)) gia_corrected_minmax = np.zeros((nmon)) # calculate GIA uncertainty as "worst-case" not RMS - for k,GIA_FILE in enumerate(gia_files[g]): - gia_corrected_variance+=np.abs(mass[:,k]-gia_corrected_mean) + for k, GIA_FILE in enumerate(gia_files[g]): + gia_corrected_variance += np.abs( + mass[:, k] - gia_corrected_mean + ) # calculate GIA uncertainty as "worst-case" min max error for t in range(nmon): - gia_corrected_minmax[t]=np.abs(np.max(mass[t,:])-np.min(mass[t,:])) + gia_corrected_minmax[t] = np.abs( + np.max(mass[t, :]) - np.min(mass[t, :]) + ) # calculate uncertainty in mean GIA - gia_corrected_error = gia_corrected_variance/(np.float64(nRheology)-1.0) - gia_corrected_error = gia_corrected_error*np.sign(tdec-tdec.mean()) - gia_corrected_minmax = gia_corrected_minmax*np.sign(tdec-tdec.mean()) - gia_error_rate = (gia_corrected_error[-1]-gia_corrected_error[0])/(tdec[-1]-tdec[0]) - gia_minmax_rate = (gia_corrected_minmax[-1]-gia_corrected_minmax[0])/(tdec[-1]-tdec[0]) + gia_corrected_error = gia_corrected_variance / ( + np.float64(nRheology) - 1.0 + ) + gia_corrected_error = gia_corrected_error * np.sign( + tdec - tdec.mean() + ) + gia_corrected_minmax = gia_corrected_minmax * np.sign( + tdec - tdec.mean() + ) + gia_error_rate = ( + gia_corrected_error[-1] - gia_corrected_error[0] + ) / (tdec[-1] - tdec[0]) + gia_minmax_rate = ( + gia_corrected_minmax[-1] - gia_corrected_minmax[0] + ) / (tdec[-1] - tdec[0]) # calculate GIA errors at confidence interval # t.ppf parallels tinv in matlab - tstar = scipy.stats.t.ppf(1.0-(alpha/2.0),nRheology-1.0) if g in ('IJ05-R2','SM09') else 2.0 - gia_corrected_conf = tstar*np.abs(gia_error_rate) + tstar = ( + scipy.stats.t.ppf(1.0 - (alpha / 2.0), nRheology - 1.0) + if g in ('IJ05-R2', 'SM09') + else 2.0 + ) + gia_corrected_conf = tstar * np.abs(gia_error_rate) # add to plot with colors and label plot_label = plot_title[i] # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - mnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): + tnan = np.full_like(month, np.nan, dtype=np.float64) + mnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): valid = np.count_nonzero(mon == m) if valid: - mm, = np.nonzero(mon == m) + (mm,) = np.nonzero(mon == m) tnan[d] = tdec[mm] - mnan[d] = gia_corrected_mean[mm] - np.mean(gia_corrected_mean) + mnan[d] = gia_corrected_mean[mm] - np.mean( + gia_corrected_mean + ) # plot all dates - ax.plot(tnan, mnan, color=plot_colors[i], label=plot_label, zorder=3+i) + ax.plot( + tnan, mnan, color=plot_colors[i], label=plot_label, zorder=3 + i + ) # fill between monthly errors - ax.fill_between(tnan, mnan-grace_error, y2=mnan+grace_error, - color=plot_colors[i], alpha=0.35, zorder=1+i) + ax.fill_between( + tnan, + mnan - grace_error, + y2=mnan + grace_error, + color=plot_colors[i], + alpha=0.35, + zorder=1 + i, + ) # plot RACMO SMB time series for comparison - if ('RACMO' in SMB): + if 'RACMO' in SMB: # read RACMO surface mass balance file - subdir = sd.format('RACMO2.3p2','ANT27_SMB_cumul','','',LMAX,RACMO_START,RACMO_END) - RACMO_file = ff.format('RACMO2.3p2','ANT27_SMB_cumul_',reg,'RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - RACMO = np.loadtxt(base_dir.joinpath('RACMO','XANT27_1979-2022',subdir,RACMO_file))[:nmon,:] + subdir = sd.format( + 'RACMO2.3p2', + 'ANT27_SMB_cumul', + '', + '', + LMAX, + RACMO_START, + RACMO_END, + ) + RACMO_file = ff.format( + 'RACMO2.3p2', + 'ANT27_SMB_cumul_', + reg, + 'RAD1.5_', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + RACMO = np.loadtxt( + base_dir.joinpath( + 'RACMO', 'XANT27_1979-2022', subdir, RACMO_file + ) + )[:nmon, :] # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - mnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): - valid = np.count_nonzero(RACMO[:,0] == m) + tnan = np.full_like(month, np.nan, dtype=np.float64) + mnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): + valid = np.count_nonzero(RACMO[:, 0] == m) if valid: - mm, = np.nonzero(RACMO[:,0] == m) - tnan[d] = RACMO[mm,1] - mnan[d] = RACMO[mm,2] - np.mean(RACMO[:,2]) + (mm,) = np.nonzero(RACMO[:, 0] == m) + tnan[d] = RACMO[mm, 1] + mnan[d] = RACMO[mm, 2] - np.mean(RACMO[:, 2]) # plot all dates - ax.plot(tnan, mnan, color=plot_colors[2], label='RACMO2.3p2', zorder=5) + ax.plot( + tnan, mnan, color=plot_colors[2], label='RACMO2.3p2', zorder=5 + ) # plot GSFC-fdm SMB time series for comparison - if ('GSFC-fdm' in SMB): + if 'GSFC-fdm' in SMB: # read MERRA-2 hybrid surface mass balance file - subdir = 'HEX_GSFC_FDM_{0}_{1}_{2}_L{3:d}'.format('v1_2_1','ais','SMB_a',LMAX) - MERRA2_file = ff.format('GSFC_FDM','v1_2_1_SMB_a_',reg,'RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - MERRA2 = np.loadtxt(base_dir.joinpath('MERRA2_hybrid','v1.2.1',subdir,MERRA2_file)) + subdir = 'HEX_GSFC_FDM_{0}_{1}_{2}_L{3:d}'.format( + 'v1_2_1', 'ais', 'SMB_a', LMAX + ) + MERRA2_file = ff.format( + 'GSFC_FDM', + 'v1_2_1_SMB_a_', + reg, + 'RAD1.5_', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + MERRA2 = np.loadtxt( + base_dir.joinpath( + 'MERRA2_hybrid', 'v1.2.1', subdir, MERRA2_file + ) + ) # plot all dates - ax.plot(MERRA2[:,1], MERRA2[:,2] - np.mean(MERRA2[:,2]), - color=plot_colors[3], label='GSFC-fdm v1.2.1', zorder=5) + ax.plot( + MERRA2[:, 1], + MERRA2[:, 2] - np.mean(MERRA2[:, 2]), + color=plot_colors[3], + label='GSFC-fdm v1.2.1', + zorder=5, + ) # plot JPL-GEMB SMB time series for comparison - if ('GEMB' in SMB): + if 'GEMB' in SMB: # read GEMB surface mass balance file - subdir = sd.format('GEMB','v1_2_Antarctica_SMB_cumul','','',LMAX,GEMB_START,GEMB_END) - GEMB_file = ff.format('GEMB','v1_2_SMB_cumul_',reg,'RAD1.5_',ocean_str,LMAX,gw_str,ds_str) - GEMB = np.loadtxt(base_dir.joinpath('GEMB','v1.2',subdir,GEMB_file)) + subdir = sd.format( + 'GEMB', + 'v1_2_Antarctica_SMB_cumul', + '', + '', + LMAX, + GEMB_START, + GEMB_END, + ) + GEMB_file = ff.format( + 'GEMB', + 'v1_2_SMB_cumul_', + reg, + 'RAD1.5_', + ocean_str, + LMAX, + gw_str, + ds_str, + ) + GEMB = np.loadtxt( + base_dir.joinpath('GEMB', 'v1.2', subdir, GEMB_file) + ) # create a time series with nans for missing months - tnan = np.full_like(month,np.nan,dtype=np.float64) - mnan = np.full_like(month,np.nan,dtype=np.float64) - for d,m in enumerate(month): - valid = np.count_nonzero(GEMB[:,0] == m) + tnan = np.full_like(month, np.nan, dtype=np.float64) + mnan = np.full_like(month, np.nan, dtype=np.float64) + for d, m in enumerate(month): + valid = np.count_nonzero(GEMB[:, 0] == m) if valid: - mm, = np.nonzero(GEMB[:,0] == m) - tnan[d] = GEMB[mm,1] - mnan[d] = GEMB[mm,2] - np.mean(GEMB[:,2]) + (mm,) = np.nonzero(GEMB[:, 0] == m) + tnan[d] = GEMB[mm, 1] + mnan[d] = GEMB[mm, 2] - np.mean(GEMB[:, 2]) # plot all dates - ax.plot(tnan, mnan, color=plot_colors[4], label='GEMB v1.2', zorder=5) + ax.plot( + tnan, mnan, color=plot_colors[4], label='GEMB v1.2', zorder=5 + ) # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9, - day=3,hour=12,minute=12) - ax.axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(12,6)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax.axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(12, 6)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(mon == 186) - kk, = np.flatnonzero(mon == 198) + (jj,) = np.flatnonzero(mon == 186) + (kk,) = np.flatnonzero(mon == 198) # ax.axvline(tdec[jj],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) # ax.axvline(tdec[kk],color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) - vs = ax.axvspan(tdec[jj],tdec[kk],color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (6,3) + vs = ax.axvspan( + tdec[jj], tdec[kk], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (6, 3) # add labels - textprops = dict(size=14,weight='bold') - at = matplotlib.offsetbox.AnchoredText(fig_text[reg], - prop=textprops, pad=0, frameon=False, loc=2) + textprops = dict(size=14, weight='bold') + at = matplotlib.offsetbox.AnchoredText( + fig_text[reg], prop=textprops, pad=0, frameon=False, loc=2 + ) ax.add_artist(at) - textprops = dict(size=14,weight='bold') - at = matplotlib.offsetbox.AnchoredText(reg_text[reg], - prop=textprops, pad=0, frameon=False, loc=1) + textprops = dict(size=14, weight='bold') + at = matplotlib.offsetbox.AnchoredText( + reg_text[reg], prop=textprops, pad=0, frameon=False, loc=1 + ) ax.add_artist(at) # set ticks major_ticks = np.arange(2002, 2024, 4) @@ -793,72 +1296,139 @@ def plot_mascon_SLF_timeseries(base_dir,PROC,DREL,START_MON,END_MON,MISSING,SMB= ax.set_ylabel('Mass [Gt]', labelpad=ypad[reg]) # add legend - lgd = ax1['EAIS'].legend(loc=3,frameon=False) + lgd = ax1['EAIS'].legend(loc=3, frameon=False) # lgd = ax1['EAIS'].legend(loc=3,frameon=False,handletextpad=-0.2,handlelength=0) # set width, color and style of lines # lgd.get_frame().set_boxstyle('square,pad=0.1') # lgd.get_frame().set_edgecolor('black') lgd.get_frame().set_alpha(1.0) for line in lgd.get_lines(): - line.set_linewidth(6) + line.set_linewidth(6) # line.set_linewidth(0) - for i,text in enumerate(lgd.get_texts()): + for i, text in enumerate(lgd.get_texts()): text.set_color(plot_colors[i]) text.set_weight('bold') # adjust plot to figure dimensions - fig.subplots_adjust(left=0.08,right=0.99,bottom=0.1,top=0.99,wspace=0.2,hspace=0.125) - figurefile = filepath.joinpath('fig4ab_{0}_{1}.pdf'.format(PROC,DREL)) + fig.subplots_adjust( + left=0.08, right=0.99, bottom=0.1, top=0.99, wspace=0.2, hspace=0.125 + ) + figurefile = filepath.joinpath('fig4ab_{0}_{1}.pdf'.format(PROC, DREL)) plt.savefig(figurefile, format='pdf') plt.cla() plt.clf() plt.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser() # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, default=None, - choices=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + default=None, + choices=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series') - parser.add_argument('--end','-E', - type=int, default=230, - help='Ending GRACE/GRACE-FO month for time series') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167,172, - 177,178,182,200,201] - parser.add_argument('--missing','-M', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months in time series') - parser.add_argument('--smb','-s', - type=str, default=[], choices=('RACMO','GSFC-fdm','GEMB'), nargs='+', - help='Plot Surface Mass Balance (SMB) time series') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=230, + help='Ending GRACE/GRACE-FO month for time series', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-M', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months in time series', + ) + parser.add_argument( + '--smb', + '-s', + type=str, + default=[], + choices=('RACMO', 'GSFC-fdm', 'GEMB'), + nargs='+', + help='Plot Surface Mass Balance (SMB) time series', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run program for parameters - plot_mascon_SLF_timeseries(args.directory,args.center,args.release, - args.start,args.end,args.missing,SMB=args.smb) + plot_mascon_SLF_timeseries( + args.directory, + args.center, + args.release, + args.start, + args.end, + args.missing, + SMB=args.smb, + ) + # run main program if __name__ == '__main__': diff --git a/scripts/regional_spherical_caps.py b/scripts/regional_spherical_caps.py index 6789e992..20cb21d4 100644 --- a/scripts/regional_spherical_caps.py +++ b/scripts/regional_spherical_caps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" regional_spherical_caps.py Written by Tyler Sutterley (05/2023) @@ -88,6 +88,7 @@ with traceback error handling Written 10/2013 """ + from __future__ import print_function import sys @@ -100,6 +101,7 @@ import traceback import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -109,8 +111,10 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate spherical caps from a coordinate index -def regional_spherical_caps(LMAX, +def regional_spherical_caps( + LMAX, MASCON_FILE=None, COORDINATE_FILE=None, HEADER=0, @@ -120,8 +124,8 @@ def regional_spherical_caps(LMAX, DATAFORM=None, OUTPUT_DIRECTORY=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # check if coordinate file exists COORDINATE_FILE = pathlib.Path(COORDINATE_FILE).expanduser().absolute() if not COORDINATE_FILE.exists(): @@ -144,9 +148,9 @@ def regional_spherical_caps(LMAX, # column 1: cap number # column 2: longitude of center point # column 3: latitude of center point - num = coord[:,0].astype(int) - lon = coord[:,1] - lat = coord[:,2] + num = coord[:, 0].astype(int) + lon = coord[:, 1] + lat = coord[:, 2] ncap = len(num) # radius of each spherical cap if RAD_CAP: @@ -154,19 +158,20 @@ def regional_spherical_caps(LMAX, RAD_CAP = np.zeros((ncap)) + RAD_CAP else: # column 4: radius of spherical cap - RAD_CAP = coord[:,3] + RAD_CAP = coord[:, 3] # equivalent water thickness of each spherical cap dinput = 1.0 # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # harmonic units dfactor = gravtk.units(lmax=LMAX).harmonic(*LOVE) # Earth Parameters - rho_e = dfactor.rho_e# Average Density of the Earth [g/cm^3] - rad_e = dfactor.rad_e# Average Radius of the Earth [cm] + rho_e = dfactor.rho_e # Average Density of the Earth [g/cm^3] + rad_e = dfactor.rad_e # Average Radius of the Earth [cm] # colatitude of the spherical cap centers th = np.radians(90.0 - lat) @@ -184,18 +189,33 @@ def regional_spherical_caps(LMAX, # calculate the spherical harmonic coefficients # write spherical harmonics to file # print file path of spherical harmonics to index file - for i,cap_number in enumerate(num): + for i, cap_number in enumerate(num): # plms for computing spherical cap at latitude i - plm_i = np.squeeze(PLM[:,:,i]) + plm_i = np.squeeze(PLM[:, :, i]) # Calculate spherical harmonic coefficients - Ylms = gravtk.gen_spherical_cap(dinput, lon[i], lat[i], LMAX=LMAX, - RAD_CAP=RAD_CAP[i], UNITS=1, PLM=plm_i, LOVE=LOVE) + Ylms = gravtk.gen_spherical_cap( + dinput, + lon[i], + lat[i], + LMAX=LMAX, + RAD_CAP=RAD_CAP[i], + UNITS=1, + PLM=plm_i, + LOVE=LOVE, + ) # calculate equivalent area after harmonic conversion (1 cmwe) # area = (volume*density)/(mass/area) - ar = 4.0*np.pi*(rad_e**3.0)*rho_e*np.squeeze(Ylms.clm[0,0])/3.0 + ar = ( + 4.0 + * np.pi + * (rad_e**3.0) + * rho_e + * np.squeeze(Ylms.clm[0, 0]) + / 3.0 + ) # calculate equivalent radius (should equal RAD_CAP) - rad = np.degrees(np.sqrt(ar/np.pi) / rad_e) + rad = np.degrees(np.sqrt(ar / np.pi) / rad_e) # if verbose output: sanity check of radii args = (cap_number, RAD_CAP[i], rad) logging.info('{0:4d} {1:10.4f} {2:10.4f}'.format(*args)) @@ -217,17 +237,17 @@ def regional_spherical_caps(LMAX, attributes['title'] = str(RAD_CAP[i]) attributes['reference'] = COORDINATE_FILE.name # output spherical harmonic file to file format - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) Ylms.to_ascii(OUTPUT_FILE, date=False, verbose=VERBOSE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netcdf (.nc) - Ylms.to_netCDF4(OUTPUT_FILE, date=False, verbose=VERBOSE, - **attributes) - elif (DATAFORM == 'HDF5'): + Ylms.to_netCDF4( + OUTPUT_FILE, date=False, verbose=VERBOSE, **attributes + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) - Ylms.to_HDF5(OUTPUT_FILE, date=False, verbose=VERBOSE, - **attributes) + Ylms.to_HDF5(OUTPUT_FILE, date=False, verbose=VERBOSE, **attributes) # change the permissions mode of the output file OUTPUT_FILE.chmod(mode=MODE) @@ -239,11 +259,12 @@ def regional_spherical_caps(LMAX, # return list of output files return output_files + # PURPOSE: print a file log for the regional spherical cap calculation def output_log_file(input_arguments, output_files): # format: regional_spherical_cap_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) - LOGFILE='regional_spherical_cap_run_{0}_PID-{1:d}.log'.format(*args) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) + LOGFILE = 'regional_spherical_cap_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) fid = gravtk.utilities.create_unique_file(DIRECTORY.joinpath(LOGFILE)) @@ -259,11 +280,14 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the regional spherical cap calculation def output_error_log_file(input_arguments): # format: regional_spherical_cap_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) - LOGFILE='regional_spherical_cap_failed_run_{0}_PID-{1:d}.log'.format(*args) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) + LOGFILE = 'regional_spherical_cap_failed_run_{0}_PID-{1:d}.log'.format( + *args + ) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) fid = gravtk.utilities.create_unique_file(DIRECTORY.joinpath(LOGFILE)) @@ -278,81 +302,123 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Computes and outputs spherical harmonics for a set of spherical caps """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) # mascon index file and parameters - parser.add_argument('--mascon-file', + parser.add_argument( + '--mascon-file', type=pathlib.Path, required=True, - help='Index file of mascons spherical harmonics') - parser.add_argument('--coordinate-file', + help='Index file of mascons spherical harmonics', + ) + parser.add_argument( + '--coordinate-file', type=pathlib.Path, required=True, - help='File with spatial coordinates of mascon centers') + help='File with spatial coordinates of mascon centers', + ) # number of header lines to skip in coordinate file - parser.add_argument('--header','-H', - type=int, default=0, - help='Number of header lines to skip in coordinate file') + parser.add_argument( + '--header', + '-H', + type=int, + default=0, + help='Number of header lines to skip in coordinate file', + ) # spherical cap radius (if using a uniform set of caps) - parser.add_argument('--cap-radius', - type=float, - help='Spherical cap radius (degrees)') + parser.add_argument( + '--cap-radius', type=float, help='Spherical cap radius (degrees)' + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # Output log file for each job in forms # regional_spherical_cap_run_2002-04-01_PID-00000.log # regional_spherical_cap_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -362,7 +428,8 @@ def main(): try: info(args) # run regional_spherical_caps algorithm with parameters - output_files = regional_spherical_caps(args.lmax, + output_files = regional_spherical_caps( + args.lmax, MASCON_FILE=args.mascon_file, COORDINATE_FILE=args.coordinate_file, HEADER=args.header, @@ -372,18 +439,20 @@ def main(): DATAFORM=args.format, OUTPUT_DIRECTORY=args.output_directory, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/regress_grace_maps.py b/scripts/regress_grace_maps.py index ba4d0a2a..842304ed 100755 --- a/scripts/regress_grace_maps.py +++ b/scripts/regress_grace_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" regress_grace_maps.py Written by Tyler Sutterley (07/2026) @@ -96,6 +96,7 @@ Updated 06/2015: added output_files for log files Written 09/2013 """ + from __future__ import print_function, division import sys @@ -108,6 +109,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -117,8 +119,11 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # program module to run with specified parameters -def regress_grace_maps(LMAX, RAD, +def regress_grace_maps( + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -135,8 +140,8 @@ def regress_grace_maps(LMAX, RAD, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # create output directory if currently non-existent OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -167,42 +172,51 @@ def regress_grace_maps(LMAX, RAD, output_format = '{0}{1}_L{2:d}{3}{4}{5}_{6}{7}_{8:03d}-{9:03d}.{10}' # GRACE months to read - months = sorted(set(np.arange(START,END+1)) - set(MISSING)) + months = sorted(set(np.arange(START, END + 1)) - set(MISSING)) # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output Degree Interval - if (INTERVAL == 1): + if INTERVAL == 1: # (-180:180,90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2): + 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) - elif (INTERVAL == 3): + nlon = np.int64(360.0 / dlon) + nlat = np.int64(180.0 / dlat) + elif INTERVAL == 3: # non-global grid set with BOUNDS parameter - minlon,maxlon,minlat,maxlat = BOUNDS.copy() - lon = np.arange(minlon+dlon/2.0, maxlon+dlon/2.0, dlon) - lat = np.arange(maxlat-dlat/2.0, minlat-dlat/2.0, -dlat) + minlon, maxlon, minlat, maxlat = BOUNDS.copy() + lon = np.arange(minlon + dlon / 2.0, maxlon + dlon / 2.0, dlon) + lat = np.arange(maxlat - dlat / 2.0, minlat - dlat / 2.0, -dlat) nlon = len(lon) nlat = len(lat) # input data spatial object spatial_list = [] - for t,grace_month in enumerate(months): + for t, grace_month in enumerate(months): # input GRACE/GRACE-FO spatial file - fargs = (FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, grace_month, suffix) + fargs = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + grace_month, + suffix, + ) input_file = OUTPUT_DIRECTORY.joinpath(input_format.format(*fargs)) # read GRACE/GRACE-FO spatial file - if (DATAFORM == 'ascii'): - dinput = gravtk.spatial().from_ascii(input_file, - spacing=[dlon,dlat], nlon=nlon, nlat=nlat) - elif (DATAFORM == 'netCDF4'): + if DATAFORM == 'ascii': + dinput = gravtk.spatial().from_ascii( + input_file, spacing=[dlon, dlat], nlon=nlon, nlat=nlat + ) + elif DATAFORM == 'netCDF4': # netcdf (.nc) dinput = gravtk.spatial().from_netCDF4(input_file) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 (.H5) dinput = gravtk.spatial().from_HDF5(input_file) # append to spatial list @@ -215,14 +229,16 @@ def regress_grace_maps(LMAX, RAD, spatial_list = None # Setting output parameters for each fit type - coef_str = ['x{0:d}'.format(o) for o in range(ORDER+1)] - unit_suffix = [' yr^{0:d}'.format(-o) if o else '' for o in range(ORDER+1)] - if (ORDER == 0):# Mean + coef_str = ['x{0:d}'.format(o) for o in range(ORDER + 1)] + unit_suffix = [ + ' yr^{0:d}'.format(-o) if o else '' for o in range(ORDER + 1) + ] + if ORDER == 0: # Mean fit_longname = ['Mean'] - elif (ORDER == 1):# Trend - fit_longname = ['Constant','Trend'] - elif (ORDER == 2):# Quadratic - fit_longname = ['Constant','Linear','Quadratic'] + elif ORDER == 1: # Trend + fit_longname = ['Constant', 'Trend'] + elif ORDER == 2: # Quadratic + fit_longname = ['Constant', 'Linear', 'Quadratic'] # amplitude string for cyclical components amp_str = [] @@ -243,28 +259,29 @@ def regress_grace_maps(LMAX, RAD, # extra terms for tidal aliasing components or custom fits TERMS = [] term_index = [] - for i,c in enumerate(CYCLES): + for i, c in enumerate(CYCLES): # check if fitting with semi-annual or annual terms - if (c == 0.5): - coef_str.extend(['SS','SC']) + if c == 0.5: + coef_str.extend(['SS', 'SC']) amp_str.append('SEMI') amp_title['SEMI'] = 'Semi-Annual Amplitude' ph_title['SEMI'] = 'Semi-Annual Phase' fit_longname.extend(['Semi-Annual Sine', 'Semi-Annual Cosine']) - unit_suffix.extend(['','']) - elif (c == 1.0): - coef_str.extend(['AS','AC']) + unit_suffix.extend(['', '']) + elif c == 1.0: + coef_str.extend(['AS', 'AC']) amp_str.append('ANN') amp_title['ANN'] = 'Annual Amplitude' ph_title['ANN'] = 'Annual Phase' fit_longname.extend(['Annual Sine', 'Annual Cosine']) - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # check if fitting with tidal aliasing terms - for t,period in tidal_aliasing.items(): - if np.isclose(c, (period/365.25)): + for t, period in tidal_aliasing.items(): + if np.isclose(c, (period / 365.25)): # terms for tidal aliasing during GRACE and GRACE-FO periods - TERMS.extend(gravtk.time_series.aliasing_terms(grid.time, - period=period)) + TERMS.extend( + gravtk.time_series.aliasing_terms(grid.time, period=period) + ) # labels for tidal aliasing during GRACE period coef_str.extend([f'{t}SGRC', f'{t}CGRC']) amp_str.append(f'{t}GRC') @@ -272,7 +289,7 @@ def regress_grace_maps(LMAX, RAD, ph_title[f'{t}GRC'] = f'{t} Tidal Alias (GRACE) Phase' fit_longname.append(f'{t} Tidal Alias (GRACE) Sine') fit_longname.append(f'{t} Tidal Alias (GRACE) Cosine') - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # labels for tidal aliasing during GRACE-FO period coef_str.extend([f'{t}SGFO', f'{t}CGFO']) amp_str.append(f'{t}GFO') @@ -280,7 +297,7 @@ def regress_grace_maps(LMAX, RAD, ph_title[f'{t}GFO'] = f'{t} Tidal Alias (GRACE-FO) Phase' fit_longname.append(f'{t} Tidal Alias (GRACE-FO) Sine') fit_longname.append(f'{t} Tidal Alias (GRACE-FO) Cosine') - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # index to remove the original tidal aliasing term term_index.append(i) # remove the original tidal aliasing terms @@ -288,7 +305,7 @@ def regress_grace_maps(LMAX, RAD, # Fitting seasonal components ncomp = len(coef_str) - ncycles = 2*len(CYCLES) + len(TERMS) + ncycles = 2 * len(CYCLES) + len(TERMS) # confidence interval for regression fit errors CONF = 0.95 @@ -296,43 +313,60 @@ def regress_grace_maps(LMAX, RAD, out = dinput.zeros_like() out.data = np.zeros((nlat, nlon, ncomp)) out.error = np.zeros((nlat, nlon, ncomp)) - out.mask = np.ones((nlat, nlon, ncomp),dtype=bool) + out.mask = np.ones((nlat, nlon, ncomp), dtype=bool) # Fit Significance FS = {} # SSE: Sum of Squares Error # AIC: Akaike information criterion # BIC: Bayesian information criterion # R2Adj: Adjusted Coefficient of Determination - for key in ['SSE','AIC','BIC','R2Adj']: + for key in ['SSE', 'AIC', 'BIC', 'R2Adj']: FS[key] = dinput.zeros_like() # calculate the regression coefficients and fit significance for i in range(nlat): for j in range(nlon): # Calculating the regression coefficients - tsbeta = gravtk.time_series.regress(grid.time, grid.data[i,j,:], - ORDER=ORDER, CYCLES=CYCLES, TERMS=TERMS, CONF=CONF) + tsbeta = gravtk.time_series.regress( + grid.time, + grid.data[i, j, :], + ORDER=ORDER, + CYCLES=CYCLES, + TERMS=TERMS, + CONF=CONF, + ) # save regression components for k in range(0, ncomp): - out.data[i,j,k] = tsbeta['beta'][k] - out.error[i,j,k] = tsbeta['error'][k] - out.mask[i,j,k] = False + out.data[i, j, k] = tsbeta['beta'][k] + out.error[i, j, k] = tsbeta['error'][k] + out.mask[i, j, k] = False # Fit significance terms # Degrees of Freedom nu = tsbeta['DOF'] # Converting Mean Square Error to Sum of Squares Error - FS['SSE'].data[i,j] = tsbeta['MSE']*nu - FS['AIC'].data[i,j] = tsbeta['AIC'] - FS['BIC'].data[i,j] = tsbeta['BIC'] - FS['R2Adj'].data[i,j] = tsbeta['R2Adj'] + FS['SSE'].data[i, j] = tsbeta['MSE'] * nu + FS['AIC'].data[i, j] = tsbeta['AIC'] + FS['BIC'].data[i, j] = tsbeta['BIC'] + FS['R2Adj'].data[i, j] = tsbeta['R2Adj'] # list of output files output_files = [] # Output spatial files - for i in range(0,ncomp): + for i in range(0, ncomp): # output spatial file name - f1 = (FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, coef_str[i], '', START, END, suffix) + f1 = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + coef_str[i], + '', + START, + END, + suffix, + ) file1 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f1)) # full attributes UNITS_TITLE = f'{units_name}{unit_suffix[i]}' @@ -340,40 +374,70 @@ def regress_grace_maps(LMAX, RAD, FILE_TITLE = f'GRACE/GRACE-FO_Spatial_Data_{fit_longname[i]}' # output regression fit to file output = out.index(i, date=False) - output_data(output, FILENAME=file1, DATAFORM=DATAFORM, - UNITS=UNITS_TITLE, LONGNAME=LONGNAME, TITLE=FILE_TITLE, - CONF=CONF, VERBOSE=VERBOSE, MODE=MODE) + output_data( + output, + FILENAME=file1, + DATAFORM=DATAFORM, + UNITS=UNITS_TITLE, + LONGNAME=LONGNAME, + TITLE=FILE_TITLE, + CONF=CONF, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add output files to list object output_files.append(file1) # if fitting coefficients with cyclical components # output amplitude and phase of cyclical components - for i,flag in enumerate(amp_str): + for i, flag in enumerate(amp_str): # Indice pointing to the cyclical components - j = 1 + ORDER + 2*i + j = 1 + ORDER + 2 * i # Allocating memory for output amplitude and phase amp = dinput.zeros_like() ph = dinput.zeros_like() # calculating amplitude and phase of spatial field - amp.data,ph.data = gravtk.time_series.amplitude( - out.data[:,:,j], out.data[:,:,j+1] + amp.data, ph.data = gravtk.time_series.amplitude( + out.data[:, :, j], out.data[:, :, j + 1] ) # convert phase from -180:180 to 0:360 ph.data = np.where(ph.data < 0, ph.data + 360.0, ph.data) # Amplitude Error - comp1 = out.error[:,:,j]*out.data[:,:,j]/amp.data - comp2 = out.error[:,:,j+1]*out.data[:,:,j+1]/amp.data + comp1 = out.error[:, :, j] * out.data[:, :, j] / amp.data + comp2 = out.error[:, :, j + 1] * out.data[:, :, j + 1] / amp.data amp.error = np.hypot(comp1, comp2) # Phase Error (degrees) - comp1 = out.error[:,:,j]*out.data[:,:,j+1]/(amp.data**2) - comp2 = out.error[:,:,j+1]*out.data[:,:,j]/(amp.data**2) + comp1 = out.error[:, :, j] * out.data[:, :, j + 1] / (amp.data**2) + comp2 = out.error[:, :, j + 1] * out.data[:, :, j] / (amp.data**2) ph.error = np.degrees(np.hypot(comp1, comp2)) # output file names for amplitude, phase and errors - f2 = (FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, flag, '_AMPL', START, END, suffix) - f3 = (FILE_PREFIX, units, LMAX, order_str, - gw_str, ds_str, flag,'_PHASE', START, END, suffix) + f2 = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + flag, + '_AMPL', + START, + END, + suffix, + ) + f3 = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + flag, + '_PHASE', + START, + END, + suffix, + ) file2 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f2)) file3 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f3)) # full attributes @@ -383,12 +447,28 @@ def regress_grace_maps(LMAX, RAD, AMP_TITLE = f'GRACE/GRACE-FO_Spatial_Data_{amp_title[flag]}' PH_TITLE = f'GRACE/GRACE-FO_Spatial_Data_{ph_title[flag]}' # Output seasonal amplitude and phase to files - output_data(amp, FILENAME=file2, DATAFORM=DATAFORM, - UNITS=AMP_UNITS, LONGNAME=LONGNAME, TITLE=AMP_TITLE, - CONF=CONF, VERBOSE=VERBOSE, MODE=MODE) - output_data(ph, FILENAME=file3, DATAFORM=DATAFORM, - UNITS=PH_UNITS, LONGNAME='Phase', TITLE=PH_TITLE, - CONF=CONF, VERBOSE=VERBOSE, MODE=MODE) + output_data( + amp, + FILENAME=file2, + DATAFORM=DATAFORM, + UNITS=AMP_UNITS, + LONGNAME=LONGNAME, + TITLE=AMP_TITLE, + CONF=CONF, + VERBOSE=VERBOSE, + MODE=MODE, + ) + output_data( + ph, + FILENAME=file3, + DATAFORM=DATAFORM, + UNITS=PH_UNITS, + LONGNAME='Phase', + TITLE=PH_TITLE, + CONF=CONF, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add output files to list object output_files.append(file2) output_files.append(file3) @@ -400,27 +480,55 @@ def regress_grace_maps(LMAX, RAD, signif_longname['BIC'] = 'Bayesian information criterion' signif_longname['R2Adj'] = 'Adjusted Coefficient of Determination' # for each fit significance term - for key,fs in FS.items(): + for key, fs in FS.items(): # output file names for fit significance signif_str = f'{key}_' - f4 = (FILE_PREFIX, units, LMAX, order_str, gw_str, ds_str, - signif_str, coef_str[ORDER], START, END, suffix) + f4 = ( + FILE_PREFIX, + units, + LMAX, + order_str, + gw_str, + ds_str, + signif_str, + coef_str[ORDER], + START, + END, + suffix, + ) file4 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f4)) # full attributes LONGNAME = signif_longname[key] # output fit significance to file - output_data(fs, FILENAME=file4, DATAFORM=DATAFORM, - UNITS=key, LONGNAME=LONGNAME, TITLE=nu, - VERBOSE=VERBOSE, MODE=MODE) + output_data( + fs, + FILENAME=file4, + DATAFORM=DATAFORM, + UNITS=key, + LONGNAME=LONGNAME, + TITLE=nu, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add output files to list object output_files.append(file4) # return the list of output files return output_files + # PURPOSE: wrapper function for outputting data to file -def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, - LONGNAME=None, TITLE=None, CONF=0, VERBOSE=0, MODE=0o775): +def output_data( + data, + FILENAME=None, + DATAFORM=None, + UNITS=None, + LONGNAME=None, + TITLE=None, + CONF=0, + VERBOSE=0, + MODE=0o775, +): # field mapping for output regression data field_mapping = {} field_mapping['lat'] = 'lat' @@ -445,30 +553,43 @@ def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, attributes['error']['description'] = 'Uncertainty_in_model_fit' attributes['error']['long_name'] = LONGNAME attributes['error']['units'] = UNITS - attributes['error']['confidence'] = 100*CONF + attributes['error']['confidence'] = 100 * CONF # output global attributes REFERENCE = f'Output from {pathlib.Path(sys.argv[0]).name}' # write to output file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) data.to_ascii(FILENAME, date=False, verbose=VERBOSE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netcdf (.nc) - data.to_netCDF4(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) - elif (DATAFORM == 'HDF5'): + data.to_netCDF4( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) - data.to_HDF5(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) + data.to_HDF5( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) # change the permissions mode of the output file FILENAME.chmod(mode=MODE) + # PURPOSE: print a file log for the GRACE/GRACE-FO regression def output_log_file(input_arguments, output_files): # format: GRACE_processing_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_processing_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -485,10 +606,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE/GRACE-FO regression def output_error_log_file(input_arguments): # format: GRACE_processing_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_processing_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -504,105 +626,219 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Reads in GRACE/GRACE-FO spatial files and calculates the trends at each grid point following an input regression model """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + help='Output directory for spatial files', + ) + parser.add_argument( + '--file-prefix', + '-P', type=str, - help='Prefix string for input and output files') + help='Prefix string for input and output files', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month for time series regression') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month for time series regression') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month for time series regression', + ) + parser.add_argument( + '--end', + '-E', + type=int, + default=232, + help='Ending GRACE/GRACE-FO month for time series regression', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # output units - parser.add_argument('--units','-U', - type=int, default=1, choices=[1,2,3,4,5], - help='Output units') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[1, 2, 3, 4, 5], + help='Output units', + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2,3], - help=('Output grid interval ' - '(1: global, 2: centered global, 3: non-global)')) - parser.add_argument('--bounds', - type=float, nargs=4, metavar=('lon_min','lon_max','lat_min','lat_max'), - help='Bounding box for non-global grid') + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2, 3], + help=( + 'Output grid interval ' + '(1: global, 2: centered global, 3: non-global)' + ), + ) + parser.add_argument( + '--bounds', + type=float, + nargs=4, + metavar=('lon_min', 'lon_max', 'lat_min', 'lat_max'), + help='Bounding box for non-global grid', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # regression parameters # 0: mean # 1: trend # 2: acceleration - parser.add_argument('--order', - type=int, default=2, - help='Regression fit polynomial order') + parser.add_argument( + '--order', type=int, default=2, help='Regression fit polynomial order' + ) # regression fit cyclical terms - parser.add_argument('--cycles', - type=float, default=[0.5,1.0,161.0/365.25], nargs='+', - help='Regression fit cyclical terms') + parser.add_argument( + '--cycles', + type=float, + default=[0.5, 1.0, 161.0 / 365.25], + nargs='+', + help='Regression fit cyclical terms', + ) # Output log file for each job in forms # GRACE_processing_run_2002-04-01_PID-00000.log # GRACE_processing_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -631,18 +867,20 @@ def main(): OUTPUT_DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/remove_grace_spatial.py b/scripts/remove_grace_spatial.py index 0524f86f..8cf3a2b6 100644 --- a/scripts/remove_grace_spatial.py +++ b/scripts/remove_grace_spatial.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" remove_grace_spatial.py Written by Tyler Sutterley (05/2023) Removes GRACE/GRACE-FO monthly spatial files after running programs @@ -40,6 +40,7 @@ Updated 10/2020: use argparse to set command line parameters Written 06/2020 """ + from __future__ import print_function import sys @@ -51,6 +52,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -60,8 +62,11 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: Remove GRACE/GRACE-FO spatial fields to free space -def remove_grace_spatial(LMAX, RAD, +def remove_grace_spatial( + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -71,13 +76,13 @@ def remove_grace_spatial(LMAX, RAD, DATAFORM=None, REDISTRIBUTE_REMOVED=False, OUTPUT_DIRECTORY=None, - FILE_PREFIX=None): - + FILE_PREFIX=None, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() # GRACE/GRACE-FO months - months = sorted(set(np.arange(START,END+1)) - set(MISSING)) + months = sorted(set(np.arange(START, END + 1)) - set(MISSING)) nmon = len(months) # output filename suffix @@ -92,85 +97,177 @@ def remove_grace_spatial(LMAX, RAD, # distributing removed mass uniformly over ocean ocean_str = '_OCN' if REDISTRIBUTE_REMOVED else '' # input spatial units - unit_list = ['cmwe', 'mmGH', 'mmCU', u'\u03BCGal', 'mbar'] + unit_list = ['cmwe', 'mmGH', 'mmCU', '\u03bcGal', 'mbar'] # input file format input_format = '{0}{1}_L{2:d}{3}{4}{5}_{6:03d}.{7}' - for t,grace_month in enumerate(months): + for t, grace_month in enumerate(months): # input GRACE/GRACE-FO spatial file - fi = input_format.format(FILE_PREFIX,unit_list[UNITS-1],LMAX, - order_str,gw_str,ds_str,grace_month,suffix) + fi = input_format.format( + FILE_PREFIX, + unit_list[UNITS - 1], + LMAX, + order_str, + gw_str, + ds_str, + grace_month, + suffix, + ) FILE = OUTPUT_DIRECTORY.joinpath(fi) # remove GRACE/GRACE-FO spatial file FILE.unlink() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Removes GRACE/GRACE-FO monthly spatial files after running programs """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + help='Output directory for spatial files', + ) + parser.add_argument( + '--file-prefix', + '-P', type=str, - help='Prefix string for input and output files') + help='Prefix string for input and output files', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # output units - parser.add_argument('--units','-U', - type=int, default=1, choices=[1,2,3,4,5], - help='Output units') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[1, 2, 3, 4, 5], + help='Output units', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -180,7 +277,9 @@ def main(): try: info(args) # run remove_grace_spatial algorithm with parameters - remove_grace_spatial(args.lmax, args.radius, + remove_grace_spatial( + args.lmax, + args.radius, START=args.start, END=args.end, MISSING=args.missing, @@ -190,7 +289,8 @@ def main(): DATAFORM=args.format, REDISTRIBUTE_REMOVED=args.redistribute_removed, OUTPUT_DIRECTORY=args.output_directory, - FILE_PREFIX=args.file_prefix) + FILE_PREFIX=args.file_prefix, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -198,6 +298,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/remove_mascon_reconstruct.py b/scripts/remove_mascon_reconstruct.py index d07d0b8d..1bb8e7af 100644 --- a/scripts/remove_mascon_reconstruct.py +++ b/scripts/remove_mascon_reconstruct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" remove_mascon_reconstruct.py Written by Tyler Sutterley (05/2023) Removes reconstructed mascon files from the index file after running program @@ -23,6 +23,7 @@ Updated 08/2020: flake8 compatible regular expression strings Written 12/2019 """ + from __future__ import print_function import sys @@ -34,6 +35,7 @@ import traceback import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -43,6 +45,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: Remove reconstructed spherical harmonic fields to free space def remove_mascon_reconstruct(RECONSTRUCT_FILE): # file parser for reading index files @@ -59,31 +62,39 @@ def remove_mascon_reconstruct(RECONSTRUCT_FILE): fi = pathlib.Path(fi).expanduser().absolute() fi.unlink() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Removes reconstructed mascon files from the index file after running program """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # mascon reconstruct parameters - parser.add_argument('--reconstruct-file', + parser.add_argument( + '--reconstruct-file', type=pathlib.Path, - help='Reconstructed mascon time series file') + help='Reconstructed mascon time series file', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -101,6 +112,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/remove_sea_level_errors.py b/scripts/remove_sea_level_errors.py index b1956240..7800ad6d 100644 --- a/scripts/remove_sea_level_errors.py +++ b/scripts/remove_sea_level_errors.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" remove_sea_level_errors.py Written by Tyler Sutterley (05/2023) Removes sea level load harmonics and spatial maps after running programs @@ -34,6 +34,7 @@ Updated 10/2020: use argparse to set command line parameters Written 07/2020 """ + from __future__ import print_function import sys @@ -46,6 +47,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -55,16 +57,20 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: Remove sea level spatial maps to free space -def remove_sea_level_errors(PROC, DREL, DSET, +def remove_sea_level_errors( + PROC, + DREL, + DSET, DATAFORM=None, MASCON_TYPE=None, REDISTRIBUTE_MASCONS=False, ITERATION=None, EXPANSION=None, RUNS=0, - OUTPUT_DIRECTORY=None): - + OUTPUT_DIRECTORY=None, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() # output filename suffix @@ -77,78 +83,127 @@ def remove_sea_level_errors(PROC, DREL, DSET, ocean_str = '_OCN' if REDISTRIBUTE_MASCONS else '' # output file format for input_distribution and output_slf - file_format='{0}_MC_ITERATION_{1}{2}{3}_L{4:d}_{5:05d}.{6}' + file_format = '{0}_MC_ITERATION_{1}{2}{3}_L{4:d}_{5:05d}.{6}' # for each monte carlo iteration for n in range(RUNS): # spherical harmonic and spatial fields from sea level programs - a1=(MASCON_TYPE,ITERATION,dset_str,ocean_str,EXPANSION,n,suffix) - a2=('SLF',ITERATION,dset_str,ocean_str,EXPANSION,n,suffix) + a1 = (MASCON_TYPE, ITERATION, dset_str, ocean_str, EXPANSION, n, suffix) + a2 = ('SLF', ITERATION, dset_str, ocean_str, EXPANSION, n, suffix) # remove sea level harmonics and spatial files FILE1 = OUTPUT_DIRECTORY.joinpath(file_format.format(*a1)) FILE2 = OUTPUT_DIRECTORY.joinpath(file_format.format(*a2)) FILE1.unlink() FILE2.unlink() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Removes sea level error harmonics and spatial maps after running programs """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # input load type (DISC, POINT or CAP) - parser.add_argument('--mascon-type','-T', - type=str.upper, default='CAP', choices=['DISC','POINT','CAP'], - help='Input load type') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--mascon-type', + '-T', + type=str.upper, + default='CAP', + choices=['DISC', 'POINT', 'CAP'], + help='Input load type', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # sea level fingerprint parameters - parser.add_argument('--iteration','-I', - type=int, default=1, - help='Sea level fingerprint iteration') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--iteration', + '-I', + type=int, + default=1, + help='Sea level fingerprint iteration', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # number of monte carlo iterations - parser.add_argument('--runs','-R', - type=int, default=10000, - help='Number of Monte Carlo iterations') + parser.add_argument( + '--runs', + '-R', + type=int, + default=10000, + help='Number of Monte Carlo iterations', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -158,14 +213,18 @@ def main(): try: info(args) # run remove_sea_level_errors algorithm with parameters - remove_sea_level_errors(args.center, args.release, args.product, + remove_sea_level_errors( + args.center, + args.release, + args.product, DATAFORM=args.format, MASCON_TYPE=args.mascon_type, REDISTRIBUTE_MASCONS=args.redistribute_mascons, ITERATION=args.iteration, EXPANSION=args.expansion, RUNS=args.runs, - OUTPUT_DIRECTORY=args.output_directory) + OUTPUT_DIRECTORY=args.output_directory, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -173,6 +232,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/remove_sea_level_fields.py b/scripts/remove_sea_level_fields.py index 3e2e8614..9ec070c7 100644 --- a/scripts/remove_sea_level_fields.py +++ b/scripts/remove_sea_level_fields.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" remove_sea_level_fields.py Written by Tyler Sutterley (05/2023) Removes sea level load harmonics and spatial maps after running programs @@ -50,6 +50,7 @@ Updated 10/2020: use argparse to set command line parameters Written 07/2020 """ + from __future__ import print_function import sys @@ -62,6 +63,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -71,8 +73,12 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: Remove sea level spatial maps to free space -def remove_sea_level_fields(PROC, DREL, DSET, +def remove_sea_level_fields( + PROC, + DREL, + DSET, START=None, END=None, MISSING=None, @@ -83,12 +89,12 @@ def remove_sea_level_fields(PROC, DREL, DSET, REDISTRIBUTE_MASCONS=False, ITERATION=None, EXPANSION=None, - OUTPUT_DIRECTORY=None): - + OUTPUT_DIRECTORY=None, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() # GRACE/GRACE-FO months - months = sorted(set(np.arange(START,END+1)) - set(MISSING)) + months = sorted(set(np.arange(START, END + 1)) - set(MISSING)) nmon = len(months) # output filename suffix suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')[DATAFORM] @@ -103,55 +109,135 @@ def remove_sea_level_fields(PROC, DREL, DSET, ocean_str = '_OCN' if REDISTRIBUTE_MASCONS else '' # output file format for input_distribution and output_slf - file_format='{0}_ITERATION_{1}{2}{3}{4}_L{5:d}_{6:03d}.{7}' - for t,gm in enumerate(months): + file_format = '{0}_ITERATION_{1}{2}{3}{4}_L{5:d}_{6:03d}.{7}' + for t, gm in enumerate(months): # spherical harmonic and spatial fields from sea level programs - a1=(MASCON_TYPE,ITERATION,dset_str,gia_str,ocean_str,EXPANSION,gm,suffix) - a2=('SLF',ITERATION,dset_str,gia_str,ocean_str,EXPANSION,gm,suffix) + a1 = ( + MASCON_TYPE, + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + gm, + suffix, + ) + a2 = ( + 'SLF', + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + gm, + suffix, + ) # remove sea level harmonics and spatial files FILE1 = OUTPUT_DIRECTORY.joinpath(file_format.format(*a1)) FILE2 = OUTPUT_DIRECTORY.joinpath(file_format.format(*a2)) FILE1.unlink() FILE2.unlink() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Removes sea level load harmonics and spatial maps after running programs """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -167,43 +253,74 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # input load type (DISC, POINT or CAP) - parser.add_argument('--mascon-type','-T', - type=str.upper, default='CAP', choices=['DISC','POINT','CAP'], - help='Input load type') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--mascon-type', + '-T', + type=str.upper, + default='CAP', + choices=['DISC', 'POINT', 'CAP'], + help='Input load type', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # sea level fingerprint parameters - parser.add_argument('--iteration','-I', - type=int, default=1, - help='Sea level fingerprint iteration') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--iteration', + '-I', + type=int, + default=1, + help='Sea level fingerprint iteration', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -213,7 +330,10 @@ def main(): try: info(args) # run remove_sea_level_fields algorithm with parameters - remove_sea_level_fields(args.center, args.release, args.product, + remove_sea_level_fields( + args.center, + args.release, + args.product, START=args.start, END=args.end, MISSING=args.missing, @@ -224,7 +344,8 @@ def main(): REDISTRIBUTE_MASCONS=args.redistribute_mascons, ITERATION=args.iteration, EXPANSION=args.expansion, - OUTPUT_DIRECTORY=args.output_directory) + OUTPUT_DIRECTORY=args.output_directory, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -232,6 +353,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/run_sea_level_equation.py b/scripts/run_sea_level_equation.py index fdeb7f46..ad2af07e 100644 --- a/scripts/run_sea_level_equation.py +++ b/scripts/run_sea_level_equation.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" run_sea_level_equation.py (06/2025) Solves the sea level equation with the option of including polar motion feedback Uses a Clenshaw summation to calculate the spherical harmonic summation @@ -117,6 +117,7 @@ Updated 04/2017: set the permissions mode of the output files with --mode Written 09/2016 """ + from __future__ import print_function import sys @@ -131,6 +132,7 @@ import collections import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -140,8 +142,11 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: Computes Sea Level Fingerprints including polar motion feedback -def run_sea_level_equation(INPUT_FILE, OUTPUT_FILE, +def run_sea_level_equation( + INPUT_FILE, + OUTPUT_FILE, LANDMASK=None, LMAX=0, LOVE_NUMBERS=0, @@ -155,8 +160,8 @@ def run_sea_level_equation(INPUT_FILE, OUTPUT_FILE, INPUT_TYPE=None, DATE=False, UNITS=None, - MODE=0o775): - + MODE=0o775, +): # set default paths INPUT_FILE = pathlib.Path(INPUT_FILE).expanduser().absolute() OUTPUT_FILE = pathlib.Path(OUTPUT_FILE).expanduser().absolute() @@ -172,36 +177,38 @@ def run_sea_level_equation(INPUT_FILE, OUTPUT_FILE, # Land-Sea Mask with Antarctica from Rignot (2017) and Greenland from GEUS # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, - varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape + nth, nphi = landsea.shape land_function = np.zeros((nth, nphi), dtype=np.float64) # calculate colatitude in radians th = np.radians(90.0 - landsea.lat) # extract land function from file # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + land_function[indx, indy] = 1.0 # read load love numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth model and love numbers attributes['earth_model'] = LOVE.model attributes['earth_love_numbers'] = LOVE.citation attributes['reference_frame'] = LOVE.reference # add attributes for body tide love numbers - if (BODY_TIDE_LOVE == 0): + if BODY_TIDE_LOVE == 0: attributes['earth_body_tide'] = 'Wahr (1981)' - elif (BODY_TIDE_LOVE == 1): + elif BODY_TIDE_LOVE == 1: attributes['earth_body_tide'] = 'Farrell (1972)' # add attributes for fluid love numbers - if (FLUID_LOVE == 0): + if FLUID_LOVE == 0: attributes['earth_fluid_love'] = 'Han and Wahr (1989)' - elif FLUID_LOVE in (1,2): + elif FLUID_LOVE in (1, 2): attributes['earth_fluid_love'] = 'Munk and MacDonald (1960)' - elif (FLUID_LOVE == 3): + elif FLUID_LOVE == 3: attributes['earth_fluid_love'] = 'Lambeck (1980)' # add attribute for true polar wander if POLAR: @@ -213,32 +220,38 @@ def run_sea_level_equation(INPUT_FILE, OUTPUT_FILE, if DATAFORM in single_file_formats and (INPUT_TYPE == 'spatial'): # read spatial data from input file format dataform = copy.copy(DATAFORM) - load_spatial = gravtk.spatial().from_file(INPUT_FILE, - format=DATAFORM, date=DATE) + load_spatial = gravtk.spatial().from_file( + INPUT_FILE, format=DATAFORM, date=DATE + ) attributes['lineage'] = load_spatial.filename.name elif DATAFORM in index_file_formats and (INPUT_TYPE == 'spatial'): # read spatial data from index file - _,dataform = DATAFORM.split('-') - load_spatial = gravtk.spatial().from_index(INPUT_FILE, - format=dataform, date=DATE) + _, dataform = DATAFORM.split('-') + load_spatial = gravtk.spatial().from_index( + INPUT_FILE, format=dataform, date=DATE + ) attributes['lineage'] = [f.name for f in load_spatial.filename] elif DATAFORM in single_file_formats: dataform = copy.copy(DATAFORM) # read spherical harmonic coefficients from input file format - load_Ylms = gravtk.harmonics().from_file(INPUT_FILE, - format=DATAFORM, date=DATE) + load_Ylms = gravtk.harmonics().from_file( + INPUT_FILE, format=DATAFORM, date=DATE + ) attributes['lineage'] = load_Ylms.filename.name elif DATAFORM in index_file_formats: # read spherical harmonic coefficients from index file - _,dataform = DATAFORM.split('-') - load_Ylms = gravtk.harmonics().from_index(INPUT_FILE, - format=dataform, date=DATE) + _, dataform = DATAFORM.split('-') + load_Ylms = gravtk.harmonics().from_index( + INPUT_FILE, format=dataform, date=DATE + ) attributes['lineage'] = [f.name for f in load_Ylms.filename] else: - raise ValueError(f'Unknown input data format {DATAFORM:s} for {INPUT_TYPE:s}') + raise ValueError( + f'Unknown input data format {DATAFORM:s} for {INPUT_TYPE:s}' + ) # convert input data to be iterable over time slices - if (INPUT_TYPE == 'spatial'): + if INPUT_TYPE == 'spatial': # expand dimensions to iterate over slices load_spatial.expand_dims() # number of time slices @@ -256,30 +269,47 @@ def run_sea_level_equation(INPUT_FILE, OUTPUT_FILE, # allocate for pseudo-spectral sea level equation solver sea_level = gravtk.spatial(nlon=nphi, nlat=nth) - sea_level.data = np.zeros((nth,nphi,nt)) - sea_level.mask = np.zeros((nth,nphi,nt), dtype=bool) + sea_level.data = np.zeros((nth, nphi, nt)) + sea_level.mask = np.zeros((nth, nphi, nt), dtype=bool) for i in range(nt): # print iteration if running a series - if (nt > 1): - logging.info(f'Index {i+1:d} of {nt:d}') + if nt > 1: + logging.info(f'Index {i + 1:d} of {nt:d}') # subset harmonics/spatial fields to indice - if (INPUT_TYPE == 'spatial'): + if INPUT_TYPE == 'spatial': spatial_data = load_spatial.index(i, date=DATE) # convert missing values to zero spatial_data.replace_invalid(0.0) # convert spatial field to spherical harmonics - Ylms = gravtk.gen_stokes(spatial_data.data.T, - spatial_data.lon, spatial_data.lat, UNITS=UNITS, - LMIN=0, LMAX=LMAX, LOVE=LOVE) + Ylms = gravtk.gen_stokes( + spatial_data.data.T, + spatial_data.lon, + spatial_data.lat, + UNITS=UNITS, + LMIN=0, + LMAX=LMAX, + LOVE=LOVE, + ) else: Ylms = load_Ylms.index(i, date=DATE) # run pseudo-spectral sea level equation solver - sea_level.data[:,:,i] = gravtk.sea_level_equation(Ylms.clm, Ylms.slm, - landsea.lon, landsea.lat, land_function.T, LMAX=LMAX, - LOVE=LOVE, BODY_TIDE_LOVE=BODY_TIDE_LOVE, - FLUID_LOVE=FLUID_LOVE, DENSITY=DENSITY, POLAR=POLAR, - PLM=PLM, ITERATIONS=ITERATIONS, FILL_VALUE=0).T - sea_level.mask[:,:,i] = (sea_level.data[:,:,i] == 0) + sea_level.data[:, :, i] = gravtk.sea_level_equation( + Ylms.clm, + Ylms.slm, + landsea.lon, + landsea.lat, + land_function.T, + LMAX=LMAX, + LOVE=LOVE, + BODY_TIDE_LOVE=BODY_TIDE_LOVE, + FLUID_LOVE=FLUID_LOVE, + DENSITY=DENSITY, + POLAR=POLAR, + PLM=PLM, + ITERATIONS=ITERATIONS, + FILL_VALUE=0, + ).T + sea_level.mask[:, :, i] = sea_level.data[:, :, i] == 0 # copy dimensions sea_level.lon = np.copy(landsea.lon) sea_level.lat = np.copy(landsea.lat) @@ -301,126 +331,195 @@ def run_sea_level_equation(INPUT_FILE, OUTPUT_FILE, kwargs['units'] = 'centimeters' kwargs['longname'] = 'Equivalent_Water_Thickness' # save as output DATAFORM - if (dataform == 'ascii'): + if dataform == 'ascii': # ascii (.txt) # only print ocean points sea_level.fill_value = 0 sea_level.update_mask() sea_level.to_ascii(OUTPUT_FILE, date=DATE) - elif (dataform == 'netCDF4'): + elif dataform == 'netCDF4': # netCDF4 (.nc) sea_level.to_netCDF4(OUTPUT_FILE, date=DATE, **kwargs) - elif (dataform == 'HDF5'): + elif dataform == 'HDF5': # HDF5 (.H5) sea_level.to_HDF5(OUTPUT_FILE, date=DATE, **kwargs) # set the permissions mode of the output file OUTPUT_FILE.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Solves the sea level equation with the option of including polar motion feedback """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # input and output file - parser.add_argument('infile', - type=pathlib.Path, nargs='?', - help='Input load file') - parser.add_argument('outfile', - type=pathlib.Path, nargs='?', - help='Output sea level fingerprints file') + parser.add_argument( + 'infile', type=pathlib.Path, nargs='?', help='Input load file' + ) + parser.add_argument( + 'outfile', + type=pathlib.Path, + nargs='?', + help='Output sea level fingerprints file', + ) # land mask file - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for calculating sea level fingerprints') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for calculating sea level fingerprints', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=240, - help='Maximum spherical harmonic degree') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=240, + help='Maximum spherical harmonic degree', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # different treatments of the body tide Love numbers of degree 2 # 0: Wahr (1981) and Wahr (1985) values from PREM # 1: Farrell (1972) values from Gutenberg-Bullen oceanic mantle model - parser.add_argument('--body','-b', - type=int, default=0, choices=[0,1], - help='Treatment of the body tide Love number') + parser.add_argument( + '--body', + '-b', + type=int, + default=0, + choices=[0, 1], + help='Treatment of the body tide Love number', + ) # density of water in g/cm^3 - parser.add_argument('--density','-d', - type=float, default=1.0, - help='Density of water in g/cm^3') + parser.add_argument( + '--density', + '-d', + type=float, + default=1.0, + help='Density of water in g/cm^3', + ) # different treatments of the fluid Love number of gravitational potential # 0: Han and Wahr (1989) fluid love number # 1: Munk and MacDonald (1960) secular love number # 2: Munk and MacDonald (1960) fluid love number # 3: Lambeck (1980) fluid love number - parser.add_argument('--fluid','-f', - type=int, default=0, choices=[0,1,2,3], - help='Treatment of the fluid Love number') + parser.add_argument( + '--fluid', + '-f', + type=int, + default=0, + choices=[0, 1, 2, 3], + help='Treatment of the fluid Love number', + ) # maximum number of iterations for the solver # 0th iteration: distribute the water in a uniform layer (barystatic) - parser.add_argument('--iterations','-I', - type=int, default=6, - help='Maximum number of iterations') + parser.add_argument( + '--iterations', + '-I', + type=int, + default=6, + help='Maximum number of iterations', + ) # option for polar feedback - parser.add_argument('--polar-feedback', - default=False, action='store_true', - help='Include effects of polar feedback') + parser.add_argument( + '--polar-feedback', + default=False, + action='store_true', + help='Include effects of polar feedback', + ) # option for setting reference frame for load love numbers # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # input and output data format (ascii, netCDF4, HDF5) choices = [] - choices.extend(['ascii','netCDF4','HDF5']) - choices.extend(['index-ascii','index-netCDF4','index-HDF5']) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=choices, - help='Input and output data format') - # define the input data type for the load files - parser.add_argument('--input-type','-T', - type=str, default='harmonics', choices=['harmonics','spatial'], - help='Input data type for load fields') + choices.extend(['ascii', 'netCDF4', 'HDF5']) + choices.extend(['index-ascii', 'index-netCDF4', 'index-HDF5']) + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=choices, + help='Input and output data format', + ) + # define the input data type for the load files + parser.add_argument( + '--input-type', + '-T', + type=str, + default='harmonics', + choices=['harmonics', 'spatial'], + help='Input data type for load fields', + ) # Input and output files have date information - parser.add_argument('--date','-D', - default=False, action='store_true', - help='Input and output files have date information') + parser.add_argument( + '--date', + '-D', + default=False, + action='store_true', + help='Input and output files have date information', + ) # input units # 1: cm of water thickness (cmwe) # 2: Gigatonnes (Gt) # 3: mm of water thickness kg/m^2 - parser.add_argument('--units','-U', - type=int, default=1, choices=[1,2,3], - help='Input units of spatial fields') + parser.add_argument( + '--units', + '-U', + type=int, + default=1, + choices=[1, 2, 3], + help='Input units of spatial fields', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the output files (octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -430,7 +529,9 @@ def main(): try: info(args) # run sea level fingerprints program with parameters - run_sea_level_equation(args.infile, args.outfile, + run_sea_level_equation( + args.infile, + args.outfile, LANDMASK=args.mask, LMAX=args.lmax, LOVE_NUMBERS=args.love, @@ -444,7 +545,8 @@ def main(): INPUT_TYPE=args.input_type, DATE=args.date, UNITS=args.units, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -452,6 +554,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/scale_grace_maps.py b/scripts/scale_grace_maps.py index 03823300..c6cc3ed7 100644 --- a/scripts/scale_grace_maps.py +++ b/scripts/scale_grace_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" scale_grace_maps.py Written by Tyler Sutterley (05/2023) @@ -182,6 +182,7 @@ Updated 02/2021: changed remove index to files with specified formats Updated 02/2021: for public release """ + from __future__ import print_function, division import sys @@ -195,6 +196,7 @@ import traceback import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -204,10 +206,17 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: import GRACE/GRACE-FO files for a given months range # Calculates monthly scaled spatial maps from GRACE/GRACE-FO # spherical harmonic coefficients -def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, +def scale_grace_maps( + base_dir, + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -242,8 +251,8 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # recursively create output Directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -258,8 +267,9 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, 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 - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # atmospheric ECMWF "jump" flag (if ATM) atm_str = '_wATM' if ATM else '' @@ -273,43 +283,46 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, fill_value = -9999.0 # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' else: # else = 1 - wt = np.ones((LMAX+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 = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, - LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + 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)) + 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)) # field mapping for input spatial variables - field_mapping = dict(lon='lon', lat='lat', data='kfactor', - error='error', magnitude='power') + field_mapping = dict( + lon='lon', lat='lat', data='kfactor', error='error', magnitude='power' + ) # read data for input scale files (ascii, netCDF4, HDF5) - if (DATAFORM == 'ascii'): - kfactor = gravtk.scaling_factors().from_ascii(SCALE_FILE, - spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): - kfactor = gravtk.scaling_factors().from_netCDF4(SCALE_FILE, - date=False, field_mapping=field_mapping) - elif (DATAFORM == 'HDF5'): - kfactor = gravtk.scaling_factors().from_HDF5(SCALE_FILE, - date=False, field_mapping=field_mapping) + if DATAFORM == 'ascii': + kfactor = gravtk.scaling_factors().from_ascii( + SCALE_FILE, spacing=[dlon, dlat], nlat=nlat, nlon=nlon + ) + elif DATAFORM == 'netCDF4': + kfactor = gravtk.scaling_factors().from_netCDF4( + SCALE_FILE, date=False, field_mapping=field_mapping + ) + elif DATAFORM == 'HDF5': + kfactor = gravtk.scaling_factors().from_HDF5( + SCALE_FILE, date=False, field_mapping=field_mapping + ) # input data shape nlat, nlon = kfactor.shape @@ -317,18 +330,36 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # 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 = gravtk.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_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + Ylms = gravtk.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_C40=SLR_C40, + 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 = gravtk.harmonics().from_dict(Ylms) # 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 = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GRACE_Ylms.subtract(mean_Ylms) else: @@ -355,7 +386,7 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # default file prefix if not FILE_PREFIX: - fargs = (PROC,DREL,DSET,Ylms['title'],gia_str) + 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 @@ -366,35 +397,37 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, 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) + 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'): + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - Ylms = gravtk.harmonics().from_file(REMOVE_FILE, - format=REMOVEFORM) - elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'): + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,removeform = REMOVEFORM.split('-') + _, removeform = REMOVEFORM.split('-') # index containing files in data format - Ylms = gravtk.harmonics().from_index(REMOVE_FILE, - format=removeform) + Ylms = gravtk.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) + 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] + 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 + 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] + 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() @@ -405,10 +438,22 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # calculating GRACE/GRACE-FO error (Wahr et al. 2006) # output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) - fargs = (PROC,DREL,DSET,LMAX,order_str,ds_str,atm_str,GRACE_Ylms.month[0], - GRACE_Ylms.month[-1], suffix[DATAFORM]) + fargs = ( + 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}' - GRACE_Ylms.directory = pathlib.Path(Ylms['directory']).expanduser().absolute() + GRACE_Ylms.directory = ( + pathlib.Path(Ylms['directory']).expanduser().absolute() + ) DELTA_FILE = GRACE_Ylms.directory.joinpath(delta_format.format(*fargs)) # check full path of the GRACE directory for delta file # if file was previously calculated: will read file @@ -419,38 +464,41 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # Delta coefficients of GRACE time series (Error components) delta_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - delta_Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - delta_Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + 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'))): + 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 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 + 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']): + 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 = gravtk.time_series.smooth(GRACE_Ylms.time, - val1[l,m,:], HFWTH=HFWTH) + smth = gravtk.time_series.smooth( + GRACE_Ylms.time, val1[l, m, :], HFWTH=HFWTH + ) # number of smoothed points nsmth = len(smth['data']) tsmth = np.mean(smth['time']) # GRACE/GRACE-FO delta Ylms # variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) - val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth) + val2[l, m] = np.sqrt(np.sum(smth['noise'] ** 2) / nsmth) # attributes for output files attributes = {} attributes['title'] = 'GRACE/GRACE-FO Spherical Harmonic Errors' - attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # save GRACE/GRACE-FO delta harmonics to file delta_Ylms.time = np.copy(tsmth) delta_Ylms.month = np.int64(nsmth) @@ -461,8 +509,7 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, output_files.append(DELTA_FILE) else: # read GRACE/GRACE-FO delta harmonics from file - delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, - format=DATAFORM) + delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, format=DATAFORM) # 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) @@ -473,17 +520,17 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, 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.month = np.zeros((nfiles), dtype=np.int64) grid.data = np.zeros((nlat, nlon, nfiles)) - grid.mask = np.zeros((nlat, nlon, nfiles),dtype=bool) + grid.mask = np.zeros((nlat, nlon, nfiles), dtype=bool) # Computing plms for converting to spatial domain - phi = np.radians(grid.lon[np.newaxis,:]) + phi = np.radians(grid.lon[np.newaxis, :]) theta = np.radians(90.0 - grid.lat) PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(theta)) # square of legendre polynomials truncated to order MMAX - mm = np.arange(0, MMAX+1) - PLM2 = PLM[:,mm,:]**2 + mm = np.arange(0, MMAX + 1) + PLM2 = PLM[:, mm, :] ** 2 # dfactor is the degree dependent coefficients # for converting to centimeters water equivalent (cmwe) @@ -491,7 +538,7 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # converting harmonics to truncated, smoothed coefficients in units # combining harmonics to calculate output spatial fields - for i,gm in enumerate(GRACE_Ylms.month): + 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 @@ -499,10 +546,17 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # Remove monthly files to be removed Ylms.subtract(remove_Ylms.index(i)) # smooth harmonics and convert to output units - Ylms.convolve(dfactor*wt) + Ylms.convolve(dfactor * wt) # convert spherical harmonics to output spatial grid - grid.data[:,:,i] = gravtk.harmonic_summation(Ylms.clm, Ylms.slm, - grid.lon, grid.lat, LMAX=LMAX, MMAX=MMAX, PLM=PLM).T + grid.data[:, :, i] = gravtk.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) @@ -512,8 +566,18 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, grid.replace_invalid(fill_value, mask=kfactor.mask) # output monthly files to ascii, netCDF4 or HDF5 - fargs = (FILE_PREFIX, '', units, LMAX, order_str, gw_str, - ds_str, grid.month[0], grid.month[-1], suffix[DATAFORM]) + fargs = ( + FILE_PREFIX, + '', + units, + LMAX, + order_str, + gw_str, + ds_str, + grid.month[0], + grid.month[-1], + suffix[DATAFORM], + ) FILE = OUTPUT_DIRECTORY.joinpath(file_format.format(*fargs)) # attributes for output files attributes = {} @@ -521,13 +585,13 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, attributes['longname'] = copy.copy(units_longname) attributes['title'] = 'GRACE/GRACE-FO Spatial Data' attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) grid.to_ascii(FILE, date=True, verbose=VERBOSE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netCDF4 grid.to_netCDF4(FILE, date=True, verbose=VERBOSE, **attributes) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 grid.to_HDF5(FILE, date=True, verbose=VERBOSE, **attributes) # set the permissions mode of the output files @@ -539,27 +603,37 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, 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(np.power(kfactor.magnitude,-1)) + ratio = scaled_power.scale(np.power(kfactor.magnitude, -1)) # replace invalid values with 0 ratio = np.nan_to_num(ratio.data, nan=0.0, posinf=0.0, neginf=0.0) error = grid.copy() - error.data = kfactor.error*ratio + error.data = kfactor.error * ratio error.mask = np.copy(kfactor.mask) error.update_mask() # output monthly error files to ascii, netCDF4 or HDF5 - fargs = (FILE_PREFIX, 'ERROR_', units, LMAX, order_str, gw_str, - ds_str, grid.month[0], grid.month[-1], suffix[DATAFORM]) + fargs = ( + FILE_PREFIX, + 'ERROR_', + units, + LMAX, + order_str, + gw_str, + ds_str, + grid.month[0], + grid.month[-1], + suffix[DATAFORM], + ) FILE = OUTPUT_DIRECTORY.joinpath(file_format.format(*fargs)) # attributes for output files attributes['title'] = 'GRACE/GRACE-FO Scaling Error' - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) error.to_ascii(FILE, date=False, verbose=VERBOSE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netCDF4 error.to_netCDF4(FILE, date=False, verbose=VERBOSE, **attributes) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 error.to_HDF5(FILE, date=False, verbose=VERBOSE, **attributes) # set the permissions mode of the output files @@ -574,46 +648,56 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, delta.time = np.copy(tsmth) delta.month = np.copy(nsmth) delta.data = np.zeros((nlat, nlon)) - delta.mask = np.zeros((nlat, nlon),dtype=bool) + 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 + 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) + 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) + 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] + 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): + 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) + 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 + delta.data[:] = np.sqrt(np.dot(ccos.T, d_cos) + np.dot(ssin.T, d_sin)).T # 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 - fargs = (FILE_PREFIX, 'DELTA_', units, LMAX, order_str, gw_str, - ds_str, grid.month[0], grid.month[-1], suffix[DATAFORM]) + fargs = ( + FILE_PREFIX, + 'DELTA_', + units, + LMAX, + order_str, + gw_str, + ds_str, + grid.month[0], + grid.month[-1], + suffix[DATAFORM], + ) FILE = OUTPUT_DIRECTORY.joinpath(file_format.format(*fargs)) # attributes for output files attributes['title'] = 'GRACE/GRACE-FO Spatial Error' - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) delta.to_ascii(FILE, date=True, verbose=VERBOSE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netCDF4 delta.to_netCDF4(FILE, date=True, verbose=VERBOSE, **attributes) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 delta.to_HDF5(FILE, date=True, verbose=VERBOSE, **attributes) # set the permissions mode of the output files @@ -624,10 +708,11 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # return the list of output files return output_files + # PURPOSE: print a file log for the GRACE/GRACE-FO analysis def output_log_file(input_arguments, output_files): # format: scale_GRACE_maps_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'scale_GRACE_maps_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -644,10 +729,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE/GRACE-FO analysis def output_error_log_file(input_arguments): # format: scale_GRACE_maps_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'scale_GRACE_maps_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -663,92 +749,194 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates scaled spatial maps from GRACE/GRACE-FO spherical harmonic coefficients """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') - parser.add_argument('--output-directory','-O', + help='Working data directory', + ) + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + help='Output directory for spatial files', + ) + parser.add_argument( + '--file-prefix', + '-P', type=str, - help='Prefix string for input and output files') + help='Prefix string for input and output files', + ) # Data processing center or satellite mission - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # minimum spherical harmonic degree - parser.add_argument('--lmin', - type=int, default=1, - help='Minimum spherical harmonic degree') + parser.add_argument( + '--lmin', type=int, default=1, help='Minimum spherical harmonic degree' + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # Gaussian smoothing radius (km) - parser.add_argument('--radius','-R', - type=float, default=0, - help='Gaussian smoothing radius (km)') + parser.add_argument( + '--radius', + '-R', + type=float, + default=0, + help='Gaussian smoothing radius (km)', + ) # Use a decorrelation (destriping) filter - parser.add_argument('--destripe','-d', - default=False, action='store_true', - help='Use decorrelation (destriping) filter') + parser.add_argument( + '--destripe', + '-d', + default=False, + action='store_true', + help='Use decorrelation (destriping) filter', + ) # output grid parameters - parser.add_argument('--spacing', - type=float, nargs='+', default=[0.5,0.5], metavar=('dlon','dlat'), - help='Spatial resolution of output data') - parser.add_argument('--interval', - type=int, default=2, choices=[1,2], - help=('Output grid interval (1: global, 2: centered global)')) + parser.add_argument( + '--spacing', + type=float, + nargs='+', + default=[0.5, 0.5], + metavar=('dlon', 'dlat'), + help='Spatial resolution of output data', + ) + parser.add_argument( + '--interval', + type=int, + default=2, + choices=[1, 2], + help=('Output grid interval (1: global, 2: centered global)'), + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -764,21 +952,32 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # use atmospheric jump corrections from Fagiolini et al. (2015) - parser.add_argument('--atm-correction', - default=False, action='store_true', - help='Apply atmospheric jump correction coefficients') + parser.add_argument( + '--atm-correction', + default=False, + action='store_true', + help='Apply atmospheric jump correction coefficients', + ) # correct for pole tide drift follow Wahr et al. (2015) - parser.add_argument('--pole-tide', - default=False, action='store_true', - help='Correct for pole tide drift') + parser.add_argument( + '--pole-tide', + default=False, + action='store_true', + help='Correct for pole tide drift', + ) # Update Degree 1 coefficients with SLR or derived values # Tellus: GRACE/GRACE-FO TN-13 from PO.DAAC # https://grace.jpl.nasa.gov/data/get-data/geocenter/ @@ -790,91 +989,162 @@ def arguments(): # https://doi.org/10.1029/2007JB005338 # GFZ: GRACE/GRACE-FO coefficients from GFZ GravIS # http://gravis.gfz-potsdam.de/corrections - parser.add_argument('--geocenter', - metavar='DEG1', type=str, - choices=['Tellus','SLR','SLF','UCI','Swenson','GFZ'], - help='Update Degree 1 coefficients with SLR or derived values') - parser.add_argument('--geocenter-file', + parser.add_argument( + '--geocenter', + metavar='DEG1', + type=str, + choices=['Tellus', 'SLR', 'SLF', 'UCI', 'Swenson', 'GFZ'], + help='Update Degree 1 coefficients with SLR or derived values', + ) + parser.add_argument( + '--geocenter-file', type=pathlib.Path, - help='Specific geocenter file if not default') - parser.add_argument('--interpolate-geocenter', - default=False, action='store_true', - help='Least-squares model missing Degree 1 coefficients') + help='Specific geocenter file if not default', + ) + parser.add_argument( + '--interpolate-geocenter', + default=False, + action='store_true', + help='Least-squares model missing Degree 1 coefficients', + ) # replace low degree harmonics with values from Satellite Laser Ranging - parser.add_argument('--slr-c20', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C20 coefficients with SLR values') - parser.add_argument('--slr-21', - type=str, default=None, choices=['CSR','GFZ','GSFC'], - help='Replace C21 and S21 coefficients with SLR values') - parser.add_argument('--slr-22', - type=str, default=None, choices=['CSR','GSFC'], - help='Replace C22 and S22 coefficients with SLR values') - parser.add_argument('--slr-c30', - type=str, default=None, choices=['CSR','GFZ','GSFC','LARES'], - help='Replace C30 coefficients with SLR values') - parser.add_argument('--slr-c40', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C40 coefficients with SLR values') - parser.add_argument('--slr-c50', - type=str, default=None, choices=['CSR','GSFC','LARES'], - help='Replace C50 coefficients with SLR values') + parser.add_argument( + '--slr-c20', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C20 coefficients with SLR values', + ) + parser.add_argument( + '--slr-21', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC'], + help='Replace C21 and S21 coefficients with SLR values', + ) + parser.add_argument( + '--slr-22', + type=str, + default=None, + choices=['CSR', 'GSFC'], + help='Replace C22 and S22 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c30', + type=str, + default=None, + choices=['CSR', 'GFZ', 'GSFC', 'LARES'], + help='Replace C30 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c40', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C40 coefficients with SLR values', + ) + parser.add_argument( + '--slr-c50', + type=str, + default=None, + choices=['CSR', 'GSFC', 'LARES'], + help='Replace C50 coefficients with SLR values', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # mean file to remove - parser.add_argument('--mean-file', + parser.add_argument( + '--mean-file', type=pathlib.Path, - help='GRACE/GRACE-FO mean file to remove from the harmonic data') + help='GRACE/GRACE-FO mean file to remove from the harmonic data', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--mean-format', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5','gfc'], - help='Input data format for GRACE/GRACE-FO mean file') + parser.add_argument( + '--mean-format', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5', 'gfc'], + help='Input data format for GRACE/GRACE-FO mean file', + ) # monthly files to be removed from the GRACE/GRACE-FO data - parser.add_argument('--remove-file', - type=pathlib.Path, nargs='+', - help='Monthly files to be removed from the GRACE/GRACE-FO data') + parser.add_argument( + '--remove-file', + type=pathlib.Path, + nargs='+', + help='Monthly files to be removed from the GRACE/GRACE-FO data', + ) choices = [] - choices.extend(['ascii','netCDF4','HDF5']) - choices.extend(['index-ascii','index-netCDF4','index-HDF5']) - parser.add_argument('--remove-format', - type=str, nargs='+', choices=choices, - help='Input data format for files to be removed') - parser.add_argument('--redistribute-removed', - default=False, action='store_true', - help='Redistribute removed mass fields over the ocean') + choices.extend(['ascii', 'netCDF4', 'HDF5']) + choices.extend(['index-ascii', 'index-netCDF4', 'index-HDF5']) + parser.add_argument( + '--remove-format', + type=str, + nargs='+', + choices=choices, + help='Input data format for files to be removed', + ) + parser.add_argument( + '--redistribute-removed', + default=False, + action='store_true', + help='Redistribute removed mass fields over the ocean', + ) # scaling factor file - parser.add_argument('--scale-file', + parser.add_argument( + '--scale-file', type=pathlib.Path, - required=True, help='Scaling factor file') + required=True, + help='Scaling factor file', + ) # land-sea mask for redistributing fluxes - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing land water flux') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing land water flux', + ) # Output log file for each job in forms # scale_GRACE_maps_run_2002-04-01_PID-00000.log # scale_GRACE_maps_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -925,18 +1195,20 @@ def main(): OUTPUT_DIRECTORY=args.output_directory, FILE_PREFIX=args.file_prefix, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/sea_level_differences.py b/scripts/sea_level_differences.py index 42cf0be1..dc3a7764 100644 --- a/scripts/sea_level_differences.py +++ b/scripts/sea_level_differences.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" sea_level_difference.py Written by Tyler Sutterley (05/2023) @@ -47,6 +47,7 @@ Updated 06/2018: using getopt to set parameters Written 08/2017 """ + import sys import os import logging @@ -56,6 +57,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -65,8 +67,12 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate the SLF error map -def sea_level_difference(PROC, DREL, DSET, +def sea_level_difference( + PROC, + DREL, + DSET, START=None, END=None, DATAFORM=None, @@ -77,8 +83,8 @@ def sea_level_difference(PROC, DREL, DSET, LANDMASK=None, RUNS=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -101,15 +107,16 @@ def sea_level_difference(PROC, DREL, DSET, # Land-Sea Mask with Antarctica from Rignot (2017) and Greenland from GEUS # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF4 file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, - varname='LSMASK') - dlon,dlat = landsea.spacing + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # create land function - land_function = np.zeros((nlat, nlon),dtype=np.float64) + land_function = np.zeros((nlat, nlon), dtype=np.float64) # combine land and island levels for land function - indy,indx = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - land_function[indy,indx] = 1.0 + indy, indx = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + land_function[indy, indx] = 1.0 # calculate deviations of monte carlo fields from zero VARIANCE = landsea.zeros_like() @@ -118,18 +125,24 @@ def sea_level_difference(PROC, DREL, DSET, VARIANCE.fill_value = -9999.0 for n in range(RUNS): # read SLF files - F1 = slf_pattern.format(ITERATION,dset_str,ocean_str,EXPANSION, - n,suffix[DATAFORM]) + F1 = slf_pattern.format( + ITERATION, dset_str, ocean_str, EXPANSION, n, suffix[DATAFORM] + ) FILE1 = OUTPUT_DIRECTORY.joinpath(F1) - val = gravtk.spatial().from_file(FILE1, - format=DATAFORM, date=False, spacing=[dlon,dlat], - nlon=nlon, nlat=nlat) + val = gravtk.spatial().from_file( + FILE1, + format=DATAFORM, + date=False, + spacing=[dlon, dlat], + nlon=nlon, + nlat=nlat, + ) VARIANCE.data += val.power(2.0).data VARIANCE.mask |= val.mask # update mask VARIANCE.update_mask() # calculate RMS of variance - ERROR = VARIANCE.scale(1.0/(RUNS-1.0)).power(0.5) + ERROR = VARIANCE.scale(1.0 / (RUNS - 1.0)).power(0.5) ERROR.update_mask() # attributes for output files @@ -139,89 +152,146 @@ def sea_level_difference(PROC, DREL, DSET, attributes['title'] = 'Sea_Level_Fingerprint' attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' # save to file - F2 = file_format.format(ITERATION,dset_str,ocean_str,EXPANSION, - START,END,suffix[DATAFORM]) + F2 = file_format.format( + ITERATION, dset_str, ocean_str, EXPANSION, START, END, suffix[DATAFORM] + ) FILE2 = OUTPUT_DIRECTORY.joinpath(F2) - ERROR.to_file(FILE2, format=DATAFORM, - date=False, verbose=VERBOSE, **attributes) + ERROR.to_file( + FILE2, format=DATAFORM, date=False, verbose=VERBOSE, **attributes + ) # change the permissions mode FILE2.chmod(mode=MODE) # add to output file list output_files.append(FILE2) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the sea level fingerprint error map """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') + help='Output directory for spatial files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # mascon parameters - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # sea level fingerprint parameters - parser.add_argument('--iteration','-I', - type=int, default=1, - help='Sea level fingerprint iteration') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--iteration', + '-I', + type=int, + default=1, + help='Sea level fingerprint iteration', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # land-sea mask for redistributing mascon mass and land water flux - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing mascon mass and land water flux') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing mascon mass and land water flux', + ) # number of monte carlo iterations - parser.add_argument('--runs','-R', - type=int, default=10000, - help='Number of Monte Carlo iterations') + parser.add_argument( + '--runs', + '-R', + type=int, + default=10000, + help='Number of Monte Carlo iterations', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -245,7 +315,8 @@ def main(): RUNS=args.runs, OUTPUT_DIRECTORY=args.output_directory, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -253,6 +324,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/sea_level_error.py b/scripts/sea_level_error.py index c2e991c7..ca210ace 100755 --- a/scripts/sea_level_error.py +++ b/scripts/sea_level_error.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" sea_level_error.py Written by Tyler Sutterley (05/2023) Reads in sea level grid error files and converts to spherical @@ -78,6 +78,7 @@ Updated 06/2018: using python3 compatible octal and input Written 04/2018 """ + from __future__ import print_function import sys @@ -91,6 +92,7 @@ import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -100,8 +102,13 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # program module to run with specified parameters -def sea_level_error(PROC, DREL, DSET, LMAX, +def sea_level_error( + PROC, + DREL, + DSET, + LMAX, MMAX=None, LOVE_NUMBERS=0, REFERENCE=None, @@ -112,8 +119,8 @@ def sea_level_error(PROC, DREL, DSET, LMAX, RUNS=0, LANDMASK=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -122,8 +129,9 @@ def sea_level_error(PROC, DREL, DSET, LMAX, suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # read load love numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE) + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE + ) # for datasets not GSM: will add a label for the dataset dset_str = '' if (DSET == 'GSM') else f'_{DSET}' @@ -135,16 +143,17 @@ def sea_level_error(PROC, DREL, DSET, LMAX, # Land-Sea Mask with Antarctica from Rignot (2017) and Greenland from GEUS # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF4 file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, - varname='LSMASK') - dlon,dlat = landsea.spacing + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # calculate Fully-Normalized Legendre Polynomials th = np.radians(90.0 - landsea.lat) PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(th)) # create index file for least_squares_mascons.py - args = (ITERATION,dset_str,ocean_str,LMAX,order_str) + args = (ITERATION, dset_str, ocean_str, LMAX, order_str) INDEX = 'SLF_MC_ITERATION_{0}_INDEX{1}{2}_CLM_L{3:d}{4}.txt'.format(*args) index_file = OUTPUT_DIRECTORY.joinpath(INDEX) fid = index_file.open(mode='w', encoding='utf8') @@ -160,36 +169,66 @@ def sea_level_error(PROC, DREL, DSET, LMAX, output_files = [] for n in range(0, RUNS): # sea level file for iteration (spatial fields) - SLF = file_format.format(ITERATION,dset_str,ocean_str,'', - EXPANSION,'',n,suffix[DATAFORM]) + SLF = file_format.format( + ITERATION, + dset_str, + ocean_str, + '', + EXPANSION, + '', + n, + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(SLF) # read sea level file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(INPUT_FILE, - date=False, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + INPUT_FILE, + date=False, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM == 'netCDF4': # netcdf (.nc) dinput = gravtk.spatial().from_netCDF4(INPUT_FILE, date=False) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 (.H5) dinput = gravtk.spatial().from_HDF5(INPUT_FILE, date=False) # Converting sea level field into spherical harmonics (can truncate) - Ylms = gravtk.gen_stokes(dinput.data.T, dinput.lon, dinput.lat, - UNITS=1, LMIN=0, LMAX=LMAX, MMAX=MMAX, LOVE=LOVE, PLM=PLM) + Ylms = gravtk.gen_stokes( + dinput.data.T, + dinput.lon, + dinput.lat, + UNITS=1, + LMIN=0, + LMAX=LMAX, + MMAX=MMAX, + LOVE=LOVE, + PLM=PLM, + ) # output (truncated) spherical harmonics to file - FILE = file_format.format(ITERATION,dset_str,ocean_str,'_CLM', - LMAX,order_str,n,suffix[DATAFORM]) + FILE = file_format.format( + ITERATION, + dset_str, + ocean_str, + '_CLM', + LMAX, + order_str, + n, + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(FILE) - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) Ylms.to_ascii(OUTPUT_FILE, date=False) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netcdf (.nc) Ylms.to_netCDF4(OUTPUT_FILE, date=False, **attributes) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 (.H5) Ylms.to_HDF5(OUTPUT_FILE, date=False, **attributes) # change output file permissions mode to MODE @@ -205,10 +244,11 @@ def sea_level_error(PROC, DREL, DSET, LMAX, # return the list of output files return output_files + # PURPOSE: print a file log for the sea level harmonics calculation def output_log_file(input_arguments, output_files): # format: sea_level_error_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()),os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'sea_level_error_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -225,10 +265,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the sea level harmonics calculation def output_error_log_file(input_arguments): # format: failed_sea_level_error_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()),os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'failed_sea_level_error_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -244,98 +285,168 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Reads in sea level grid error files and converts to spherical harmonics """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # sea level fingerprint parameters - parser.add_argument('--iteration','-I', - type=int, default=1, - help='Sea level fingerprint iteration') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--iteration', + '-I', + type=int, + default=1, + help='Sea level fingerprint iteration', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # number of monte carlo iterations - parser.add_argument('--runs', - type=int, default=10000, - help='Number of Monte Carlo iterations') + parser.add_argument( + '--runs', + type=int, + default=10000, + help='Number of Monte Carlo iterations', + ) # land-sea mask for redistributing mascon mass and land water flux - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing mascon mass and land water flux') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing mascon mass and land water flux', + ) # Output log file for each job in forms # sea_level_error_run_2002-04-01_PID-00000.log # sea_level_error_failed_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about processing run - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -360,18 +471,20 @@ def main(): RUNS=args.runs, LANDMASK=args.mask, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/sea_level_mean.py b/scripts/sea_level_mean.py index 0cf996c9..ab0e6117 100644 --- a/scripts/sea_level_mean.py +++ b/scripts/sea_level_mean.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" sea_level_mean.py Written by Tyler Sutterley (05/2023) @@ -62,6 +62,7 @@ calculate mean for different sea level iterations Written 08/2017 """ + import sys import os import copy @@ -85,6 +86,7 @@ # A et al. (2013) gia_mean_str['AW13-ICE6G'] = '_AW13_ICE6G_mean' + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -94,8 +96,12 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate the mean SLF map for each GIA model -def sea_level_mean(PROC, DREL, DSET, +def sea_level_mean( + PROC, + DREL, + DSET, START=None, END=None, GIA=None, @@ -107,8 +113,8 @@ def sea_level_mean(PROC, DREL, DSET, EXPANSION=None, LANDMASK=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -131,15 +137,16 @@ def sea_level_mean(PROC, DREL, DSET, # Land-Sea Mask with Antarctica from Rignot (2017) and Greenland from GEUS # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF4 file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, - varname='LSMASK') - dlon,dlat = landsea.spacing + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # create land function - land_function = np.zeros((nlat, nlon),dtype=np.float64) + land_function = np.zeros((nlat, nlon), dtype=np.float64) # combine land and island levels for land function - indy,indx = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - land_function[indy,indx] = 1.0 + indy, indx = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + land_function[indy, indx] = 1.0 # allocate lists for input variables dinput = {} @@ -151,89 +158,154 @@ def sea_level_mean(PROC, DREL, DSET, # number of rheologies and ice histories to run N = len(GIA_FILES) # iterate GIA models - for h,GIA_FILE in enumerate(GIA_FILES): + for h, GIA_FILE in enumerate(GIA_FILES): # input GIA spherical harmonic datafiles GIA_Ylms_rate = gravtk.gia().from_GIA(GIA_FILE, GIA=GIA) gia_str = f'_{GIA_Ylms_rate.title}' # read mass trend files - for key in ['x1','x2']: - F1 = file_format.format(ITERATION, dset_str, gia_str, - ocean_str, EXPANSION, key, START, END, suffix[DATAFORM]) + for key in ['x1', 'x2']: + F1 = file_format.format( + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + key, + START, + END, + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(F1) - field_mapping = dict(lon='lon', lat='lat', data='data', error='error') - temp = gravtk.spatial().from_file(INPUT_FILE, - format=DATAFORM, date=False, field_mapping=field_mapping, - spacing=[dlon,dlat], nlon=nlon, nlat=nlat) + field_mapping = dict( + lon='lon', lat='lat', data='data', error='error' + ) + temp = gravtk.spatial().from_file( + INPUT_FILE, + format=DATAFORM, + date=False, + field_mapping=field_mapping, + spacing=[dlon, dlat], + nlon=nlon, + nlat=nlat, + ) dinput[key].append(temp) # read AIC files - for key in ['AIC_x0','AIC_x1','AIC_x2']: - F1 = file_format.format(ITERATION, dset_str, gia_str, - ocean_str, EXPANSION, key, START, END, suffix[DATAFORM]) + for key in ['AIC_x0', 'AIC_x1', 'AIC_x2']: + F1 = file_format.format( + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + key, + START, + END, + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(F1) field_mapping = dict(lon='lon', lat='lat', data='data') - temp = gravtk.spatial().from_file(INPUT_FILE, - format=DATAFORM, date=False, field_mapping=field_mapping, - spacing=[dlon,dlat], nlon=nlon, nlat=nlat) + temp = gravtk.spatial().from_file( + INPUT_FILE, + format=DATAFORM, + date=False, + field_mapping=field_mapping, + spacing=[dlon, dlat], + nlon=nlon, + nlat=nlat, + ) dinput[key].append(temp) # create combined spatial objects output = {} units = {} # calculate mean GIA-corrected x1 change (for all Earth rheologies) - x1 = gravtk.spatial().from_list(dinput['x1'],date=False) + x1 = gravtk.spatial().from_list(dinput['x1'], date=False) output['x1'] = x1.mean() - units['x1'] = '{0} yr^{1:d}'.format('centimeters',-1) + units['x1'] = '{0} yr^{1:d}'.format('centimeters', -1) # calculate mean acceleration x2 change (for all Earth rheologies) - x2 = gravtk.spatial().from_list(dinput['x2'],date=False) + x2 = gravtk.spatial().from_list(dinput['x2'], date=False) output['x2'] = x2.mean().scale(2.0) - units['x2'] = '{0} yr^{1:d}'.format('centimeters',-2) + units['x2'] = '{0} yr^{1:d}'.format('centimeters', -2) # GRACE satellite error component - e1 = x1.copy(); e1.data = np.copy(x1.error) - output['x1'].error = e1.sum(power=2.0).scale(1.0/N).power(0.5).data + e1 = x1.copy() + e1.data = np.copy(x1.error) + output['x1'].error = e1.sum(power=2.0).scale(1.0 / N).power(0.5).data output['x1'].update_mask() - e2 = x2.copy(); e2.data = np.copy(x2.error) - output['x2'].error = e2.sum(power=2.0).scale(4.0/N).power(0.5).data + e2 = x2.copy() + e2.data = np.copy(x2.error) + output['x2'].error = e2.sum(power=2.0).scale(4.0 / N).power(0.5).data output['x2'].update_mask() # significance means - AICx0 = gravtk.spatial().from_list(dinput['AIC_x0'],date=False).mean() - AICx1 = gravtk.spatial().from_list(dinput['AIC_x1'],date=False).mean() - AICx2 = gravtk.spatial().from_list(dinput['AIC_x2'],date=False).mean() + AICx0 = gravtk.spatial().from_list(dinput['AIC_x0'], date=False).mean() + AICx1 = gravtk.spatial().from_list(dinput['AIC_x1'], date=False).mean() + AICx2 = gravtk.spatial().from_list(dinput['AIC_x2'], date=False).mean() # masked trend values - ii,jj = np.nonzero((np.abs(output['x1'].data) <= output['x1'].error) | - (AICx1.data >= AICx0.data)) + ii, jj = np.nonzero( + (np.abs(output['x1'].data) <= output['x1'].error) + | (AICx1.data >= AICx0.data) + ) output['MASKED_x1'] = output['x1'].copy() - output['MASKED_x1'].mask[ii,jj] = True + output['MASKED_x1'].mask[ii, jj] = True output['MASKED_x1'].update_mask() - units['MASKED_x1'] = '{0} yr^{1:d}'.format('centimeters',-1) + units['MASKED_x1'] = '{0} yr^{1:d}'.format('centimeters', -1) # write masked acceleration values to file - ii,jj = np.nonzero((np.abs(output['x2'].data) <= output['x2'].error) | - (AICx2.data >= AICx1.data)) + ii, jj = np.nonzero( + (np.abs(output['x2'].data) <= output['x2'].error) + | (AICx2.data >= AICx1.data) + ) output['MASKED_x2'] = output['x2'].copy() - output['MASKED_x2'].mask[ii,jj] = True + output['MASKED_x2'].mask[ii, jj] = True output['MASKED_x2'].update_mask() - units['MASKED_x2'] = '{0} yr^{1:d}'.format('centimeters',-2) + units['MASKED_x2'] = '{0} yr^{1:d}'.format('centimeters', -2) # attributes for output files longname = 'Equivalent_Water_Thickness' title = 'Sea_Level_Fingerprint' # output data to file - for key,val in output.items(): - F2 = file_format.format(ITERATION, dset_str, gia_mean_str[GIA], - ocean_str, EXPANSION, key, START, END, suffix[DATAFORM]) + for key, val in output.items(): + F2 = file_format.format( + ITERATION, + dset_str, + gia_mean_str[GIA], + ocean_str, + EXPANSION, + key, + START, + END, + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(F2) - output_data(val, FILENAME=OUTPUT_FILE, DATAFORM=DATAFORM, - UNITS=units[key], LONGNAME=longname, TITLE=title, - CONF=0.95, VERBOSE=VERBOSE, MODE=MODE) + output_data( + val, + FILENAME=OUTPUT_FILE, + DATAFORM=DATAFORM, + UNITS=units[key], + LONGNAME=longname, + TITLE=title, + CONF=0.95, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add to output file list output_files.append(OUTPUT_FILE) # return the list of output files return output_files + # PURPOSE: wrapper function for outputting data to file -def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, - LONGNAME=None, TITLE=None, CONF=0, VERBOSE=0, MODE=0o775): +def output_data( + data, + FILENAME=None, + DATAFORM=None, + UNITS=None, + LONGNAME=None, + TITLE=None, + CONF=0, + VERBOSE=0, + MODE=0o775, +): # field mapping for output regression data field_mapping = {} field_mapping['lat'] = 'lat' @@ -258,60 +330,95 @@ def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, attributes['error']['description'] = 'Uncertainty_in_model_fit' attributes['error']['long_name'] = LONGNAME attributes['error']['units'] = UNITS - attributes['error']['confidence'] = 100*CONF + attributes['error']['confidence'] = 100 * CONF # output global attributes REFERENCE = f'Output from {pathlib.Path(sys.argv[0]).name}' # write to output file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) data.to_ascii(FILENAME, date=False, verbose=VERBOSE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netcdf (.nc) - data.to_netCDF4(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) - elif (DATAFORM == 'HDF5'): + data.to_netCDF4( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) - data.to_HDF5(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) + data.to_HDF5( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) # change the permissions mode of the output file FILENAME.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Calculates the mean sea level fingerprint map for each glacial isostatic adjustment model """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters # working data directory - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for spatial files') + help='Output directory for spatial files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -327,49 +434,82 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - nargs='+', help='GIA files to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, nargs='+', help='GIA files to read' + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) # mascon parameters - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # sea level fingerprint parameters - parser.add_argument('--iteration','-I', - type=int, default=1, - help='Sea level fingerprint iteration') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--iteration', + '-I', + type=int, + default=1, + help='Sea level fingerprint iteration', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # land-sea mask for redistributing mascon mass and land water flux - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing mascon mass and land water flux') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing mascon mass and land water flux', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -394,7 +534,8 @@ def main(): LANDMASK=args.mask, OUTPUT_DIRECTORY=args.output_directory, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the @@ -402,6 +543,7 @@ def main(): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # run main program if __name__ == '__main__': main() diff --git a/scripts/sea_level_regress.py b/scripts/sea_level_regress.py index 623b1df9..1e12f470 100644 --- a/scripts/sea_level_regress.py +++ b/scripts/sea_level_regress.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" sea_level_regress.py Written by Tyler Sutterley (07/2026) @@ -92,6 +92,7 @@ Updated 08/2017: running at 0.5x0.5 degrees Written 08/2017 """ + from __future__ import print_function, division import sys @@ -104,6 +105,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -113,8 +115,13 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # program module to run with specified parameters -def sea_level_regress(PROC, DREL, DSET, LMAX, +def sea_level_regress( + PROC, + DREL, + DSET, + LMAX, START=None, END=None, MISSING=None, @@ -129,15 +136,15 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, LANDMASK=None, OUTPUT_DIRECTORY=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): OUTPUT_DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) # GRACE/GRACE-FO months - months = sorted(set(np.arange(START,END+1)) - set(MISSING)) + months = sorted(set(np.arange(START, END + 1)) - set(MISSING)) nmon = len(months) # output filename suffix suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')[DATAFORM] @@ -153,20 +160,23 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, # input and output file formats input_format = 'SLF_ITERATION_{0}{1}{2}{3}_L{4:d}_{5:03d}.{6}' - output_format = 'SLF_ITERATION_{0}{1}{2}{3}_L{4:d}_{5}{6}_{7:03d}-{8:03d}.{9}' + output_format = ( + 'SLF_ITERATION_{0}{1}{2}{3}_L{4:d}_{5}{6}_{7:03d}-{8:03d}.{9}' + ) # Land-Sea Mask with Antarctica from Rignot (2017) and Greenland from GEUS # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF4 file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, - varname='LSMASK') - dlon,dlat = landsea.spacing + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # create land function - land_function = np.zeros((nlat, nlon),dtype=np.float64) + land_function = np.zeros((nlat, nlon), dtype=np.float64) # combine land and island levels for land function - indy,indx = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - land_function[indy,indx] = 1.0 + indy, indx = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + land_function[indy, indx] = 1.0 # calculate ocean function from land function ocean_function = 1.0 - land_function # bad value and land function as mask @@ -177,21 +187,29 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, spatial_list = [] for t in range(0, nmon): # sea level file for month - fi = input_format.format(ITERATION,dset_str,gia_str,ocean_str, - EXPANSION,months[t],suffix) + fi = input_format.format( + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + months[t], + suffix, + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(fi) # read sea level file - if (DATAFORM == 'ascii'): - dinput = gravtk.spatial().from_ascii(INPUT_FILE, - spacing=[dlon,dlat], nlon=nlon, nlat=nlat) - elif (DATAFORM == 'netCDF4'): + if DATAFORM == 'ascii': + dinput = gravtk.spatial().from_ascii( + INPUT_FILE, spacing=[dlon, dlat], nlon=nlon, nlat=nlat + ) + elif DATAFORM == 'netCDF4': # netcdf (.nc) dinput = gravtk.spatial().from_netCDF4(INPUT_FILE) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 (.H5) dinput = gravtk.spatial().from_HDF5(INPUT_FILE) # append to spatial list - dinput.replace_invalid(FILL_VALUE,mask=MASK) + dinput.replace_invalid(FILL_VALUE, mask=MASK) spatial_list.append(dinput) # concatenate list to single spatial object @@ -199,14 +217,16 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, spatial_list = None # Setting output parameters for each fit type - coef_str = ['x{0:d}'.format(o) for o in range(ORDER+1)] - unit_suffix = [' yr^{0:d}'.format(-o) if o else '' for o in range(ORDER+1)] - if (ORDER == 0):# Mean + coef_str = ['x{0:d}'.format(o) for o in range(ORDER + 1)] + unit_suffix = [ + ' yr^{0:d}'.format(-o) if o else '' for o in range(ORDER + 1) + ] + if ORDER == 0: # Mean fit_longname = ['Mean'] - elif (ORDER == 1):# Trend - fit_longname = ['Constant','Trend'] - elif (ORDER == 2):# Quadratic - fit_longname = ['Constant','Linear','Quadratic'] + elif ORDER == 1: # Trend + fit_longname = ['Constant', 'Trend'] + elif ORDER == 2: # Quadratic + fit_longname = ['Constant', 'Linear', 'Quadratic'] # amplitude string for cyclical components amp_str = [] @@ -227,28 +247,29 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, # extra terms for tidal aliasing components or custom fits TERMS = [] term_index = [] - for i,c in enumerate(CYCLES): + for i, c in enumerate(CYCLES): # check if fitting with semi-annual or annual terms - if (c == 0.5): - coef_str.extend(['SS','SC']) + if c == 0.5: + coef_str.extend(['SS', 'SC']) amp_str.append('SEMI') amp_title['SEMI'] = 'Semi-Annual Amplitude' ph_title['SEMI'] = 'Semi-Annual Phase' fit_longname.extend(['Semi-Annual Sine', 'Semi-Annual Cosine']) - unit_suffix.extend(['','']) - elif (c == 1.0): - coef_str.extend(['AS','AC']) + unit_suffix.extend(['', '']) + elif c == 1.0: + coef_str.extend(['AS', 'AC']) amp_str.append('ANN') amp_title['ANN'] = 'Annual Amplitude' ph_title['ANN'] = 'Annual Phase' fit_longname.extend(['Annual Sine', 'Annual Cosine']) - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # check if fitting with tidal aliasing terms - for t,period in tidal_aliasing.items(): - if np.isclose(c, (period/365.25)): + for t, period in tidal_aliasing.items(): + if np.isclose(c, (period / 365.25)): # terms for tidal aliasing during GRACE and GRACE-FO periods - TERMS.extend(gravtk.time_series.aliasing_terms(grid.time, - period=period)) + TERMS.extend( + gravtk.time_series.aliasing_terms(grid.time, period=period) + ) # labels for tidal aliasing during GRACE period coef_str.extend([f'{t}SGRC', f'{t}CGRC']) amp_str.append(f'{t}GRC') @@ -256,7 +277,7 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, ph_title[f'{t}GRC'] = f'{t} Tidal Alias (GRACE) Phase' fit_longname.append(f'{t} Tidal Alias (GRACE) Sine') fit_longname.append(f'{t} Tidal Alias (GRACE) Cosine') - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # labels for tidal aliasing during GRACE-FO period coef_str.extend([f'{t}SGFO', f'{t}CGFO']) amp_str.append(f'{t}GFO') @@ -264,7 +285,7 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, ph_title[f'{t}GFO'] = f'{t} Tidal Alias (GRACE-FO) Phase' fit_longname.append(f'{t} Tidal Alias (GRACE-FO) Sine') fit_longname.append(f'{t} Tidal Alias (GRACE-FO) Cosine') - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # index to remove the original tidal aliasing term term_index.append(i) # remove the original tidal aliasing terms @@ -272,7 +293,7 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, # Fitting seasonal components ncomp = len(coef_str) - ncycles = 2*len(CYCLES) + len(TERMS) + ncycles = 2 * len(CYCLES) + len(TERMS) # confidence interval for regression fit errors CONF = 0.95 @@ -280,44 +301,60 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, out = dinput.zeros_like() out.data = np.zeros((nlat, nlon, ncomp)) out.error = np.zeros((nlat, nlon, ncomp)) - out.mask = np.ones((nlat, nlon, ncomp),dtype=bool) + out.mask = np.ones((nlat, nlon, ncomp), dtype=bool) # Fit Significance FS = {} # SSE: Sum of Squares Error # AIC: Akaike information criterion # BIC: Bayesian information criterion # R2Adj: Adjusted Coefficient of Determination - for key in ['SSE','AIC','BIC','R2Adj']: + for key in ['SSE', 'AIC', 'BIC', 'R2Adj']: FS[key] = dinput.zeros_like() # valid values for ocean function - indy,indx = np.nonzero(ocean_function) + indy, indx = np.nonzero(ocean_function) # calculate the regression coefficients and fit significance - for i,j in zip(indy,indx): + for i, j in zip(indy, indx): # Calculating the regression coefficients - tsbeta = gravtk.time_series.regress(grid.time, grid.data[i,j,:], - ORDER=ORDER, CYCLES=CYCLES, TERMS=TERMS, CONF=CONF) + tsbeta = gravtk.time_series.regress( + grid.time, + grid.data[i, j, :], + ORDER=ORDER, + CYCLES=CYCLES, + TERMS=TERMS, + CONF=CONF, + ) # save regression components for k in range(0, ncomp): - out.data[i,j,k] = tsbeta['beta'][k] - out.error[i,j,k] = tsbeta['error'][k] - out.mask[i,j,k] = False + out.data[i, j, k] = tsbeta['beta'][k] + out.error[i, j, k] = tsbeta['error'][k] + out.mask[i, j, k] = False # Fit significance terms # Degrees of Freedom nu = tsbeta['DOF'] # Converting Mean Square Error to Sum of Squares Error - FS['SSE'].data[i,j] = tsbeta['MSE']*nu - FS['AIC'].data[i,j] = tsbeta['AIC'] - FS['BIC'].data[i,j] = tsbeta['BIC'] - FS['R2Adj'].data[i,j] = tsbeta['R2Adj'] + FS['SSE'].data[i, j] = tsbeta['MSE'] * nu + FS['AIC'].data[i, j] = tsbeta['AIC'] + FS['BIC'].data[i, j] = tsbeta['BIC'] + FS['R2Adj'].data[i, j] = tsbeta['R2Adj'] # list of output files output_files = [] # Output spatial files - for i in range(0,ncomp): + for i in range(0, ncomp): # output spatial file name - f1 = (ITERATION, dset_str, gia_str, ocean_str, EXPANSION, - coef_str[i], '', START, END, suffix) + f1 = ( + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + coef_str[i], + '', + START, + END, + suffix, + ) file1 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f1)) # full attributes UNITS_TITLE = f'centimeters{unit_suffix[i]}' @@ -325,44 +362,90 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, FILE_TITLE = f'Sea_Level_Fingerprint_{fit_longname[i]}' # output regression fit and fit error to file output = out.index(i, date=False) - output_data(output, FILENAME=file1, DATAFORM=DATAFORM, - UNITS=UNITS_TITLE, LONGNAME=LONGNAME, TITLE=FILE_TITLE, - CONF=CONF, VERBOSE=VERBOSE, MODE=MODE) + output_data( + output, + FILENAME=file1, + DATAFORM=DATAFORM, + UNITS=UNITS_TITLE, + LONGNAME=LONGNAME, + TITLE=FILE_TITLE, + CONF=CONF, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add output files to list object output_files.append(file1) # if fitting coefficients with seasonal components # output amplitude and phase of cyclical components - for i,flag in enumerate(amp_str): + for i, flag in enumerate(amp_str): # Indice pointing to the cyclical components - j = 1 + ORDER + 2*i + j = 1 + ORDER + 2 * i # Allocating memory for output amplitude and phase amp = dinput.zeros_like() amp.error = np.zeros((nlat, nlon)) ph = dinput.zeros_like() ph.error = np.zeros((nlat, nlon)) # calculating amplitude and phase of spatial field - amp.data[indy,indx],ph.data[indy,indx] = gravtk.time_series.amplitude( - out.data[indy,indx,j], out.data[indy,indx,j+1] + amp.data[indy, indx], ph.data[indy, indx] = ( + gravtk.time_series.amplitude( + out.data[indy, indx, j], out.data[indy, indx, j + 1] + ) ) # convert phase from -180:180 to 0:360 ph.data = np.where( (ph.data < 0) & np.logical_not(ph.mask), ph.data + 360.0, ph.data ) # Amplitude Error - comp1=out.error[indy,indx,j]*out.data[indy,indx,j]/amp.data[indy,indx] - comp2=out.error[indy,indx,j+1]*out.data[indy,indx,j+1]/amp.data[indy,indx] - amp.error[indy,indx] = np.hypot(comp1, comp2) + comp1 = ( + out.error[indy, indx, j] + * out.data[indy, indx, j] + / amp.data[indy, indx] + ) + comp2 = ( + out.error[indy, indx, j + 1] + * out.data[indy, indx, j + 1] + / amp.data[indy, indx] + ) + amp.error[indy, indx] = np.hypot(comp1, comp2) # Phase Error (degrees) - comp1=out.error[indy,indx,j]*out.data[indy,indx,j+1]/(amp.data[indy,indx]**2) - comp2=out.error[indy,indx,j+1]*out.data[indy,indx,j]/(amp.data[indy,indx]**2) - ph.error[indy,indx] = np.degrees(np.hypot(comp1, comp2)) + comp1 = ( + out.error[indy, indx, j] + * out.data[indy, indx, j + 1] + / (amp.data[indy, indx] ** 2) + ) + comp2 = ( + out.error[indy, indx, j + 1] + * out.data[indy, indx, j] + / (amp.data[indy, indx] ** 2) + ) + ph.error[indy, indx] = np.degrees(np.hypot(comp1, comp2)) # output file names for amplitude and phase - f2 = (ITERATION, dset_str, gia_str, ocean_str, EXPANSION, - flag, '_AMPL', START, END, suffix) - f3 = (ITERATION, dset_str, gia_str, ocean_str, EXPANSION, - flag, '_PHASE', START, END, suffix) + f2 = ( + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + flag, + '_AMPL', + START, + END, + suffix, + ) + f3 = ( + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + flag, + '_PHASE', + START, + END, + suffix, + ) file2 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f2)) file3 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f3)) # full attributes @@ -372,12 +455,28 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, AMP_TITLE = f'Sea_Level_Fingerprint_{amp_title[flag]}' PH_TITLE = f'Sea_Level_Fingerprint_{ph_title[flag]}' # Output seasonal amplitude and phase to files - output_data(amp, FILENAME=file2, DATAFORM=DATAFORM, - UNITS=AMP_UNITS, LONGNAME=LONGNAME, TITLE=AMP_TITLE, - CONF=CONF, VERBOSE=VERBOSE, MODE=MODE) - output_data(ph, FILENAME=file3, DATAFORM=DATAFORM, - UNITS=PH_UNITS, LONGNAME='Phase', TITLE=PH_TITLE, - CONF=CONF, VERBOSE=VERBOSE, MODE=MODE) + output_data( + amp, + FILENAME=file2, + DATAFORM=DATAFORM, + UNITS=AMP_UNITS, + LONGNAME=LONGNAME, + TITLE=AMP_TITLE, + CONF=CONF, + VERBOSE=VERBOSE, + MODE=MODE, + ) + output_data( + ph, + FILENAME=file3, + DATAFORM=DATAFORM, + UNITS=PH_UNITS, + LONGNAME='Phase', + TITLE=PH_TITLE, + CONF=CONF, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add output files to list object output_files.append(file2) output_files.append(file3) @@ -389,27 +488,54 @@ def sea_level_regress(PROC, DREL, DSET, LMAX, signif_longname['BIC'] = 'Bayesian information criterion' signif_longname['R2Adj'] = 'Adjusted Coefficient of Determination' # for each fit significance term - for key,fs in FS.items(): + for key, fs in FS.items(): # output file names for fit significance signif_str = f'{key}_' - f4 = (ITERATION, dset_str, gia_str, ocean_str, EXPANSION, - signif_str, coef_str[ORDER], START, END, suffix) + f4 = ( + ITERATION, + dset_str, + gia_str, + ocean_str, + EXPANSION, + signif_str, + coef_str[ORDER], + START, + END, + suffix, + ) file4 = OUTPUT_DIRECTORY.joinpath(output_format.format(*f4)) # full attributes LONGNAME = signif_longname[key] # output fit significance to file - output_data(fs, FILENAME=file4, DATAFORM=DATAFORM, - UNITS=key, LONGNAME=LONGNAME, TITLE=nu, - VERBOSE=VERBOSE, MODE=MODE) + output_data( + fs, + FILENAME=file4, + DATAFORM=DATAFORM, + UNITS=key, + LONGNAME=LONGNAME, + TITLE=nu, + VERBOSE=VERBOSE, + MODE=MODE, + ) # add output files to list object output_files.append(file4) # return the list of output files return output_files + # PURPOSE: wrapper function for outputting data to file -def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, - LONGNAME=None, TITLE=None, CONF=0, VERBOSE=0, MODE=0o775): +def output_data( + data, + FILENAME=None, + DATAFORM=None, + UNITS=None, + LONGNAME=None, + TITLE=None, + CONF=0, + VERBOSE=0, + MODE=0o775, +): # field mapping for output regression data field_mapping = {} field_mapping['lat'] = 'lat' @@ -434,30 +560,43 @@ def output_data(data, FILENAME=None, DATAFORM=None, UNITS=None, attributes['error']['description'] = 'Uncertainty_in_model_fit' attributes['error']['long_name'] = LONGNAME attributes['error']['units'] = UNITS - attributes['error']['confidence'] = 100*CONF + attributes['error']['confidence'] = 100 * CONF # output global attributes REFERENCE = f'Output from {pathlib.Path(sys.argv[0]).name}' # write to output file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) data.to_ascii(FILENAME, date=False, verbose=VERBOSE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netcdf (.nc) - data.to_netCDF4(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) - elif (DATAFORM == 'HDF5'): + data.to_netCDF4( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) - data.to_HDF5(FILENAME, date=False, verbose=VERBOSE, - field_mapping=field_mapping, attributes=attributes, - title=TITLE, reference=REFERENCE) + data.to_HDF5( + FILENAME, + date=False, + verbose=VERBOSE, + field_mapping=field_mapping, + attributes=attributes, + title=TITLE, + reference=REFERENCE, + ) # change the permissions mode of the output file FILENAME.chmod(mode=MODE) + # PURPOSE: print a file log for the sea level regression calculation def output_log_file(input_arguments, output_files): # format: sea_level_regress_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'sea_level_regress_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -474,10 +613,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the sea level regression calculation def output_error_log_file(input_arguments): # format: failed_sea_level_regress_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'failed_sea_level_regress_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -493,48 +633,116 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Reads in sea level grid files and calculates the trends at each grid point following an input regression model """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', + parser.add_argument( + '--start', + '-S', type=int, - help='Starting GRACE/GRACE_FO month for time series regression') - parser.add_argument('--end','-E', + help='Starting GRACE/GRACE_FO month for time series regression', + ) + parser.add_argument( + '--end', + '-E', type=int, - help='Ending GRACE/GRACE_FO month for time series regression') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + help='Ending GRACE/GRACE_FO month for time series regression', + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -550,65 +758,105 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # sea level fingerprint parameters - parser.add_argument('--iteration','-I', - type=int, default=1, - help='Sea level fingerprint iteration') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--iteration', + '-I', + type=int, + default=1, + help='Sea level fingerprint iteration', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # regression parameters # 0: mean # 1: trend # 2: acceleration - parser.add_argument('--order', - type=int, default=2, - help='Regression fit polynomial order') + parser.add_argument( + '--order', type=int, default=2, help='Regression fit polynomial order' + ) # regression fit cyclical terms - parser.add_argument('--cycles', - type=float, default=[0.5,1.0,161.0/365.25], nargs='+', - help='Regression fit cyclical terms') + parser.add_argument( + '--cycles', + type=float, + default=[0.5, 1.0, 161.0 / 365.25], + nargs='+', + help='Regression fit cyclical terms', + ) # land-sea mask for redistributing mascon mass and land water flux - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing mascon mass and land water flux') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing mascon mass and land water flux', + ) # Output log file for each job in forms # sea_level_regress_run_2002-04-01_PID-00000.log # failed_sea_level_regress_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -637,18 +885,20 @@ def main(): LANDMASK=args.mask, OUTPUT_DIRECTORY=args.output_directory, VERBOSE=args.verbose, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/sea_level_stokes.py b/scripts/sea_level_stokes.py index 01c3b1b4..b9b26560 100755 --- a/scripts/sea_level_stokes.py +++ b/scripts/sea_level_stokes.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" sea_level_stokes.py Written by Tyler Sutterley (05/2023) Reads in sea level grid files and converts to spherical harmonics @@ -99,6 +99,7 @@ Updated 08/2017: running at 0.5x0.5 degrees Written 08/2017 """ + from __future__ import print_function import sys @@ -111,6 +112,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -120,8 +122,13 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # program module to run with specified parameters -def sea_level_stokes(PROC, DREL, DSET, LMAX, +def sea_level_stokes( + PROC, + DREL, + DSET, + LMAX, START=None, END=None, MISSING=None, @@ -136,22 +143,23 @@ def sea_level_stokes(PROC, DREL, DSET, LMAX, EXPANSION=None, LANDMASK=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # output directory setup OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): OUTPUT_DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) # GRACE/GRACE-FO months - months = sorted(set(np.arange(START,END+1)) - set(MISSING)) + months = sorted(set(np.arange(START, END + 1)) - set(MISSING)) nmon = len(months) # output filename suffix suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # read load love numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE) + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE + ) # for datasets not GSM: will add a label for the dataset dset_str = '' if (DSET == 'GSM') else f'_{DSET}' @@ -167,9 +175,10 @@ def sea_level_stokes(PROC, DREL, DSET, LMAX, # Land-Sea Mask with Antarctica from Rignot (2017) and Greenland from GEUS # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF4 file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, - varname='LSMASK') - dlon,dlat = landsea.spacing + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) + dlon, dlat = landsea.spacing nlat, nlon = landsea.shape # calculate Fully-Normalized Legendre Polynomials th = np.radians(90.0 - landsea.lat) @@ -177,9 +186,19 @@ def sea_level_stokes(PROC, DREL, DSET, LMAX, # create index file for calc_mascon.py # (spherical harmonics to be removed from the GRACE data) - args = (ITERATION,dset_str,gia_str,ocean_str,LMAX,order_str,START,END) - INDEX = ('SLF_ITERATION_{0}_INDEX{1}{2}{3}_CLM_L{4:d}{5}_' - '{6:03d}-{7:03d}.txt').format(*args) + args = ( + ITERATION, + dset_str, + gia_str, + ocean_str, + LMAX, + order_str, + START, + END, + ) + INDEX = ( + 'SLF_ITERATION_{0}_INDEX{1}{2}{3}_CLM_L{4:d}{5}_{6:03d}-{7:03d}.txt' + ).format(*args) index_file = OUTPUT_DIRECTORY.joinpath(INDEX) fid = index_file.open(mode='w', encoding='utf8') # print the path to the index file @@ -194,38 +213,66 @@ def sea_level_stokes(PROC, DREL, DSET, LMAX, attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' for t in range(0, nmon): # sea level file for month (spatial fields) - SLF = file_format.format(ITERATION, dset_str, gia_str, ocean_str, '', - EXPANSION, '', months[t], suffix[DATAFORM]) + SLF = file_format.format( + ITERATION, + dset_str, + gia_str, + ocean_str, + '', + EXPANSION, + '', + months[t], + suffix[DATAFORM], + ) INPUT_FILE = OUTPUT_DIRECTORY.joinpath(SLF) # read sea level file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(INPUT_FILE, - spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + INPUT_FILE, spacing=[dlon, dlat], nlat=nlat, nlon=nlon + ) + elif DATAFORM == 'netCDF4': # netcdf (.nc) dinput = gravtk.spatial().from_netCDF4(INPUT_FILE) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 (.H5) dinput = gravtk.spatial().from_HDF5(INPUT_FILE) # Converting sea level field into spherical harmonics (can truncate) - Ylms = gravtk.gen_stokes(dinput.data.T, dinput.lon, dinput.lat, - UNITS=1, LMIN=0, LMAX=LMAX, MMAX=MMAX, LOVE=LOVE, PLM=PLM) + Ylms = gravtk.gen_stokes( + dinput.data.T, + dinput.lon, + dinput.lat, + UNITS=1, + LMIN=0, + LMAX=LMAX, + MMAX=MMAX, + LOVE=LOVE, + PLM=PLM, + ) Ylms.time = np.copy(dinput.time) Ylms.month = months[t] # output (truncated) spherical harmonics to file - FILE = file_format.format(ITERATION, dset_str, gia_str, ocean_str, - 'CLM_', LMAX, order_str, months[t], suffix[DATAFORM]) + FILE = file_format.format( + ITERATION, + dset_str, + gia_str, + ocean_str, + 'CLM_', + LMAX, + order_str, + months[t], + suffix[DATAFORM], + ) OUTPUT_FILE = OUTPUT_DIRECTORY.joinpath(FILE) - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) Ylms.to_ascii(OUTPUT_FILE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netcdf (.nc) Ylms.to_netCDF4(OUTPUT_FILE, **attributes) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 (.H5) Ylms.to_HDF5(OUTPUT_FILE, **attributes) # change output file permissions mode to MODE @@ -241,10 +288,11 @@ def sea_level_stokes(PROC, DREL, DSET, LMAX, # return the list of output files return output_files + # PURPOSE: print a file log for the sea level harmonics calculation def output_log_file(input_arguments, output_files): # format: sea_level_stokes_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'sea_level_stokes_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -261,10 +309,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the sea level harmonics calculation def output_error_log_file(input_arguments): # format: failed_sea_level_stokes_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'failed_sea_level_stokes_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -280,65 +329,144 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( description="""Reads in sea level grid files and converts to spherical harmonics """, - fromfile_prefix_chars="@" + fromfile_prefix_chars='@', ) parser.convert_arg_line_to_args = gravtk.utilities.convert_arg_line_to_args # command line parameters - parser.add_argument('--output-directory','-O', + parser.add_argument( + '--output-directory', + '-O', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Output directory for mascon files') + help='Output directory for mascon files', + ) # GRACE/GRACE-FO data processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, required=True, - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + required=True, + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, default='RL06', - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO Level-2 data product - parser.add_argument('--product','-p', - metavar='DSET', type=str, default='GSM', - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str, + default='GSM', + help='GRACE/GRACE-FO Level-2 data product', + ) # maximum spherical harmonic degree and order - parser.add_argument('--lmax','-l', - type=int, default=60, - help='Maximum spherical harmonic degree') - parser.add_argument('--mmax','-m', - type=int, default=None, - help='Maximum spherical harmonic order') + parser.add_argument( + '--lmax', + '-l', + type=int, + default=60, + help='Maximum spherical harmonic degree', + ) + parser.add_argument( + '--mmax', + '-m', + type=int, + default=None, + help='Maximum spherical harmonic order', + ) # start and end GRACE/GRACE-FO months - parser.add_argument('--start','-S', - type=int, default=4, - help='Starting GRACE/GRACE-FO month') - parser.add_argument('--end','-E', - type=int, default=232, - help='Ending GRACE/GRACE-FO month') - MISSING = [6,7,18,109,114,125,130,135,140,141,146,151,156,162,166,167, - 172,177,178,182,187,188,189,190,191,192,193,194,195,196,197,200,201] - parser.add_argument('--missing','-N', - metavar='MISSING', type=int, nargs='+', default=MISSING, - help='Missing GRACE/GRACE-FO months') + parser.add_argument( + '--start', + '-S', + type=int, + default=4, + help='Starting GRACE/GRACE-FO month', + ) + parser.add_argument( + '--end', '-E', type=int, default=232, help='Ending GRACE/GRACE-FO month' + ) + MISSING = [ + 6, + 7, + 18, + 109, + 114, + 125, + 130, + 135, + 140, + 141, + 146, + 151, + 156, + 162, + 166, + 167, + 172, + 177, + 178, + 182, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 200, + 201, + ] + parser.add_argument( + '--missing', + '-N', + metavar='MISSING', + type=int, + nargs='+', + default=MISSING, + help='Missing GRACE/GRACE-FO months', + ) # different treatments of the load Love numbers # 0: Han and Wahr (1995) values from PREM # 1: Gegout (2005) values from PREM # 2: Wang et al. (2012) values from PREM # 3: Wang et al. (2012) values from PREM with hard sediment # 4: Wang et al. (2012) values from PREM with soft sediment - parser.add_argument('--love','-n', - type=int, default=0, choices=[0,1,2,3,4], - help='Treatment of the Load Love numbers') + parser.add_argument( + '--love', + '-n', + type=int, + default=0, + choices=[0, 1, 2, 3, 4], + help='Treatment of the Load Love numbers', + ) # option for setting reference frame for gravitational load love number # reference frame options (CF, CM, CE) - parser.add_argument('--reference', - type=str.upper, default='CF', choices=['CF','CM','CE'], - help='Reference frame for load Love numbers') + parser.add_argument( + '--reference', + type=str.upper, + default='CF', + choices=['CF', 'CM', 'CE'], + help='Reference frame for load Love numbers', + ) # GIA model type list models = {} models['IJ05-R2'] = 'Ivins R2 GIA Models' @@ -354,54 +482,90 @@ def arguments(): models['netCDF4'] = 'reformatted GIA in netCDF4 format' models['HDF5'] = 'reformatted GIA in HDF5 format' # GIA model type - parser.add_argument('--gia','-G', - type=str, metavar='GIA', choices=models.keys(), - help='GIA model type to read') + parser.add_argument( + '--gia', + '-G', + type=str, + metavar='GIA', + choices=models.keys(), + help='GIA model type to read', + ) # full path to GIA file - parser.add_argument('--gia-file', - type=pathlib.Path, - help='GIA file to read') + parser.add_argument( + '--gia-file', type=pathlib.Path, help='GIA file to read' + ) # input data format (ascii, netCDF4, HDF5) - parser.add_argument('--format','-F', - type=str, default='netCDF4', choices=['ascii','netCDF4','HDF5'], - help='Input/output data format') - parser.add_argument('--redistribute-mascons', - default=False, action='store_true', - help='Redistribute mascon mass over the ocean') + parser.add_argument( + '--format', + '-F', + type=str, + default='netCDF4', + choices=['ascii', 'netCDF4', 'HDF5'], + help='Input/output data format', + ) + parser.add_argument( + '--redistribute-mascons', + default=False, + action='store_true', + help='Redistribute mascon mass over the ocean', + ) # sea level fingerprint parameters - parser.add_argument('--iteration','-I', - type=int, default=1, - help='Sea level fingerprint iteration') - parser.add_argument('--expansion','-e', - type=int, default=240, - help='Spherical harmonic expansion for sea level fingerprints') + parser.add_argument( + '--iteration', + '-I', + type=int, + default=1, + help='Sea level fingerprint iteration', + ) + parser.add_argument( + '--expansion', + '-e', + type=int, + default=240, + help='Spherical harmonic expansion for sea level fingerprints', + ) # land-sea mask for redistributing mascon mass and land water flux - lsmask = gravtk.utilities.get_data_path(['data','landsea_hd.nc']) - parser.add_argument('--mask', - type=pathlib.Path, default=lsmask, - help='Land-sea mask for redistributing mascon mass and land water flux') + lsmask = gravtk.utilities.get_data_path(['data', 'landsea_hd.nc']) + parser.add_argument( + '--mask', + type=pathlib.Path, + default=lsmask, + help='Land-sea mask for redistributing mascon mass and land water flux', + ) # Output log file for each job in forms # sea_level_stokes_run_2002-04-01_PID-00000.log # failed_sea_level_stokes_run_2002-04-01_PID-00000.log - parser.add_argument('--log', - default=False, action='store_true', - help='Output log file for each job') + parser.add_argument( + '--log', + default=False, + action='store_true', + help='Output log file for each job', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -430,18 +594,20 @@ def main(): EXPANSION=args.expansion, LANDMASK=args.mask, OUTPUT_DIRECTORY=args.output_directory, - MODE=args.mode) + MODE=args.mode, + ) except Exception as exc: # if there has been an error exception # print the type, value, and stack trace of the # current exception being handled logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) - if args.log:# write failed job completion log file + if args.log: # write failed job completion log file output_error_log_file(args) else: - if args.log:# write successful job completion log file - output_log_file(args,output_files) + if args.log: # write successful job completion log file + output_log_file(args, output_files) + # run main program if __name__ == '__main__': diff --git a/scripts/simple_parallel_shell.py b/scripts/simple_parallel_shell.py index c8fa04a6..a7ce7424 100755 --- a/scripts/simple_parallel_shell.py +++ b/scripts/simple_parallel_shell.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" simple_parallel_shell.py Written by Tyler Sutterley (05/2023) Runs a shell script in parallel using python multiprocessing @@ -21,6 +21,7 @@ Updated 11/2019: remove commented lines from list of commands Written 10/2019 """ + from __future__ import print_function import sys @@ -34,14 +35,16 @@ import subprocess import multiprocessing + # PURPOSE: converts a command line argument into an environment dictionary def argtoenv(arg): output = {} for item in arg.split(','): - key,val = item.split(':') + key, val = item.split(':') output[key] = pathlib.Path(val).expanduser().absolute() return output + # PURPOSE: run command and handle error exceptions def execute_command(comm, shell=False, env=None, verbose=False): # create logger @@ -64,6 +67,7 @@ def execute_command(comm, shell=False, env=None, verbose=False): logging.critical(f'process id {os.getpid():d} failed') logging.error(traceback.format_exc()) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -72,32 +76,49 @@ def arguments(): """ ) # command line parameters - parser.add_argument('files', - type=pathlib.Path, nargs='+', - help='Shell script files to run') + parser.add_argument( + 'files', type=pathlib.Path, nargs='+', help='Shell script files to run' + ) # number of processes to run in parallel - parser.add_argument('--np','-P', - metavar='PROCESSES', type=int, default=0, - help='Number of processes to run in parallel') + parser.add_argument( + '--np', + '-P', + metavar='PROCESSES', + type=int, + default=0, + help='Number of processes to run in parallel', + ) # Mapping of environment variables - parser.add_argument('--environment','-E', + parser.add_argument( + '--environment', + '-E', type=argtoenv, - help='Mapping of environment variables') + help='Mapping of environment variables', + ) # Run specified commands through the shell - parser.add_argument('--shell','-S', - default=False, action='store_true', - help='Run specified commands through the shell') - parser.add_argument('--verbose','-V', - default=False, action='store_true', - help='Verbose output of run') + parser.add_argument( + '--shell', + '-S', + default=False, + action='store_true', + help='Run specified commands through the shell', + ) + parser.add_argument( + '--verbose', + '-V', + default=False, + action='store_true', + help='Verbose output of run', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create list of commands to run from all input shell script files command_list = [] @@ -107,17 +128,18 @@ def main(): input_shell_file = pathlib.Path(fi).expanduser().absolute() with input_shell_file.open(mode='r', encoding='utf-8') as f: # connect lines that have a line continuation before splitting - file_contents = f.read().replace('\r\n','\n').replace('\\\n','') + file_contents = f.read().replace('\r\n', '\n').replace('\\\n', '') command_list.extend(file_contents.splitlines()) # reduce command list to valid and uncommented lines - command_list = [l for l in command_list if re.match(r'^(?!#)(.+?)',l)] + command_list = [l for l in command_list if re.match(r'^(?!#)(.+?)', l)] # run in parallel with multiprocessing Pool pool = multiprocessing.Pool(processes=args.np) # for each command for comm in command_list: - kwds = dict(env=args.environment, shell=args.shell, - verbose=args.verbose) + kwds = dict( + env=args.environment, shell=args.shell, verbose=args.verbose + ) pool.apply_async(execute_command, args=(comm,), kwds=kwds) # start multiprocessing jobs # close the pool @@ -126,6 +148,7 @@ def main(): # exit the completed processes pool.join() + # run main program if __name__ == '__main__': main() diff --git a/scripts/upload_to_figshare.py b/scripts/upload_to_figshare.py index 729ddacc..69eff417 100644 --- a/scripts/upload_to_figshare.py +++ b/scripts/upload_to_figshare.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" upload_to_figshare.py Written by Tyler Sutterley (05/2023) @@ -22,6 +22,7 @@ Updated 05/2022: use argparse descriptions within documentation Written 05/2021 """ + import os import netrc import getpass @@ -30,37 +31,46 @@ import builtins import gravity_toolkit as gravtk + # PURPOSE: upload geocenter files to figshare -def upload_to_figshare(grace_dir,DREL,username=None,password=None, - timeout=None,verbose=False): +def upload_to_figshare( + grace_dir, DREL, username=None, password=None, timeout=None, verbose=False +): # directory setup grace_dir = pathlib.Path(grace_dir).expanduser().absolute() # figshare directory for geocenter products - directory = ('Geocenter Estimates from Time-Variable Gravity ' - 'and Ocean Model Outputs') + directory = ( + 'Geocenter Estimates from Time-Variable Gravity and Ocean Model Outputs' + ) # labels for each processing center input_flags = {} - input_flags['CSR'] = ['SLF_iter','SLF_iter_wAOD'] - input_flags['GFZ'] = ['SLF_iter','SLF_iter_wAOD','SLF_iter_wSLR21'] - input_flags['JPL'] = ['SLF_iter','SLF_iter_wAOD'] + input_flags['CSR'] = ['SLF_iter', 'SLF_iter_wAOD'] + input_flags['GFZ'] = ['SLF_iter', 'SLF_iter_wAOD', 'SLF_iter_wSLR21'] + input_flags['JPL'] = ['SLF_iter', 'SLF_iter_wAOD'] # ocean model labels - model_str = 'OMCT' if DREL in ('RL04','RL05') else 'MPIOM' + model_str = 'OMCT' if DREL in ('RL04', 'RL05') else 'MPIOM' # build list of geocenter files geocenter_files = [] # for each processing center - for PROC,center_flags in input_flags.items(): + for PROC, center_flags in input_flags.items(): # for each data product flag for flag in center_flags: - f = '{0}_{1}_{2}_{3}.txt'.format(PROC,DREL,model_str,flag) + f = '{0}_{1}_{2}_{3}.txt'.format(PROC, DREL, model_str, flag) geocenter_file = grace_dir.joinpath(f) if not geocenter_file.exists(): raise FileNotFoundError(f'Geocenter file {f} not found') else: geocenter_files.append(geocenter_file) # upload geocenter files to figshare - gravtk.utilities.to_figshare(geocenter_files, - username=username,password=password,timeout=timeout, - directory=directory,verbose=verbose) + gravtk.utilities.to_figshare( + geocenter_files, + username=username, + password=password, + timeout=timeout, + directory=directory, + verbose=verbose, + ) + # PURPOSE: create argument parser def arguments(): @@ -70,48 +80,78 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # figshare credentials - parser.add_argument('--user','-U', - type=str, default=os.environ.get('FIGSHARE_FTP_USER'), - help='Username for Figshare ftp login') - parser.add_argument('--password','-W', - type=str, default=os.environ.get('FIGSHARE_PASSWORD'), - help='Password for Figshare ftp login') - parser.add_argument('--netrc','-N', + parser.add_argument( + '--user', + '-U', + type=str, + default=os.environ.get('FIGSHARE_FTP_USER'), + help='Username for Figshare ftp login', + ) + parser.add_argument( + '--password', + '-W', + type=str, + default=os.environ.get('FIGSHARE_PASSWORD'), + help='Password for Figshare ftp login', + ) + parser.add_argument( + '--netrc', + '-N', type=pathlib.Path, default=pathlib.Path.home().joinpath('.netrc'), - help='Path to .netrc file for authentication') + help='Path to .netrc file for authentication', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, - default='RL06', choices=['RL04','RL05','RL06'], - help='GRACE/GRACE-FO data release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + default='RL06', + choices=['RL04', 'RL05', 'RL06'], + help='GRACE/GRACE-FO data release', + ) # connection timeout - parser.add_argument('--timeout','-t', - type=int, default=None, - help='Timeout in seconds for blocking operations') + parser.add_argument( + '--timeout', + '-t', + type=int, + default=None, + help='Timeout in seconds for blocking operations', + ) # verbose will output information about each output file - parser.add_argument('--verbose','-V', - default=False, action='store_true', - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + default=False, + action='store_true', + help='Verbose output of run', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # figshare ftp hostname HOST = 'ftps.figshare.com' # get figshare ftp credentials try: - args.user,_,args.password = netrc.netrc(args.netrc).authenticators(HOST) + args.user, _, args.password = netrc.netrc(args.netrc).authenticators( + HOST + ) except: # check that figshare ftp credentials were entered if not args.user: @@ -123,9 +163,15 @@ def main(): args.password = getpass.getpass(prompt) # run program with parameters - upload_to_figshare(args.directory,args.release, - username=args.user,password=args.password, - timeout=args.timeout,verbose=args.verbose) + upload_to_figshare( + args.directory, + args.release, + username=args.user, + password=args.password, + timeout=args.timeout, + verbose=args.verbose, + ) + # run main program if __name__ == '__main__': diff --git a/setup.py b/setup.py index 40ede658..97837a09 100644 --- a/setup.py +++ b/setup.py @@ -3,10 +3,19 @@ # list of all scripts to be included with package scripts = [] -for dir in ['access','dealiasing','geocenter','mapping','scripts','utilities']: - scripts.extend([os.path.join(dir,f) for f in os.listdir(dir) if f.endswith('.py')]) -scripts.append(os.path.join('gravity_toolkit','grace_date.py')) -scripts.append(os.path.join('gravity_toolkit','grace_months_index.py')) +for dir in [ + 'access', + 'dealiasing', + 'geocenter', + 'mapping', + 'scripts', + 'utilities', +]: + scripts.extend( + [os.path.join(dir, f) for f in os.listdir(dir) if f.endswith('.py')] + ) +scripts.append(os.path.join('gravity_toolkit', 'grace_date.py')) +scripts.append(os.path.join('gravity_toolkit', 'grace_months_index.py')) setup( name='gravity-toolkit', diff --git a/utilities/make_grace_index.py b/utilities/make_grace_index.py index d4d2ecac..7ee24987 100644 --- a/utilities/make_grace_index.py +++ b/utilities/make_grace_index.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" make_grace_index.py Written by Tyler Sutterley (10/2023) Creates index files of GRACE/GRACE-FO Level-2 data @@ -32,6 +32,7 @@ Updated 08/2022: make the data product optional Written 08/2022 """ + from __future__ import print_function import sys @@ -40,14 +41,15 @@ import pathlib import gravity_toolkit as gravtk -# PURPOSE: Creates index files of GRACE/GRACE-FO data -def make_grace_index(DIRECTORY, PROC=[], DREL=[], DSET=[], - VERSION=[], MODE=None): +# PURPOSE: Creates index files of GRACE/GRACE-FO data +def make_grace_index( + DIRECTORY, PROC=[], DREL=[], DSET=[], VERSION=[], MODE=None +): # input directory setup DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() # mission shortnames - shortname = {'grace':'GRAC', 'grace-fo':'GRFO'} + shortname = {'grace': 'GRAC', 'grace-fo': 'GRFO'} # GRACE/GRACE-FO level-2 spherical harmonic products logging.info('GRACE/GRACE-FO L2 Global Spherical Harmonics:') # for each processing center (CSR, GFZ, JPL) @@ -57,22 +59,24 @@ def make_grace_index(DIRECTORY, PROC=[], DREL=[], DSET=[], # for each level-2 product for ds in DSET: # local directory for exact data product - local_dir = DIRECTORY.joinpath( pr, rl, ds) + local_dir = DIRECTORY.joinpath(pr, rl, ds) # check if local directory exists if not local_dir.exists(): continue # list of GRACE/GRACE-FO files for index grace_files = [] # for each satellite mission (grace, grace-fo) - for i,mi in enumerate(['grace','grace-fo']): + for i, mi in enumerate(['grace', 'grace-fo']): # print string of exact data product logging.info(f'{mi} {pr}/{rl}/{ds}') # regular expression operator for data product - rx = gravtk.utilities.compile_regex_pattern(pr, rl, ds, - mission=shortname[mi], version=VERSION[i]) + rx = gravtk.utilities.compile_regex_pattern( + pr, rl, ds, mission=shortname[mi], version=VERSION[i] + ) # find local GRACE/GRACE-FO files to create index - granules = [f.name for f in local_dir.iterdir() - if rx.match(f.name)] + granules = [ + f.name for f in local_dir.iterdir() if rx.match(f.name) + ] # extend list of GRACE/GRACE-FO files grace_files.extend(granules) @@ -86,6 +90,7 @@ def make_grace_index(DIRECTORY, PROC=[], DREL=[], DSET=[], # change permissions of index file index_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -95,55 +100,95 @@ def arguments(): ) # command line parameters # # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO processing center - parser.add_argument('--center','-c', - metavar='PROC', type=str, nargs='+', - default=['CSR','GFZ','JPL'], choices=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO processing center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + nargs='+', + default=['CSR', 'GFZ', 'JPL'], + choices=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO processing center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, nargs='+', + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + nargs='+', default=['RL06'], - help='GRACE/GRACE-FO data release') + help='GRACE/GRACE-FO data release', + ) # GRACE/GRACE-FO data product - parser.add_argument('--product','-p', - metavar='DSET', type=str.upper, nargs='+', - default=['GSM'], choices=['GAA','GAB','GAC','GAD','GSM'], - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str.upper, + nargs='+', + default=['GSM'], + choices=['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + help='GRACE/GRACE-FO Level-2 data product', + ) # GRACE/GRACE-FO data version - parser.add_argument('--version','-v', - metavar='VERSION', type=str, nargs=2, - default=['0','1'], - help='GRACE/GRACE-FO Level-2 data version') + parser.add_argument( + '--version', + '-v', + metavar='VERSION', + type=str, + nargs=2, + default=['0', '1'], + help='GRACE/GRACE-FO Level-2 data version', + ) # verbose will output information about each output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of processing run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of processing run', + ) # permissions mode of the directories and files synced (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permission mode of files created') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permission mode of files created', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] logging.basicConfig(level=loglevels[args.verbose]) # run program with parameters - make_grace_index(args.directory, PROC=args.center, - DREL=args.release, DSET=args.product, - VERSION=args.version, MODE=args.mode) + make_grace_index( + args.directory, + PROC=args.center, + DREL=args.release, + DSET=args.product, + VERSION=args.version, + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/utilities/nominal_grace_date.py b/utilities/nominal_grace_date.py index 73746842..24fa2c25 100755 --- a/utilities/nominal_grace_date.py +++ b/utilities/nominal_grace_date.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" nominal_grace_date.py (05/2023) Creates a GRACE/GRACE-FO date file using nominal calendar dates @@ -42,6 +42,7 @@ import numpy as np import gravity_toolkit as gravtk + def nominal_grace_date(base_dir, DREL, RANGE=None, MODE=0o775): # output to AOD1B directory PROC, DSET = ('AOD1B', 'GSM') @@ -55,29 +56,34 @@ def nominal_grace_date(base_dir, DREL, RANGE=None, MODE=0o775): grace_date_file = grace_dir.joinpath(f'{PROC}_{DREL}_DATES.txt') fid = grace_date_file.open(mode='w', encoding='utf8') # date file header information - a = ('Mid-date','Month','Start_Day','End_Day','Total_Days') - print('{0} {1:>10} {2:>11} {3:>10} {4:>13}'.format(*a),file=fid) + a = ('Mid-date', 'Month', 'Start_Day', 'End_Day', 'Total_Days') + print('{0} {1:>10} {2:>11} {3:>10} {4:>13}'.format(*a), file=fid) # for each year in the range count = 0 - for yr in range(RANGE[0], RANGE[1]+1): + for yr in range(RANGE[0], RANGE[1] + 1): # cumulative days per month (check if year is a leap year) - if ((np.int64(yr) % 4) == 0): + if (np.int64(yr) % 4) == 0: # Leap Year - cdays = [1,32,61,92,122,153,183,214,245,275,306,336,367] + cdays = [1, 32, 61, 92, 122, 153, 183, 214, 245, 275, 306, 336, 367] else: # Standard Year - cdays = [1,32,60,91,121,152,182,213,244,274,305,335,366] + cdays = [1, 32, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366] # for each month - for m in range(0,12): + for m in range(0, 12): # calculate year decimal and GRACE month - tdec, = gravtk.time.convert_calendar_decimal(yr,m+1) - grace_month = gravtk.time.calendar_to_grace(yr,m+1) - number_of_days = cdays[m+1] - cdays[m] - print((f'{tdec:13.8f} {grace_month:03d} ' - f'{yr:8.0f} {cdays[m]:03.0f} ' - f'{yr:8.0f} {cdays[m+1]-1:03.0f} ' - f'{count:8.0f}'), file=fid) + (tdec,) = gravtk.time.convert_calendar_decimal(yr, m + 1) + grace_month = gravtk.time.calendar_to_grace(yr, m + 1) + number_of_days = cdays[m + 1] - cdays[m] + print( + ( + f'{tdec:13.8f} {grace_month:03d} ' + f'{yr:8.0f} {cdays[m]:03.0f} ' + f'{yr:8.0f} {cdays[m + 1] - 1:03.0f} ' + f'{count:8.0f}' + ), + file=fid, + ) # add to count count += number_of_days # close the date file @@ -85,6 +91,7 @@ def nominal_grace_date(base_dir, DREL, RANGE=None, MODE=0o775): # change the permissions mode of the output file grace_date_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -93,41 +100,65 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, nargs='+', + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + nargs='+', default=['RL06'], - help='GRACE/GRACE-FO Data Release') + help='GRACE/GRACE-FO Data Release', + ) # start and end year to calculate nominal dates - parser.add_argument('--year','-Y', - metavar=('start','end'), type=int, nargs=2, - default=[2002,2023], - help='Year range to run') + parser.add_argument( + '--year', + '-Y', + metavar=('start', 'end'), + type=int, + nargs=2, + default=[2002, 2023], + help='Year range to run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run program for each release for DREL in args.release: - nominal_grace_date(args.directory, DREL, - RANGE=args.year, MODE=args.mode) + nominal_grace_date( + args.directory, DREL, RANGE=args.year, MODE=args.mode + ) + # run main program if __name__ == '__main__': diff --git a/utilities/quick_mascon_plot.py b/utilities/quick_mascon_plot.py index a2fc9336..ac136a90 100644 --- a/utilities/quick_mascon_plot.py +++ b/utilities/quick_mascon_plot.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" quick_mascon_plot.py Written by Tyler Sutterley (06/2024) Plots a mascon time series file for a particular format @@ -54,6 +54,7 @@ Updated 11/2019: using getopt to set parameters. use figure tight layout Written 10/2019 """ + import sys import pathlib import argparse @@ -63,19 +64,22 @@ # attempt imports plt = gravtk.utilities.import_dependency('matplotlib.pyplot') + # PURPOSE: read mascon time series file and create plot -def run_plot(i, input_file, - individual=False, - header=0, - marker=None, - zorder=None, - title=None, - time=None, - units=None, - legend=None, - error=False, - monthly=True - ): +def run_plot( + i, + input_file, + individual=False, + header=0, + marker=None, + zorder=None, + title=None, + time=None, + units=None, + legend=None, + error=False, + monthly=True, +): """ Plots a mascon time series file for a particular format @@ -99,68 +103,74 @@ def run_plot(i, input_file, """ # if creating individual plots if individual: - plt.figure(i+1) + plt.figure(i + 1) # read data input_file = pathlib.Path(input_file).expanduser().absolute() print(input_file.name) dinput = np.loadtxt(input_file, skiprows=header) # calculate regression - TERMS = gravtk.time_series.aliasing_terms(dinput[:,1]) - x1 = gravtk.time_series.regress(dinput[:,1],dinput[:,2],ORDER=1, - CYCLES=[0.5,1.0], TERMS=TERMS) - x2 = gravtk.time_series.regress(dinput[:,1],dinput[:,2],ORDER=2, - CYCLES=[0.5,1.0], TERMS=TERMS) + TERMS = gravtk.time_series.aliasing_terms(dinput[:, 1]) + x1 = gravtk.time_series.regress( + dinput[:, 1], dinput[:, 2], ORDER=1, CYCLES=[0.5, 1.0], TERMS=TERMS + ) + x2 = gravtk.time_series.regress( + dinput[:, 1], dinput[:, 2], ORDER=2, CYCLES=[0.5, 1.0], TERMS=TERMS + ) # print regression coefficients - args = ('x1',x1['beta'][1],x1['error'][1]) + args = ('x1', x1['beta'][1], x1['error'][1]) print('{0}: {1:0.4f} +/- {2:0.4f}'.format(*args)) - args = ('x2',2.0*x2['beta'][2],2.0*x2['error'][2]) + args = ('x2', 2.0 * x2['beta'][2], 2.0 * x2['error'][2]) print('{0}: {1:0.4f} +/- {2:0.4f}'.format(*args)) - args = ('AIC',x2['AIC']-x1['AIC']) + args = ('AIC', x2['AIC'] - x1['AIC']) print('{0}: {1:0.4f}'.format(*args)) # plot all data or monthly data with gap if monthly: # calculate months and remove GAP from missing - START_MON,END_MON = (dinput[0,0],dinput[-1,0]) - all_months = np.arange(START_MON,END_MON+1,dtype=np.int64) - GAP = [187,188,189,190,191,192,193,194,195,196,197] - MISSING = sorted(set(all_months) - set(dinput[:,0]) - set(GAP)) + START_MON, END_MON = (dinput[0, 0], dinput[-1, 0]) + all_months = np.arange(START_MON, END_MON + 1, dtype=np.int64) + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] + MISSING = sorted(set(all_months) - set(dinput[:, 0]) - set(GAP)) months = sorted(set(all_months) - set(MISSING)) # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) if error: - err = np.full_like(months,np.nan,dtype=np.float64) - for t,m in enumerate(months): - valid = np.count_nonzero(dinput[:,0] == m) + err = np.full_like(months, np.nan, dtype=np.float64) + for t, m in enumerate(months): + valid = np.count_nonzero(dinput[:, 0] == m) if valid: - mm, = np.nonzero(dinput[:,0] == m) - tdec[t] = dinput[mm,1] - data[t] = dinput[mm,2] - dinput[0,2] + (mm,) = np.nonzero(dinput[:, 0] == m) + tdec[t] = dinput[mm, 1] + data[t] = dinput[mm, 2] - dinput[0, 2] if error: - err[t] = dinput[mm,3] + err[t] = dinput[mm, 3] else: - tdec = np.copy(dinput[:,1]) - data = np.copy(dinput[:,2]) - dinput[0,2] + tdec = np.copy(dinput[:, 1]) + data = np.copy(dinput[:, 2]) - dinput[0, 2] if error: - err = np.copy(dinput[:,3]) + err = np.copy(dinput[:, 3]) # plot all dates - l, = plt.plot(tdec, data, - label=legend, - marker=marker, - markersize=5, - zorder=zorder + (l,) = plt.plot( + tdec, data, label=legend, marker=marker, markersize=5, zorder=zorder ) # add estimated errors if error: - plt.fill_between(tdec, data-err, y2=data+err, - color=l.get_color(), alpha=0.25, zorder=zorder) + plt.fill_between( + tdec, + data - err, + y2=data + err, + color=l.get_color(), + alpha=0.25, + zorder=zorder, + ) # vertical lines for end of the GRACE mission and start of GRACE-FO if monthly & ((i == 0) | individual): - jj, = np.flatnonzero(dinput[:,0] == 186) - kk, = np.flatnonzero(dinput[:,0] == 198) - vs = plt.gca().axvspan(dinput[jj,1],dinput[kk,1], - color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (4,3) + (jj,) = np.flatnonzero(dinput[:, 0] == 186) + (kk,) = np.flatnonzero(dinput[:, 0] == 198) + vs = plt.gca().axvspan( + dinput[jj, 1], dinput[kk, 1], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (4, 3) # if on the first axes or creating individual plots if (i == 0) | individual: # add labels @@ -170,6 +180,7 @@ def run_plot(i, input_file, # use a tight layout to minimize whitespace plt.tight_layout() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -177,72 +188,130 @@ def arguments(): """ ) # command line parameters - parser.add_argument('file', - type=pathlib.Path, nargs='+', - help='Mascon data files') - parser.add_argument('--individual','-I', - default=False, action='store_true', - help='Create individual plots or combine into single') + parser.add_argument( + 'file', type=pathlib.Path, nargs='+', help='Mascon data files' + ) + parser.add_argument( + '--individual', + '-I', + default=False, + action='store_true', + help='Create individual plots or combine into single', + ) # output filename, format and dpi - parser.add_argument('--output-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='pdf', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') - parser.add_argument('--header','-H', - type=int, default=0, - help='Number of rows of header text to skip') - parser.add_argument('--marker','-m', - type=str, help='Plot marker') - parser.add_argument('--zorder','-z', - type=int, nargs='+', - help='Drawing order for each time series') - parser.add_argument('--title','-t', - type=lambda x: ' '.join(str.split(x,"_")), - help='Plot title') - parser.add_argument('--time','-T', - type=str, default='Time [Yr]', - help='Time label for x-axis') - parser.add_argument('--units','-U', - type=str, default='Mass [Gt]', - help='Units label for y-axis') - parser.add_argument('--legend','-L', - type=str, nargs='+', - help='Legend labels for each time series') - parser.add_argument('--error','-E', - default=False, action='store_true', - help='Plot mascon errors') - parser.add_argument('--all','-A', - default=True, action='store_false', - help='Plot all data without data gap') + parser.add_argument( + '--output-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='pdf', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) + parser.add_argument( + '--header', + '-H', + type=int, + default=0, + help='Number of rows of header text to skip', + ) + parser.add_argument('--marker', '-m', type=str, help='Plot marker') + parser.add_argument( + '--zorder', + '-z', + type=int, + nargs='+', + help='Drawing order for each time series', + ) + parser.add_argument( + '--title', + '-t', + type=lambda x: ' '.join(str.split(x, '_')), + help='Plot title', + ) + parser.add_argument( + '--time', + '-T', + type=str, + default='Time [Yr]', + help='Time label for x-axis', + ) + parser.add_argument( + '--units', + '-U', + type=str, + default='Mass [Gt]', + help='Units label for y-axis', + ) + parser.add_argument( + '--legend', + '-L', + type=str, + nargs='+', + help='Legend labels for each time series', + ) + parser.add_argument( + '--error', + '-E', + default=False, + action='store_true', + help='Plot mascon errors', + ) + parser.add_argument( + '--all', + '-A', + default=True, + action='store_false', + help='Plot all data without data gap', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run plot program for each input file - for i,f in enumerate(args.file): + for i, f in enumerate(args.file): legend = args.legend[i] if args.legend else None zorder = args.zorder[i] if args.zorder else None - run_plot(i, f, individual=args.individual, header=args.header, - marker=args.marker, title=args.title, time=args.time, - units=args.units, legend=legend, error=args.error, - monthly=args.all, zorder=zorder) + run_plot( + i, + f, + individual=args.individual, + header=args.header, + marker=args.marker, + title=args.title, + time=args.time, + units=args.units, + legend=legend, + error=args.error, + monthly=args.all, + zorder=zorder, + ) # add legend if applicable if args.legend: - lgd = plt.legend(loc=3,frameon=False) + lgd = plt.legend(loc=3, frameon=False) lgd.get_frame().set_alpha(1.0) for line in lgd.get_lines(): line.set_linewidth(6) @@ -253,19 +322,21 @@ def main(): fig = plt.figure(num) output = f'{args.output_file.stem}_{num}{args.output_file.suffix}' # save the figure file - fig.savefig(output, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, + fig.savefig( + output, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, dpi=args.figure_dpi, - format=args.figure_format + format=args.figure_format, ) # change the permissions mode output.chmod(mode=args.mode) elif args.output_file: # save the figure file - plt.savefig(args.output_file, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, + plt.savefig( + args.output_file, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, dpi=args.figure_dpi, - format=args.figure_format + format=args.figure_format, ) # change the permissions mode args.output_file.chmod(mode=args.mode) @@ -277,6 +348,7 @@ def main(): plt.clf() plt.close() + # run main program if __name__ == '__main__': main() diff --git a/utilities/quick_mascon_regress.py b/utilities/quick_mascon_regress.py index 77524d39..59dcbc71 100755 --- a/utilities/quick_mascon_regress.py +++ b/utilities/quick_mascon_regress.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" quick_mascon_regress.py Written by Tyler Sutterley (07/2026) Creates a regression summary file for a mascon time series file @@ -33,6 +33,7 @@ Updated 05/2022: use argparse descriptions within documentation Written 10/2021 """ + from __future__ import print_function import sys @@ -41,15 +42,17 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: Creates regression summary files for mascon files -def run_regress(input_file, - header=0, - order=None, - breakpoint=None, - cycles=None, - units=None, - stream=False - ): +def run_regress( + input_file, + header=0, + order=None, + breakpoint=None, + cycles=None, + units=None, + stream=False, +): """ Creates a regression summary file for a mascon time series file @@ -74,7 +77,7 @@ def run_regress(input_file, # fitting with either piecewise or polynomial regression if breakpoint is not None: # Setting output parameters for piecewise fit - breakpoint_index, = np.nonzero(dinput[:,0] == breakpoint) + (breakpoint_index,) = np.nonzero(dinput[:, 0] == breakpoint) cycle_index = 3 coef_str = ['x0', 'px1', 'px1'] unit_suffix = ['', ' yr^-1', ' yr^-1'] @@ -83,8 +86,10 @@ def run_regress(input_file, elif order is not None: # Setting output parameters for each fit type cycle_index = 1 + order - coef_str = ['x{0:d}'.format(o) for o in range(order+1)] - unit_suffix = [' yr^{0:d}'.format(-o) if o else '' for o in range(order+1)] + coef_str = ['x{0:d}'.format(o) for o in range(order + 1)] + unit_suffix = [ + ' yr^{0:d}'.format(-o) if o else '' for o in range(order + 1) + ] # output regression filename output_file = f'{input_file.stem}_x{order:d}_SUMMARY.txt' else: @@ -115,30 +120,33 @@ def run_regress(input_file, # extra terms for tidal aliasing components or custom fits terms = [] term_index = [] - for i,c in enumerate(cycles): + for i, c in enumerate(cycles): # check if fitting with semi-annual or annual terms - if (c == 0.5): - coef_str.extend(['SS','SC']) + if c == 0.5: + coef_str.extend(['SS', 'SC']) amp_str.append('SEMI') - unit_suffix.extend(['','']) - elif (c == 1.0): - coef_str.extend(['AS','AC']) + unit_suffix.extend(['', '']) + elif c == 1.0: + coef_str.extend(['AS', 'AC']) amp_str.append('ANN') - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # check if fitting with tidal aliasing terms - for t,period in tidal_aliasing.items(): - if np.isclose(c, (period/365.25)): + for t, period in tidal_aliasing.items(): + if np.isclose(c, (period / 365.25)): # terms for tidal aliasing during GRACE and GRACE-FO periods - terms.extend(gravtk.time_series.aliasing_terms(dinput[:,1], - period=period)) + terms.extend( + gravtk.time_series.aliasing_terms( + dinput[:, 1], period=period + ) + ) # labels for tidal aliasing during GRACE period coef_str.extend([f'{t}SGRC', f'{t}CGRC']) amp_str.append(f'{t}GRC') - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # labels for tidal aliasing during GRACE-FO period coef_str.extend([f'{t}SGFO', f'{t}CGFO']) amp_str.append(f'{t}GFO') - unit_suffix.extend(['','']) + unit_suffix.extend(['', '']) # index to remove the original tidal aliasing term term_index.append(i) # remove the original tidal aliasing terms @@ -146,13 +154,19 @@ def run_regress(input_file, # calculate regression if breakpoint is not None: - fit = gravtk.time_series.piecewise(dinput[:,1], dinput[:,2], - BREAKPOINT=breakpoint_index, CYCLES=cycles, TERMS=terms) + fit = gravtk.time_series.piecewise( + dinput[:, 1], + dinput[:, 2], + BREAKPOINT=breakpoint_index, + CYCLES=cycles, + TERMS=terms, + ) elif order is not None: - fit = gravtk.time_series.regress(dinput[:,1], dinput[:,2], - ORDER=order, CYCLES=cycles, TERMS=terms) + fit = gravtk.time_series.regress( + dinput[:, 1], dinput[:, 2], ORDER=order, CYCLES=cycles, TERMS=terms + ) # Fitting seasonal components - ncycles = 2*len(cycles) + len(terms) + ncycles = 2 * len(cycles) + len(terms) # Print output to regression summary file # Summary filename @@ -160,63 +174,76 @@ def run_regress(input_file, # Regression Formula with Correlation Structure if Applicable print('Regression Formula: {0}\n'.format('+'.join(coef_str)), file=fid) # Value, Error and Statistical Significance - args = ('Coef.','Estimate','Std. Error','95% Conf.','Units') + args = ('Coef.', 'Estimate', 'Std. Error', '95% Conf.', 'Units') fid.write('{0:5}\t{1:12}\t{2:12}\t{3:12}\t{4:10}\n'.format(*args)) - print(64*'-',file=fid) - for i,b in enumerate(fit['beta']): - args=(coef_str[i],b,fit['std_err'][i],fit['error'][i],units+unit_suffix[i]) - fid.write('{0:5}\t{1:12.4f}\t{2:12.4f}\t{3:12.4f}\t{4:10}\n'.format(*args)) + print(64 * '-', file=fid) + for i, b in enumerate(fit['beta']): + args = ( + coef_str[i], + b, + fit['std_err'][i], + fit['error'][i], + units + unit_suffix[i], + ) + fid.write( + '{0:5}\t{1:12.4f}\t{2:12.4f}\t{3:12.4f}\t{4:10}\n'.format(*args) + ) # allocate for amplitudes and phases of cyclical components - amp,ph = ({},{}) - for comp in ['beta','std_err','error']: - amp[comp] = np.zeros((ncycles//2)) - ph[comp] = np.zeros((ncycles//2)) + amp, ph = ({}, {}) + for comp in ['beta', 'std_err', 'error']: + amp[comp] = np.zeros((ncycles // 2)) + ph[comp] = np.zeros((ncycles // 2)) # calculate amplitudes and phases of cyclical components for i, flag in enumerate(amp_str): # indice pointing to the cyclical components - j = cycle_index + 2*i - amp['beta'][i],ph['beta'][i] = gravtk.time_series.amplitude( - fit['beta'][j], fit['beta'][j+1] + j = cycle_index + 2 * i + amp['beta'][i], ph['beta'][i] = gravtk.time_series.amplitude( + fit['beta'][j], fit['beta'][j + 1] ) # convert phase from -180:180 to 0:360 - if (ph['beta'][i] < 0): + if ph['beta'][i] < 0: ph['beta'][i] += 360.0 # calculate standard error and 95% confidences - for err in ['std_err','error']: + for err in ['std_err', 'error']: # Amplitude Errors - comp1 = fit[err][j]*fit['beta'][j]/amp['beta'][i] - comp2 = fit[err][j+1]*fit['beta'][j+1]/amp['beta'][i] + comp1 = fit[err][j] * fit['beta'][j] / amp['beta'][i] + comp2 = fit[err][j + 1] * fit['beta'][j + 1] / amp['beta'][i] amp[err][i] = np.hypot(comp1, comp2) # Phase Error (degrees) - comp1 = fit[err][j]*fit['beta'][j+1]/(amp['beta'][i]**2) - comp2 = fit[err][j+1]*fit['beta'][j]/(amp['beta'][i]**2) + comp1 = fit[err][j] * fit['beta'][j + 1] / (amp['beta'][i] ** 2) + comp2 = fit[err][j + 1] * fit['beta'][j] / (amp['beta'][i] ** 2) ph[err][i] = np.degrees(np.hypot(comp1, comp2)) # Amplitude, Error and Statistical Significance - args = ('Ampl.','Estimate','Std. Error','95% Conf.','Units') + args = ('Ampl.', 'Estimate', 'Std. Error', '95% Conf.', 'Units') fid.write('\n{0:5}\t{1:12}\t{2:12}\t{3:12}\t{4:10}\n'.format(*args)) - print(64*'-',file=fid) - for i,b in enumerate(amp['beta']): - args=(amp_str[i],b,amp['std_err'][i],amp['error'][i],units) - fid.write('{0:5}\t{1:12.4f}\t{2:12.4f}\t{3:12.4f}\t{4:10}\n'.format(*args)) + print(64 * '-', file=fid) + for i, b in enumerate(amp['beta']): + args = (amp_str[i], b, amp['std_err'][i], amp['error'][i], units) + fid.write( + '{0:5}\t{1:12.4f}\t{2:12.4f}\t{3:12.4f}\t{4:10}\n'.format(*args) + ) # Phase, Error and Statistical Significance - args = ('Phase','Estimate','Std. Error','95% Conf.','Units') + args = ('Phase', 'Estimate', 'Std. Error', '95% Conf.', 'Units') fid.write('\n{0:5}\t{1:12}\t{2:12}\t{3:12}\t{4:10}\n'.format(*args)) - print(64*'-',file=fid) - for i,b in enumerate(ph['beta']): - args=(amp_str[i],b,ph['std_err'][i],ph['error'][i],'Degree') - fid.write('{0:5}\t{1:12.4f}\t{2:12.4f}\t{3:12.4f}\t{4:10}\n'.format(*args)) + print(64 * '-', file=fid) + for i, b in enumerate(ph['beta']): + args = (amp_str[i], b, ph['std_err'][i], ph['error'][i], 'Degree') + fid.write( + '{0:5}\t{1:12.4f}\t{2:12.4f}\t{3:12.4f}\t{4:10}\n'.format(*args) + ) # Fit Significance Criteria print('\nFit Criterion', file=fid) - print(64*'-',file=fid) + print(64 * '-', file=fid) fid.write('{0}: {1:d}\n'.format('DOF', fit['DOF'])) - for fitstat in ['AIC','BIC','LOGLIK','MSE','NRMSE','R2','R2Adj']: + for fitstat in ['AIC', 'BIC', 'LOGLIK', 'MSE', 'NRMSE', 'R2', 'R2Adj']: fid.write('{0}: {1:f}\n'.format(fitstat, fit[fitstat])) # close the output file fid.write('\n') if stream else fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -226,48 +253,70 @@ def arguments(): ) group = parser.add_mutually_exclusive_group(required=True) # command line parameters - parser.add_argument('file', - type=pathlib.Path, nargs='+', - help='Mascon data files') - parser.add_argument('--header','-H', - type=int, default=0, - help='Number of rows of header text to skip') + parser.add_argument( + 'file', type=pathlib.Path, nargs='+', help='Mascon data files' + ) + parser.add_argument( + '--header', + '-H', + type=int, + default=0, + help='Number of rows of header text to skip', + ) # regression parameters # 0: mean # 1: trend # 2: acceleration - group.add_argument('--order', - type=int, - help='Regression fit polynomial order') + group.add_argument( + '--order', type=int, help='Regression fit polynomial order' + ) # breakpoint month for piecewise regression - group.add_argument('--breakpoint', + group.add_argument( + '--breakpoint', type=int, - help='Breakpoint GRACE/GRACE-FO month for piecewise regression') + help='Breakpoint GRACE/GRACE-FO month for piecewise regression', + ) # regression fit cyclical terms - parser.add_argument('--cycles', - type=float, default=[0.5,1.0,161.0/365.25], nargs='+', - help='Regression fit cyclical terms') - parser.add_argument('--units','-U', - type=str, default='Gt', - help='Units of input data') + parser.add_argument( + '--cycles', + type=float, + default=[0.5, 1.0, 161.0 / 365.25], + nargs='+', + help='Regression fit cyclical terms', + ) + parser.add_argument( + '--units', '-U', type=str, default='Gt', help='Units of input data' + ) # stream to sys.stdout - parser.add_argument('--stream','-S', - default=False, action='store_true', - help='Stream regression summary to standard output') + parser.add_argument( + '--stream', + '-S', + default=False, + action='store_true', + help='Stream regression summary to standard output', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run plot program for each input file - for i,f in enumerate(args.file): - run_regress(f, header=args.header, order=args.order, - breakpoint=args.breakpoint, cycles=args.cycles, - units=args.units, stream=args.stream) + for i, f in enumerate(args.file): + run_regress( + f, + header=args.header, + order=args.order, + breakpoint=args.breakpoint, + cycles=args.cycles, + units=args.units, + stream=args.stream, + ) + # run main program if __name__ == '__main__': diff --git a/utilities/run_grace_date.py b/utilities/run_grace_date.py index 7945dfb4..6d834295 100755 --- a/utilities/run_grace_date.py +++ b/utilities/run_grace_date.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" run_grace_date.py Written by Tyler Sutterley (05/2023) @@ -68,6 +68,7 @@ Updated 02/2014: minor update to if statements Written 07/2012 """ + from __future__ import print_function import sys @@ -76,6 +77,7 @@ import argparse import gravity_toolkit as gravtk + def run_grace_date(base_dir, PROC, DREL, VERBOSE=0, MODE=0o775): # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -85,20 +87,27 @@ def run_grace_date(base_dir, PROC, DREL, VERBOSE=0, MODE=0o775): DSET = {} VALID = {} # CSR RL04/5/6 at LMAX 60 - DSET['CSR'] = {'RL04':['GAC', 'GAD', 'GSM'], 'RL05':['GAC', 'GAD', 'GSM'], - 'RL06':['GAC', 'GAD', 'GSM']} - VALID['CSR'] = ['RL04','RL05','RL06'] + DSET['CSR'] = { + 'RL04': ['GAC', 'GAD', 'GSM'], + 'RL05': ['GAC', 'GAD', 'GSM'], + 'RL06': ['GAC', 'GAD', 'GSM'], + } + VALID['CSR'] = ['RL04', 'RL05', 'RL06'] # GFZ RL04/5 at LMAX 90 # GFZ RL06 at LMAX 60 - DSET['GFZ'] = {'RL04':['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], - 'RL05':['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], - 'RL06':['GAA', 'GAB', 'GAC', 'GAD', 'GSM']} - VALID['GFZ'] = ['RL04','RL05','RL06'] + DSET['GFZ'] = { + 'RL04': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + 'RL05': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + 'RL06': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + } + VALID['GFZ'] = ['RL04', 'RL05', 'RL06'] # JPL RL04/5/6 at LMAX 60 - DSET['JPL'] = {'RL04':['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], - 'RL05':['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], - 'RL06':['GAA', 'GAB', 'GAC', 'GAD', 'GSM']} - VALID['JPL'] = ['RL04','RL05','RL06'] + DSET['JPL'] = { + 'RL04': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + 'RL05': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + 'RL06': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + } + VALID['JPL'] = ['RL04', 'RL05', 'RL06'] # for each processing center for p in PROC: @@ -109,13 +118,15 @@ def run_grace_date(base_dir, PROC, DREL, VERBOSE=0, MODE=0o775): for d in DSET[p][r]: logging.info(f'GRACE Date Program: {p} {r} {d}') # create GRACE/GRACE-FO date index file - gravtk.grace_date(base_dir, PROC=p, DREL=r, DSET=d, - OUTPUT=True, MODE=MODE) + gravtk.grace_date( + base_dir, PROC=p, DREL=r, DSET=d, OUTPUT=True, MODE=MODE + ) # run GRACE/GRACE-FO months program for data releases logging.info('GRACE Months Program') gravtk.grace_months_index(base_dir, DREL=DREL, MODE=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -125,41 +136,69 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', + parser.add_argument( + '--directory', + '-D', type=pathlib.Path, default=gravtk.utilities.get_cache_path(ensure_exists=False), - help='Working data directory') + help='Working data directory', + ) # Data processing center or satellite mission - parser.add_argument('--center','-c', - metavar='PROC', type=str, nargs='+', - default=['CSR','GFZ','JPL'], - choices=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + nargs='+', + default=['CSR', 'GFZ', 'JPL'], + choices=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, nargs='+', - default=['RL06','v02.4'], - help='GRACE/GRACE-FO Data Release') + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + nargs='+', + default=['RL06', 'v02.4'], + help='GRACE/GRACE-FO Data Release', + ) # print information about each input and output file - parser.add_argument('--verbose','-V', - action='count', default=0, - help='Verbose output of run') + parser.add_argument( + '--verbose', + '-V', + action='count', + default=0, + help='Verbose output of run', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run GRACE preliminary date program - run_grace_date(args.directory, args.center, args.release, - VERBOSE=args.verbose, MODE=args.mode) + run_grace_date( + args.directory, + args.center, + args.release, + VERBOSE=args.verbose, + MODE=args.mode, + ) + # run main program if __name__ == '__main__':