diff --git a/.github/workflows/python-request.yml b/.github/workflows/python-request.yml
index 95bad2d..ac94770 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 0000000..609db62
--- /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/README.md b/README.md
index 925f89b..457488f 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ Python tools for obtaining and working with Level-2 spherical harmonic coefficie
+
|
diff --git a/access/cnes_grace_sync.py b/access/cnes_grace_sync.py
index 38dc654..21c44de 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 0720915..080baf3 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 15e7588..33b1aec 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 32f2342..e84bb69 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 f8f7e61..1106da5 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 66dfa27..ec3a942 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 d448850..a49f445 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 0d3e639..c96f144 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 9c9313d..a4fbff8 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 3fc366f..ca5f459 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 e07b6a1..0860272 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 8585217..d340da9 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 9a69ee9..5c47d14 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 74fd99e..ff800b0 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 ca4ce4c..0221b80 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 8b3bbba..6f83b71 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 b2cf733..b66aafe 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 c8802c9..a4361ae 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 b7b15bf..43224b1 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 879caea..271af50 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/geocenter_compare_tellus.py b/geocenter/geocenter_compare_tellus.py
index 7ed393c..8a0961d 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 959ec27..c5721c0 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 de676f8..c4bcc71 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 9a9b2ef..950acdc 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/monte_carlo_degree_one.py b/geocenter/monte_carlo_degree_one.py
index 7e6501f..b87f2f2 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 4836936..2f56ffe 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 3cd9d3b..d55c50a 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 b1dd7ab..f75171a 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 53dff90..2f9e75b 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 372fad2..4c03781 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 a6eff2f..99f1129 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 cfb11ab..20b921c 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 4315d32..adecf99 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 64a8f5d..85e2adc 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 932c0e0..4e88163 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 44f6082..521b2cf 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 29849fb..1649e24 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 b291906..41e6aee 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 ca8de54..92e6fc8 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 f4d561c..07b14fb 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 563e280..f3be4cd 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 cc95fd7..7cf6c81 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 271c7e6..54a139f 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 674c532..a6c932b 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 40cade0..991dea5 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 2105fb0..0fb5e5b 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 3bc4019..0bd49f5 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 f42fb58..a294b44 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 c78834d..8b4704c 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 92d7222..47c92b8 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 f90a1ad..b90d090 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 af6b192..12bb92c 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 332e6a0..63601cf 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 98a01c7..c1fa2e7 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 1d72332..ab2ace7 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 39da6de..1ef260c 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 eeb6cc7..70868ad 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 37430e6..dc575eb 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 d344488..314a0da 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 bb8c9e0..47db2cc 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 1e1e390..88a10dd 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 ded4d4b..5653973 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 8ad52ce..09e56ac 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 ad4df44..5a5b666 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 99af58d..d01431a 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 8b52a41..03ccc3a 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 6604216..2fd9e08 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 bca9ecd..ddb3d9c 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 42ea690..6ed22a7 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 d80050e..6ba1d0d 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 16cc786..c31d511 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 6fb2010..8732475 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 5a7a27f..da2abf2 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 aeb2a2e..9a0df01 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 79059f1..eaaaa51 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 5e3b0bb..701e468 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 a890d68..ea9cbff 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 3902f36..3d8c6c2 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 d040071..b11409b 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 ac5c7b1..d44c340 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 ec5101a..babbf1a 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 b0d1375..1c44d2f 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 55e0246..4b2a7cf 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 a6ae008..729ff5a 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 296ead8..3f13ee5 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 d2bda2d..25eaad3 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 b3f9c14..5311085 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 88e64a8..5fb99dd 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 f6a4b10..9e770b6 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 064847b..771ffce 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 afa9f76..8fe8f96 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 0f11713..0b212bd 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_harmonic_resolution.py b/scripts/calc_harmonic_resolution.py
index ed32d69..a720e35 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 560afbb..525cb0d 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 9bae8ab..c6e9e21 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_harmonics.py b/scripts/combine_harmonics.py
index 714cff2..a726957 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/convert_harmonics.py b/scripts/convert_harmonics.py
index c6d7eee..00342b1 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/grace_mean_harmonics.py b/scripts/grace_mean_harmonics.py
index 98fba86..770d949 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 bd16709..b3507d3 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_error.py b/scripts/grace_spatial_error.py
index f8f5f93..a1fc5e2 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 aff3cf8..b9b0975 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/mascon_reconstruct.py b/scripts/mascon_reconstruct.py
index a63065e..1063ebf 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 a4ee0a3..e3bbdaf 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/regress_grace_maps.py b/scripts/regress_grace_maps.py
index ba4d0a2..842304e 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/run_sea_level_equation.py b/scripts/run_sea_level_equation.py
index fdeb7f4..ad2af07 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 0382330..c6cc3ed 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/setup.py b/setup.py
index 40ede65..97837a0 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 d4d2eca..7ee2498 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/quick_mascon_plot.py b/utilities/quick_mascon_plot.py
index a2fc933..ac136a9 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 77524d3..59dcbc7 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 7945dfb..6d83429 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__':