diff --git a/.github/workflows/python-request.yml b/.github/workflows/python-request.yml index fd0c89d6..ac94770d 100644 --- a/.github/workflows/python-request.yml +++ b/.github/workflows/python-request.yml @@ -1,13 +1,18 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: Python on pull request +name: pytest Build on: pull_request: paths: - gravity_toolkit/** + - access/** + - dealiasing/** + - geocenter/** + - mapping/** - scripts/** + - utilities/** - test/** - .github/workflows/python-request.yml schedule: @@ -32,7 +37,7 @@ jobs: with: lfs: true - name: Set up pixi environment - uses: prefix-dev/setup-pixi@v0.9.1 + uses: prefix-dev/setup-pixi@v0.9.6 - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/.github/workflows/ruff-format.yml b/.github/workflows/ruff-format.yml new file mode 100644 index 00000000..609db62b --- /dev/null +++ b/.github/workflows/ruff-format.yml @@ -0,0 +1,18 @@ +name: Ruff Format + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - main + +jobs: + ruff-format: + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@v6 + - name: Format and annotate PR + uses: astral-sh/ruff-action@v3 + with: + version: "latest" + args: "format --check --diff" diff --git a/.github/workflows/sphinx-build.yml b/.github/workflows/sphinx-build.yml new file mode 100644 index 00000000..966a94a8 --- /dev/null +++ b/.github/workflows/sphinx-build.yml @@ -0,0 +1,39 @@ +name: "Sphinx Build" + +on: + pull_request: + paths: + - gravity_toolkit/** + - doc/** + - .github/workflows/sphinx-build.yml + types: [opened, synchronize, reopened, ready_for_review] + +# set permissions for the workflow +permissions: + contents: read + pull-requests: write + +jobs: + docs: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + env: + SPHINXOPTS: "--fail-on-warning --fresh-env --keep-going --warning-file=sphinx.log" + defaults: + run: + shell: bash -l {0} + + steps: + - uses: actions/checkout@v6 + - name: Set up pixi environment + uses: prefix-dev/setup-pixi@v0.9.6 + - name: Compile Sphinx Documentation + run: | + # run a build of the documentation + pixi run --environment=dev docs + - name: Archive Sphinx Warnings + if: github.event.pull_request.head.repo.full_name == github.repository + uses: actions/upload-artifact@v6 + with: + name: sphinx-log + path: doc/sphinx.log diff --git a/README.md b/README.md new file mode 100644 index 00000000..457488f0 --- /dev/null +++ b/README.md @@ -0,0 +1,185 @@ +# gravity-toolkit + +Python tools for obtaining and working with Level-2 spherical harmonic coefficients from the NASA/DLR Gravity Recovery and Climate Experiment (GRACE) and the NASA/GFZ Gravity Recovery and Climate Experiment Follow-On (GRACE-FO) missions + +## About + + + + + + + + + + + + + + + + + + + + + + +
Version: + + + +
Citation: + +
Tests: + + + +
Data: + + +
License: + +
+ +For more information: see the documentation at [gravity-toolkit.readthedocs.io](https://gravity-toolkit.readthedocs.io/) + +## Installation + +From PyPI: + +```bash +python3 -m pip install gravity-toolkit +``` + +To include all optional dependencies: + +```bash +python3 -m pip install gravity-toolkit[all] +``` + +Using `conda` or `mamba` from conda-forge: + +```bash +conda install -c conda-forge gravity-toolkit +``` + +```bash +mamba install -c conda-forge gravity-toolkit +``` + +Development version from GitHub: + +```bash +python3 -m pip install git+https://github.com/tsutterley/gravity-toolkit.git +``` + +### Running with Pixi + +Alternatively, you can use [Pixi](https://pixi.sh/) for a streamlined workspace environment: + +1. Install Pixi following the [installation instructions](https://pixi.sh/latest/#installation) +2. Clone the project repository: + +```bash +git clone https://github.com/tsutterley/gravity-toolkit.git +``` + +3. Move into the `gravity-toolkit` directory + +```bash +cd gravity-toolkit +``` + +4. Install dependencies and start JupyterLab: + +```bash +pixi run start +``` + +This will automatically create the environment, install all dependencies, and launch JupyterLab in the [notebooks](./doc/source/notebooks/) directory. + +## Resources + +- [NASA GRACE mission site](https://www.nasa.gov/mission_pages/Grace/index.html) +- [NASA GRACE-FO mission site](https://www.nasa.gov/missions/grace-fo) +- [JPL GRACE Tellus site](https://grace.jpl.nasa.gov/) +- [JPL GRACE-FO site](https://gracefo.jpl.nasa.gov/) +- [UTCSR GRACE site](http://www.csr.utexas.edu/grace/) +- [GRACE at the NASA Physical Oceanography Distributed Active Archive Center (PO.DAAC)](https://podaac.jpl.nasa.gov/grace) +- [GRACE at the GFZ Information System and Data Center](http://isdc.gfz-potsdam.de/grace-isdc/) + +## Dependencies + +- [boto3: Amazon Web Services (AWS) SDK for Python](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) +- [future: Compatibility layer between Python 2 and Python 3](https://python-future.org/) +- [lxml: processing XML and HTML in Python](https://pypi.python.org/pypi/lxml) +- [matplotlib: Python 2D plotting library](https://matplotlib.org/) +- [netCDF4: Python interface to the netCDF C library](https://unidata.github.io/netcdf4-python/) +- [numpy: Scientific Computing Tools For Python](https://www.numpy.org) +- [platformdirs: Python module for determining platform-specific directories](https://pypi.org/project/platformdirs/) +- [python-dateutil: powerful extensions to datetime](https://dateutil.readthedocs.io/en/stable/) +- [PyYAML: YAML parser and emitter for Python](https://github.com/yaml/pyyaml) +- [scipy: Scientific Tools for Python](https://docs.scipy.org/doc/) + +## Download + +The program homepage is: + + +A zip archive of the latest version is available directly at: + + +## Disclaimer + +This package includes software developed at the University of California at Irvine (UCI), the NASA Jet Propulsion Laboratory (JPL), NASA Goddard Space Flight Center (GSFC) and the University of Washington Applied Physics Laboratory (UW-APL). +This program is not sponsored or maintained by the Universities Space Research Association (USRA), +the Center for Space Research at the University of Texas (UTCSR), the Jet Propulsion Laboratory (JPL), +the German Research Centre for Geosciences (GeoForschungsZentrum, GFZ) or NASA. +The software is provided here for your convenience but *with no guarantees whatsoever*. + +## Contributing + +This project contains work and contributions from the [scientific community](./CONTRIBUTORS.md). +If you would like to contribute to the project, please have a look at the [contribution guidelines](./doc/source/getting_started/Contributing.rst), [open issues](https://github.com/tsutterley/gravity-toolkit/issues) and [discussions board](https://github.com/tsutterley/gravity-toolkit/discussions). + +## References + +> T. C. Sutterley, I. Velicogna, and C.-W. Hsu, +> "Self-Consistent Ice Mass Balance and Regional Sea Level From Time-Variable Gravity", +> *Earth and Space Science*, 7, (2020). +> [doi: 10.1029/2019EA000860](https://doi.org/10.1029/2019EA000860) +> +> 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). +> [doi: 10.3390/rs11182108](https://doi.org/10.3390/rs11182108) +> +> J. Wahr, S. C. Swenson, and I. Velicogna, +> "Accuracy of GRACE mass estimates", +> *Geophysical Research Letters*, 33(6), L06401, (2006). +> [doi: 10.1029/2005GL025305](https://doi.org/10.1029/2005GL025305) +> +> J. Wahr, M. Molenaar, and F. Bryan, +> "Time variability of the Earth's gravity field: Hydrological and oceanic effects and their possible > detection using GRACE", +> *Journal of Geophysical Research: Solid Earth*, 103(B12), (1998). +> [doi: 10.1029/98JB02844](https://doi.org/10.1029/98JB02844) +> +> D. Han and J. Wahr, +> "The viscoelastic relaxation of a realistically stratified earth, and a further analysis of postglacial rebound", +> *Geophysical Journal International*, 120(2), (1995). +> [doi: 10.1111/j.1365-246X.1995.tb01819.x](https://doi.org/10.1111/j.1365-246X.1995.tb01819.x) + +## Data Repositories + +> T. C. Sutterley, I. Velicogna, and C.-W. Hsu, +> "Ice Mass and Regional Sea Level Estimates from Time-Variable Gravity", (2020). +> [doi: 10.6084/m9.figshare.9702338](https://doi.org/10.6084/m9.figshare.9702338) +> +> T. C. Sutterley and I. Velicogna, +> "Geocenter Estimates from Time-Variable Gravity and Ocean Model Outputs", (2019). +> [doi: 10.6084/m9.figshare.7388540](https://doi.org/10.6084/m9.figshare.7388540) + +## License + +The content of this project is licensed under the [Creative Commons Attribution 4.0 Attribution license](https://creativecommons.org/licenses/by/4.0/) and the source code is licensed under the [MIT license](LICENSE). diff --git a/README.rst b/README.rst deleted file mode 100644 index 558ea905..00000000 --- a/README.rst +++ /dev/null @@ -1,114 +0,0 @@ -=============== -gravity-toolkit -=============== - -|License| -|Documentation Status| -|PyPI| -|conda-forge| -|commits-since| -|zenodo| - -.. |License| image:: https://img.shields.io/github/license/tsutterley/gravity-toolkit - :target: https://github.com/tsutterley/gravity-toolkit/blob/main/LICENSE - -.. |Documentation Status| image:: https://readthedocs.org/projects/gravity-toolkit/badge/?version=latest - :target: https://gravity-toolkit.readthedocs.io/en/latest/?badge=latest - -.. |PyPI| image:: https://img.shields.io/pypi/v/gravity-toolkit.svg - :target: https://pypi.python.org/pypi/gravity-toolkit/ - -.. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/gravity-toolkit - :target: https://anaconda.org/conda-forge/gravity-toolkit - -.. |commits-since| image:: https://img.shields.io/github/commits-since/tsutterley/gravity-toolkit/latest - :target: https://github.com/tsutterley/gravity-toolkit/releases/latest - -.. |zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.5156864.svg - :target: https://doi.org/10.5281/zenodo.5156864 - -Python tools for obtaining and working with Level-2 spherical harmonic coefficients from the NASA/DLR Gravity Recovery and Climate Experiment (GRACE) and the NASA/GFZ Gravity Recovery and Climate Experiment Follow-On (GRACE-FO) missions - -Resources -######### - -- `NASA GRACE mission site `_ -- `NASA GRACE-FO mission site `_ -- `JPL GRACE Tellus site `_ -- `JPL GRACE-FO site `_ -- `UTCSR GRACE site `_ -- `GRACE at the NASA Physical Oceanography Distributed Active Archive Center (PO.DAAC) `_ -- `GRACE at the GFZ Information System and Data Center `_ - -Dependencies -############ - -- `numpy: Scientific Computing Tools For Python `_ -- `scipy: Scientific Tools for Python `_ -- `dateutil: powerful extensions to datetime `_ -- `PyYAML: YAML parser and emitter for Python `_ -- `lxml: processing XML and HTML in Python `_ -- `future: Compatibility layer between Python 2 and Python 3 `_ -- `matplotlib: Python 2D plotting library `_ -- `cartopy: Python package designed for geospatial data processing `_ -- `netCDF4: Python interface to the netCDF C library `_ -- `h5py: Python interface for Hierarchal Data Format 5 (HDF5) `_ -- `geoid-toolkit: Python utilities for calculating geoid heights from static gravity field coefficients `_ - -References -########## - - T. C. Sutterley, I. Velicogna, and C.-W. Hsu, "Self-Consistent Ice Mass Balance - and Regional Sea Level From Time-Variable Gravity", *Earth and Space Science*, 7, - (2020). `doi: 10.1029/2019EA000860 `_ - - 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). `doi: 10.3390/rs11182108 `_ - - J. Wahr, S. C. Swenson, and I. Velicogna, "Accuracy of GRACE mass estimates", - *Geophysical Research Letters*, 33(6), L06401, (2006). - `doi: 10.1029/2005GL025305 `_ - - J. Wahr, M. Molenaar, and F. Bryan, "Time variability of the Earth's gravity - field: Hydrological and oceanic effects and their possible detection using - GRACE", *Journal of Geophysical Research: Solid Earth*, 103(B12), (1998). - `doi: 10.1029/98JB02844 `_ - - D. Han and J. Wahr, "The viscoelastic relaxation of a realistically stratified - earth, and a further analysis of postglacial rebound", *Geophysical Journal - International*, 120(2), (1995). - `doi: 10.1111/j.1365-246X.1995.tb01819.x `_ - -Data Repositories -################# - - T. C. Sutterley, I. Velicogna, and C.-W. Hsu, "Ice Mass and Regional Sea Level - Estimates from Time-Variable Gravity", (2020). - `doi: 10.6084/m9.figshare.9702338 `_ - - T. C. Sutterley and I. Velicogna, "Geocenter Estimates from Time-Variable - Gravity and Ocean Model Outputs", (2019). - `doi: 10.6084/m9.figshare.7388540 `_ - -Download -######## - -| The program homepage is: -| https://github.com/tsutterley/gravity-toolkit -| A zip archive of the latest version is available directly at: -| https://github.com/tsutterley/gravity-toolkit/archive/main.zip - -Disclaimer -########## - -This project contains work and contributions from the `scientific community <./CONTRIBUTORS.md>`_. -This program is not sponsored or maintained by the Universities Space Research Association (USRA), -the Center for Space Research at the University of Texas (UTCSR), the Jet Propulsion Laboratory (JPL), -the German Research Centre for Geosciences (GeoForschungsZentrum, GFZ) or NASA. -It is provided here for your convenience but *with no guarantees whatsoever*. - -License -####### - -The content of this project is licensed under the `Creative Commons Attribution 4.0 Attribution license `_ and the source code is licensed under the `MIT license `_. diff --git a/access/cnes_grace_sync.py b/access/cnes_grace_sync.py index 00232e12..21c44de8 100755 --- a/access/cnes_grace_sync.py +++ b/access/cnes_grace_sync.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" cnes_grace_sync.py Written by Tyler Sutterley (12/2022) @@ -91,6 +91,7 @@ added functionality for RL01 and RL03 (future release) Written 07/2012 """ + from __future__ import print_function import sys @@ -108,11 +109,13 @@ import posixpath import gravity_toolkit as gravtk + # PURPOSE: sync local GRACE/GRACE-FO files with CNES server -def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, - CLOBBER=False, MODE=None): +def cnes_grace_sync( + DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, CLOBBER=False, MODE=None +): # remote CNES/GRGS host directory - HOST = ['http://gravitegrace.get.obs-mip.fr','grgs.obs-mip.fr','data'] + HOST = ['http://gravitegrace.get.obs-mip.fr', 'grgs.obs-mip.fr', 'data'] # check if directory exists and recursively create if not DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() @@ -127,27 +130,27 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, DSET['RL05'] = ['GSM', 'GAA', 'GAB'] # remote path to tar files on CNES servers - REMOTE = dict(RL01={},RL02={},RL03={},RL04={},RL05={}) + REMOTE = dict(RL01={}, RL02={}, RL03={}, RL04={}, RL05={}) # RL01: GSM and GAC - REMOTE['RL01']['GSM'] = ['RL01','variable','archives'] - REMOTE['RL01']['GAC'] = ['RL01','variable','archives'] + REMOTE['RL01']['GSM'] = ['RL01', 'variable', 'archives'] + REMOTE['RL01']['GAC'] = ['RL01', 'variable', 'archives'] # RL02: GSM, GAA and GAB - REMOTE['RL02']['GSM'] = ['RL02','variable','archives'] - REMOTE['RL02']['GAA'] = ['RL02','variable','archives'] - REMOTE['RL02']['GAB'] = ['RL02','variable','archives'] + REMOTE['RL02']['GSM'] = ['RL02', 'variable', 'archives'] + REMOTE['RL02']['GAA'] = ['RL02', 'variable', 'archives'] + REMOTE['RL02']['GAB'] = ['RL02', 'variable', 'archives'] # RL03: GSM, GAA and GAB - REMOTE['RL03']['GSM'] = ['RL03-v3','archives'] - REMOTE['RL03']['GAA'] = ['RL03','variable','archives'] - REMOTE['RL03']['GAB'] = ['RL03','variable','archives'] + REMOTE['RL03']['GSM'] = ['RL03-v3', 'archives'] + REMOTE['RL03']['GAA'] = ['RL03', 'variable', 'archives'] + REMOTE['RL03']['GAB'] = ['RL03', 'variable', 'archives'] # RL04: GSM - REMOTE['RL04']['GSM'] = ['RL04-v1','archives'] + REMOTE['RL04']['GSM'] = ['RL04-v1', 'archives'] # RL05: GSM, GAA, GAB for GRACE/GRACE-FO - REMOTE['RL05']['GSM'] = ['RL05','archives'] - REMOTE['RL05']['GAA'] = ['RL05','archives'] - REMOTE['RL05']['GAB'] = ['RL05','archives'] + REMOTE['RL05']['GSM'] = ['RL05', 'archives'] + REMOTE['RL05']['GAA'] = ['RL05', 'archives'] + REMOTE['RL05']['GAB'] = ['RL05', 'archives'] # tar file names for each dataset - TAR = dict(RL01={},RL02={},RL03={},RL04={},RL05={}) + TAR = dict(RL01={}, RL02={}, RL03={}, RL04={}, RL05={}) # RL01: GSM and GAC TAR['RL01']['GSM'] = ['GRGS.SH_models.GRACEFORMAT.RL01.tar.gz'] TAR['RL01']['GAC'] = ['GRGS.dealiasing.RL01.tar.gz'] @@ -161,10 +164,14 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, TAR['RL03']['GAB'] = ['GRGS.RL03.dealiasing.monthly.tar.gz'] # RL04: GSM # TAR['RL04']['GSM'] = ['CNES.RL04-v1.monthly.OLD_IERS2010_MEAN_POLE_CONVENTION.tar.gz'] - TAR['RL04']['GSM'] = ['CNES.RL04-v1.monthly.NEW_IERS2010_MEAN_POLE_CONVENTION.tar.gz'] + TAR['RL04']['GSM'] = [ + 'CNES.RL04-v1.monthly.NEW_IERS2010_MEAN_POLE_CONVENTION.tar.gz' + ] # RL05: GSM, GAA and GAB - TAR['RL05']['GSM'] = ['CNES-GRGS.RL05.GRACE.monthly.tar.gz', - 'CNES-GRGS.RL05.GRACE-FO.monthly.tar.gz'] + TAR['RL05']['GSM'] = [ + 'CNES-GRGS.RL05.GRACE.monthly.tar.gz', + 'CNES-GRGS.RL05.GRACE-FO.monthly.tar.gz', + ] TAR['RL05']['GAA'] = ['CNES-GRGS.RL05.monthly.dealiasing.tar.gz'] TAR['RL05']['GAB'] = ['CNES-GRGS.RL05.monthly.dealiasing.tar.gz'] @@ -172,7 +179,7 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, if LOG: # output to log file # format: CNES_sync_2002-04-01.log - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) LOGFILE = DIRECTORY.joinpath(f'CNES_sync_{today}.log') fid1 = LOGFILE.open(mode='w', encoding='utf8') logging.basicConfig(stream=fid1, level=logging.INFO) @@ -200,18 +207,27 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, local_file = DIRECTORY.joinpath('CNES', rl, t) MD5 = gravtk.utilities.get_hash(local_file) # copy remote tar file to local if new or updated - gravtk.utilities.from_http(remote_tar_path, - local=local_file, timeout=TIMEOUT, hash=MD5, chunk=16384, - verbose=True, fid=fid1, mode=MODE) + gravtk.utilities.from_http( + remote_tar_path, + local=local_file, + timeout=TIMEOUT, + hash=MD5, + chunk=16384, + verbose=True, + fid=fid1, + mode=MODE, + ) # Create and submit request to get modification time of file remote_file = posixpath.join(*remote_tar_path) request = gravtk.utilities.urllib2.Request(remote_file) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=TIMEOUT + ) # change modification time to remote time_string = response.headers['last-modified'] - remote_mtime = gravtk.utilities.get_unix_time(time_string, - format='%a, %d %b %Y %H:%M:%S %Z') + remote_mtime = gravtk.utilities.get_unix_time( + time_string, format='%a, %d %b %Y %H:%M:%S %Z' + ) # keep remote modification time of file and local access time os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) @@ -219,7 +235,9 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, tar = tarfile.open(name=local_file, mode='r:gz') # copy files from the tar file into the data directory - member_list=[m for m in tar.getmembers() if re.search(ds,m.name)] + member_list = [ + m for m in tar.getmembers() if re.search(ds, m.name) + ] # for each member of the dataset within the tar file for member in member_list: # local gzipped version of the file @@ -230,8 +248,9 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, tar.close() # find GRACE files and sort by date - grace_files = [f.name for f in local_dir.iterdir() - if re.search(ds, f.name)] + grace_files = [ + f.name for f in local_dir.iterdir() if re.search(ds, f.name) + ] # outputting GRACE filenames to index index_file = local_dir.joinpath('index.txt') with index_file.open(mode='w', encoding='utf8') as fid: @@ -245,6 +264,7 @@ def cnes_grace_sync(DIRECTORY, DREL=[], TIMEOUT=None, LOG=False, fid1.close() LOGFILE.chmod(mode=MODE) + # PURPOSE: copy file from tar file checking if file exists locally # and if the original file is newer than the local file def gzip_copy_file(tar, member, local_file, CLOBBER, MODE): @@ -261,9 +281,9 @@ def gzip_copy_file(tar, member, local_file, CLOBBER, MODE): fileobj = fileID.fileobj fileobj.seek(4) # extract little endian 4 bit unsigned integer - file2_mtime, = struct.unpack(" file2_mtime): + if file1_mtime > file2_mtime: TEST = True OVERWRITE = ' (overwrite)' else: @@ -283,6 +303,7 @@ def gzip_copy_file(tar, member, local_file, CLOBBER, MODE): os.utime(local_file, (local_file.stat().st_atime, file1_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -292,45 +313,78 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 2f9a696e..080baf35 100644 --- a/access/esa_costg_swarm_sync.py +++ b/access/esa_costg_swarm_sync.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" esa_costg_swarm_sync.py Written by Tyler Sutterley (05/2023) Syncs Swarm gravity field products from the ESA Swarm Science Server @@ -36,6 +36,7 @@ Updated 10/2021: using python logging for handling verbose output Written 09/2021 """ + from __future__ import print_function import sys @@ -52,21 +53,29 @@ import lxml.etree import gravity_toolkit as gravtk -# PURPOSE: sync local Swarm files with ESA server -def esa_costg_swarm_sync(DIRECTORY, RELEASE=None, TIMEOUT=None, LOG=False, - LIST=False, CLOBBER=False, CHECKSUM=False, MODE=0o775): +# PURPOSE: sync local Swarm files with ESA server +def esa_costg_swarm_sync( + DIRECTORY, + RELEASE=None, + TIMEOUT=None, + LOG=False, + LIST=False, + CLOBBER=False, + CHECKSUM=False, + MODE=0o775, +): # check if directory exists and recursively create if not DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() # local directory for exact data product - local_dir = DIRECTORY.joinpath('Swarm',RELEASE,'GSM') + local_dir = DIRECTORY.joinpath('Swarm', RELEASE, 'GSM') local_dir.mkdir(mode=MODE, parents=True, exist_ok=True) # create log file with list of synchronized files (or print to terminal) if LOG: # output to log file # format: ESA_Swarm_sync_2002-04-01.log - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) LOGFILE = DIRECTORY.joinpath(f'ESA_Swarm_sync_{today}.log') logging.basicConfig(filename=LOGFILE, level=logging.INFO) logging.info(f'ESA Swarm Sync Log ({today})') @@ -81,8 +90,9 @@ def esa_costg_swarm_sync(DIRECTORY, RELEASE=None, TIMEOUT=None, LOG=False, # compile xml parsers for lxml XMLparser = lxml.etree.XMLParser() # create "opener" (OpenerDirector instance) - gravtk.utilities.build_opener(None, None, - authorization_header=False, urs=HOST) + gravtk.utilities.build_opener( + None, None, authorization_header=False, urs=HOST + ) # All calls to urllib2.urlopen will now use handler # Make sure not to include the protocol in with the URL, or # HTTPPasswordMgrWithDefaultRealm will be confused. @@ -95,20 +105,24 @@ def esa_costg_swarm_sync(DIRECTORY, RELEASE=None, TIMEOUT=None, LOG=False, colnames = [] collastmod = [] # position, maximum number of files to list, flag to check if done - pos,maxfiles,prevmax = (0,500,500) + pos, maxfiles, prevmax = (0, 500, 500) # iterate to get a compiled list of files # will iterate until there are no more files to add to the lists - while (maxfiles == prevmax): + while maxfiles == prevmax: # set previous flag to maximum prevmax = maxfiles # open connection with Swarm science server at remote directory # to list maxfiles number of files at position - parameters = gravtk.utilities.urlencode({'maxfiles':prevmax, - 'pos':pos,'file':posixpath.join('swarm','Level2longterm','EGF')}) - url=posixpath.join(HOST,f'?do=list&{parameters}') + parameters = gravtk.utilities.urlencode( + { + 'maxfiles': prevmax, + 'pos': pos, + 'file': posixpath.join('swarm', 'Level2longterm', 'EGF'), + } + ) + url = posixpath.join(HOST, f'?do=list&{parameters}') request = gravtk.utilities.urllib2.Request(url=url) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen(request, timeout=TIMEOUT) table = json.loads(response.read().decode()) # extend lists with new files colnames.extend([t['name'] for t in table['results']]) @@ -119,22 +133,33 @@ def esa_costg_swarm_sync(DIRECTORY, RELEASE=None, TIMEOUT=None, LOG=False, pos += maxfiles # find lines of valid files - valid_lines = [i for i,f in enumerate(colnames) if R1.match(f)] + valid_lines = [i for i, f in enumerate(colnames) if R1.match(f)] # write each file to an index - index_file = local_dir.joinpath(local_dir,'index.txt') + index_file = local_dir.joinpath(local_dir, 'index.txt') fid = index_file.open(mode='w', encoding='utf8') # for each data and header file for i in valid_lines: # remote and local versions of the file - parameters = gravtk.utilities.urlencode({'file': - posixpath.join('swarm','Level2longterm','EGF',colnames[i])}) - remote_file = posixpath.join(HOST, - f'?do=download&{parameters}') + parameters = gravtk.utilities.urlencode( + { + 'file': posixpath.join( + 'swarm', 'Level2longterm', 'EGF', colnames[i] + ) + } + ) + remote_file = posixpath.join(HOST, f'?do=download&{parameters}') local_file = local_dir.joinpath(colnames[i]) # check that file is not in file system unless overwriting - http_pull_file(remote_file, collastmod[i], local_file, - TIMEOUT=TIMEOUT, LIST=LIST, CLOBBER=CLOBBER, - CHECKSUM=CHECKSUM, MODE=MODE) + http_pull_file( + remote_file, + collastmod[i], + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + CHECKSUM=CHECKSUM, + MODE=MODE, + ) # output Swarm filenames to index print(colnames[i], file=fid) # change permissions of index file @@ -144,10 +169,19 @@ def esa_costg_swarm_sync(DIRECTORY, RELEASE=None, TIMEOUT=None, LOG=False, if LOG: LOGFILE.chmod(mode=MODE) + # PURPOSE: pull file from a remote host checking if file exists locally # and if the remote file is newer than the local file -def http_pull_file(remote_file, remote_mtime, local_file, TIMEOUT=120, - LIST=False, CLOBBER=False, CHECKSUM=False, MODE=0o775): +def http_pull_file( + remote_file, + remote_mtime, + local_file, + TIMEOUT=120, + LIST=False, + CLOBBER=False, + CHECKSUM=False, + MODE=0o775, +): # if file exists in file system: check if remote file is newer TEST = False OVERWRITE = ' (clobber)' @@ -161,22 +195,23 @@ def http_pull_file(remote_file, remote_mtime, local_file, TIMEOUT=120, # There are a wide range of exceptions that can be thrown here # including HTTPError and URLError. req = gravtk.utilities.urllib2.Request(remote_file) - resp = gravtk.utilities.urllib2.urlopen(req,timeout=TIMEOUT) + resp = gravtk.utilities.urllib2.urlopen(req, timeout=TIMEOUT) # copy remote file contents to bytesIO object remote_buffer = io.BytesIO(resp.read()) remote_buffer.seek(0) # generate checksum hash for remote file remote_hash = gravtk.utilities.get_hash(remote_buffer) # compare checksums - if (local_hash != remote_hash): + if local_hash != remote_hash: TEST = True OVERWRITE = f' (checksums: {local_hash} {remote_hash})' elif local_file.exists(): # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -202,8 +237,9 @@ def http_pull_file(remote_file, remote_mtime, local_file, TIMEOUT=120, # There are a range of exceptions that can be thrown here # including HTTPError and URLError. request = gravtk.utilities.urllib2.Request(remote_file) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=TIMEOUT + ) # copy remote file contents to local file with local_file.open(mode='wb') as f: shutil.copyfileobj(response, f, CHUNK) @@ -211,6 +247,7 @@ def http_pull_file(remote_file, remote_mtime, local_file, TIMEOUT=120, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -220,39 +257,72 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 @@ -262,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 9b7b0e89..33b1aec1 100644 --- a/access/gfz_icgem_costg_ftp.py +++ b/access/gfz_icgem_costg_ftp.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gfz_icgem_costg_ftp.py Written by Tyler Sutterley (05/2023) Syncs GRACE/GRACE-FO/Swarm COST-G data from the GFZ International @@ -45,6 +45,7 @@ Updated 10/2021: using python logging for handling verbose output Written 09/2021 """ + from __future__ import print_function import sys @@ -60,31 +61,41 @@ import posixpath import gravity_toolkit as gravtk + # PURPOSE: create and compile regular expression operator to find files def compile_regex_pattern(MISSION, DSET): - if ((DSET == 'GSM') and (MISSION == 'Swarm')): + if (DSET == 'GSM') and (MISSION == 'Swarm'): # regular expression operators for Swarm data - regex=r'(SW)_(.*?)_(EGF_SHA_2)__(.*?)_(.*?)_(.*?)(\.gfc|\.ZIP)' - elif ((DSET != 'GSM') and (MISSION == 'Swarm')): - regex=r'(GAA|GAB|GAC|GAD)_Swarm_(\d+)_(\d{2})_(\d{4})(\.gfc|\.ZIP)' + regex = r'(SW)_(.*?)_(EGF_SHA_2)__(.*?)_(.*?)_(.*?)(\.gfc|\.ZIP)' + elif (DSET != 'GSM') and (MISSION == 'Swarm'): + regex = r'(GAA|GAB|GAC|GAD)_Swarm_(\d+)_(\d{2})_(\d{4})(\.gfc|\.ZIP)' else: - regex=rf'{DSET}-2_(.*?)\.gfc$' + regex = rf'{DSET}-2_(.*?)\.gfc$' # return the compiled regular expression operator used to find files return re.compile(regex, re.VERBOSE) -# PURPOSE: sync local GRACE/GRACE-FO/Swarm files with GFZ ICGEM server -def gfz_icgem_costg_ftp(DIRECTORY, MISSION=[], RELEASE=None, TIMEOUT=None, - LOG=False, LIST=False, CLOBBER=False, CHECKSUM=False, MODE=None): +# PURPOSE: sync local GRACE/GRACE-FO/Swarm files with GFZ ICGEM server +def gfz_icgem_costg_ftp( + DIRECTORY, + MISSION=[], + RELEASE=None, + TIMEOUT=None, + LOG=False, + LIST=False, + CLOBBER=False, + CHECKSUM=False, + MODE=None, +): # check if directory exists and recursively create if not DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) # dealiasing datasets for each mission DSET = {} - DSET['Grace'] = ['GAC','GSM'] + DSET['Grace'] = ['GAC', 'GSM'] DSET['Grace-FO'] = ['GSM'] - DSET['Swarm'] = ['GAA','GAB','GAC','GAD','GSM'] + DSET['Swarm'] = ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'] # local subdirectory for data LOCAL = {} LOCAL['Grace'] = 'COSTG' @@ -95,7 +106,7 @@ def gfz_icgem_costg_ftp(DIRECTORY, MISSION=[], RELEASE=None, TIMEOUT=None, if LOG: # output to log file # format: GFZ_ICGEM_COST-G_sync_2002-04-01.log - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) LOGFILE = DIRECTORY.joinpath(f'GFZ_ICGEM_COST-G_sync_{today}.log') logging.basicConfig(filename=LOGFILE, level=logging.INFO) logging.info(f'GFZ ICGEM COST-G Sync Log ({today})') @@ -121,33 +132,42 @@ def gfz_icgem_costg_ftp(DIRECTORY, MISSION=[], RELEASE=None, TIMEOUT=None, # compile the regular expression operator to find files R1 = compile_regex_pattern(MISSION, ds) # set the remote path to download files - if ds in ('GAA','GAB','GAC','GAD') and (MISSION == 'Swarm'): - remote_path = [ftp.host,'02_COST-G',MISSION,'GAX_products',ds] - elif ds in ('GAA','GAB','GAC','GAD') and (MISSION != 'Swarm'): - remote_path = [ftp.host,'02_COST-G',MISSION,'GAX_products'] - elif (MISSION == 'Swarm'): - remote_path = [ftp.host,'02_COST-G',MISSION,'40x40'] - elif (MISSION == 'Grace'): - remote_path = [ftp.host,'02_COST-G',MISSION,'unfiltered'] - elif (MISSION == 'Grace-FO'): - remote_path = [ftp.host,'02_COST-G',MISSION] + if ds in ('GAA', 'GAB', 'GAC', 'GAD') and (MISSION == 'Swarm'): + remote_path = [ftp.host, '02_COST-G', MISSION, 'GAX_products', ds] + elif ds in ('GAA', 'GAB', 'GAC', 'GAD') and (MISSION != 'Swarm'): + remote_path = [ftp.host, '02_COST-G', MISSION, 'GAX_products'] + elif MISSION == 'Swarm': + remote_path = [ftp.host, '02_COST-G', MISSION, '40x40'] + elif MISSION == 'Grace': + remote_path = [ftp.host, '02_COST-G', MISSION, 'unfiltered'] + elif MISSION == 'Grace-FO': + remote_path = [ftp.host, '02_COST-G', MISSION] # get filenames from remote directory - remote_files,remote_mtimes = gravtk.utilities.ftp_list( - remote_path, timeout=TIMEOUT, basename=True, pattern=R1, - sort=True) + remote_files, remote_mtimes = gravtk.utilities.ftp_list( + remote_path, timeout=TIMEOUT, basename=True, pattern=R1, sort=True + ) # download the file from the ftp server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # remote and local versions of the file remote_path.append(fi) local_file = local_dir.joinpath(fi) - ftp_mirror_file(ftp, remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, CHECKSUM=CHECKSUM, MODE=MODE) + ftp_mirror_file( + ftp, + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + CHECKSUM=CHECKSUM, + MODE=MODE, + ) # remove the file from the remote path list remote_path.remove(fi) # find local GRACE/GRACE-FO/Swarm files to create index - grace_files = sorted([f.name for f in local_dir.iterdir() - if R1.match(f.name)]) + grace_files = sorted( + [f.name for f in local_dir.iterdir() if R1.match(f.name)] + ) # write each file to an index index_file = local_dir.joinpath('index.txt') with index_file.open(mode='w', encoding='utf8') as fid: @@ -163,10 +183,20 @@ def gfz_icgem_costg_ftp(DIRECTORY, MISSION=[], RELEASE=None, TIMEOUT=None, if LOG: LOGFILE.chmod(mode=MODE) + # PURPOSE: pull file from a remote host checking if file exists locally # and if the remote file is newer than the local file -def ftp_mirror_file(ftp,remote_path,remote_mtime,local_file, - TIMEOUT=None,LIST=False,CLOBBER=False,CHECKSUM=False,MODE=0o775): +def ftp_mirror_file( + ftp, + remote_path, + remote_mtime, + local_file, + TIMEOUT=None, + LIST=False, + CLOBBER=False, + CHECKSUM=False, + MODE=0o775, +): # if file exists in file system: check if remote file is newer TEST = False OVERWRITE = ' (clobber)' @@ -177,20 +207,20 @@ def ftp_mirror_file(ftp,remote_path,remote_mtime,local_file, # open the local_file in binary read mode local_hash = gravtk.utilities.get_hash(local_file) # copy remote file contents to bytesIO object - remote_buffer = gravtk.utilities.from_ftp(remote_path, - timeout=TIMEOUT) + remote_buffer = gravtk.utilities.from_ftp(remote_path, timeout=TIMEOUT) # generate checksum hash for remote file remote_hash = hashlib.md5(remote_buffer.getvalue()).hexdigest() # compare checksums - if (local_hash != remote_hash): + if local_hash != remote_hash: TEST = True OVERWRITE = f' (checksums: {local_hash} {remote_hash})' elif local_file.exists(): # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -199,7 +229,7 @@ def ftp_mirror_file(ftp,remote_path,remote_mtime,local_file, # if file does not exist locally, is to be overwritten, or CLOBBER is set if TEST or CLOBBER: # Printing files transferred - remote_ftp_url = posixpath.join('ftp://',*remote_path) + remote_ftp_url = posixpath.join('ftp://', *remote_path) logging.info(f'{remote_ftp_url} -->') logging.info(f'\t{str(local_file)}{OVERWRITE}\n') # if executing copy command (not only printing the files) @@ -220,6 +250,7 @@ def ftp_mirror_file(ftp,remote_path,remote_mtime,local_file, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -229,62 +260,108 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 f51f9b78..e84bb695 100644 --- a/access/gfz_isdc_dealiasing_sync.py +++ b/access/gfz_isdc_dealiasing_sync.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gfz_isdc_dealiasing_sync.py Written by Tyler Sutterley (10/2025) Syncs GRACE Level-1b dealiasing products from the GFZ Information @@ -48,6 +48,7 @@ Updated 03/2018: made tar file creation optional with --tar Written 03/2018 """ + from __future__ import print_function import sys @@ -63,20 +64,30 @@ import posixpath import gravity_toolkit as gravtk + # PURPOSE: syncs GRACE Level-1b dealiasing products from the GFZ data server # and optionally outputs as monthly tar files -def gfz_isdc_dealiasing_sync(base_dir, DREL, YEAR=None, MONTHS=None, TAR=False, - TIMEOUT=None, LOG=False, CLOBBER=False, MODE=None): +def gfz_isdc_dealiasing_sync( + base_dir, + DREL, + YEAR=None, + MONTHS=None, + TAR=False, + TIMEOUT=None, + LOG=False, + CLOBBER=False, + MODE=None, +): # check if directory exists and recursively create if not base_dir = pathlib.Path(base_dir).expanduser().absolute() - grace_dir = base_dir.joinpath('AOD1B',DREL) + grace_dir = base_dir.joinpath('AOD1B', DREL) grace_dir.mkdir(mode=MODE, parents=True, exist_ok=True) # create log file with list of synchronized files (or print to terminal) if LOG: # output to log file # format: GFZ_AOD1B_sync_2002-04-01.log - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) LOGFILE = base_dir.joinpath(f'GFZ_AOD1B_sync_{today}.log') logging.basicConfig(filename=LOGFILE, level=logging.INFO) logging.info(f'GFZ AOD1b Sync Log ({today})') @@ -98,14 +109,19 @@ def gfz_isdc_dealiasing_sync(base_dir, DREL, YEAR=None, MONTHS=None, TAR=False, SUFFIX = dict(RL04='tar.gz', RL05='tar.gz', RL06='tgz') # find remote yearly directories for DREL - YRS,_ = http_list([HOST,'grace','Level-1B', 'GFZ','AOD',DREL], - timeout=TIMEOUT, basename=True, pattern=R1, sort=True) + YRS, _ = http_list( + [HOST, 'grace', 'Level-1B', 'GFZ', 'AOD', DREL], + timeout=TIMEOUT, + basename=True, + pattern=R1, + sort=True, + ) # for each year for Y in YRS: # for each month of interest for M in MONTHS: # output tar file for year and month - args = (Y, M, DREL.replace('RL',''), SUFFIX[DREL]) + args = (Y, M, DREL.replace('RL', ''), SUFFIX[DREL]) FILE = 'AOD1B_{0}-{1:02d}_{2}.{3}'.format(*args) # check if output tar file exists (if TAR) local_tar_file = grace_dir.joinpath(FILE) @@ -113,22 +129,36 @@ def gfz_isdc_dealiasing_sync(base_dir, DREL, YEAR=None, MONTHS=None, TAR=False, # compile regular expressions operators for file dates # will extract year and month and calendar day from the ascii file regex_pattern = r'AOD1B_({0})-({1:02d})-(\d+)_X_\d+.asc.gz$' - R2 = re.compile(regex_pattern.format(Y,M), re.VERBOSE) - remote_files,remote_mtimes = http_list( - [HOST,'grace','Level-1B','GFZ','AOD',DREL,Y], - timeout=TIMEOUT, basename=True, pattern=R2, sort=True) + R2 = re.compile(regex_pattern.format(Y, M), re.VERBOSE) + remote_files, remote_mtimes = http_list( + [HOST, 'grace', 'Level-1B', 'GFZ', 'AOD', DREL, Y], + timeout=TIMEOUT, + basename=True, + pattern=R2, + sort=True, + ) file_count = len(remote_files) # if compressing into monthly tar files if TAR and (file_count > 0) and (TEST or CLOBBER): # copy each gzip file and store within monthly tar files tar = tarfile.open(name=local_tar_file, mode='w:gz') - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # remote version of each input file - remote = [HOST,'grace','Level-1B','GFZ','AOD',DREL,Y,fi] + remote = [ + HOST, + 'grace', + 'Level-1B', + 'GFZ', + 'AOD', + DREL, + Y, + fi, + ] logging.info(posixpath.join(*remote)) # retrieve bytes from remote file - remote_buffer = gravtk.utilities.from_sync(remote, - timeout=TIMEOUT) + remote_buffer = gravtk.utilities.from_sync( + remote, timeout=TIMEOUT + ) # add file to tar tar_info = tarfile.TarInfo(name=fi) tar_info.mtime = remote_mtime @@ -140,25 +170,40 @@ def gfz_isdc_dealiasing_sync(base_dir, DREL, YEAR=None, MONTHS=None, TAR=False, local_tar_file.chmod(mode=MODE) elif (file_count > 0) and not TAR: # copy each gzip file and keep as individual daily files - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # remote and local version of each input file - remote = [HOST,'grace','Level-1B','GFZ','AOD',DREL,Y,fi] + remote = [ + HOST, + 'grace', + 'Level-1B', + 'GFZ', + 'AOD', + DREL, + Y, + fi, + ] local_file = grace_dir.joinpath(fi) - http_pull_file(remote,remote_mtime,local_file, - CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote, + remote_mtime, + local_file, + CLOBBER=CLOBBER, + MODE=MODE, + ) # close log file and set permissions level to MODE if LOG: LOGFILE.chmod(mode=MODE) + # PURPOSE: list a directory on the GFZ https server def http_list( - HOST: str | list, - timeout: int | None = None, - context: ssl.SSLContext = gravtk.utilities._default_ssl_context, - pattern: str | re.Pattern = '', - sort: bool = False - ): + HOST: str | list, + timeout: int | None = None, + context: ssl.SSLContext = gravtk.utilities._default_ssl_context, + pattern: str | re.Pattern = '', + sort: bool = False, +): """ List a directory on the GFZ https Server @@ -192,8 +237,9 @@ def http_list( try: # Create and submit request. request = gravtk.utilities.urllib2.Request(posixpath.join(*HOST)) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=timeout, context=context) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=timeout, context=context + ) except Exception as exc: raise Exception('List error from {0}'.format(posixpath.join(*HOST))) # read the directory listing @@ -201,32 +247,41 @@ def http_list( # read and parse request for files (column names and modified times) lines = [l for l in contents if rx.search(l.decode('utf-8'))] # column names and last modified times - colnames = [None]*len(lines) - collastmod = [None]*len(lines) + colnames = [None] * len(lines) + collastmod = [None] * len(lines) for i, l in enumerate(lines): colnames[i], lastmod = rx.findall(l.decode('utf-8')).pop() # get the Unix timestamp value for a modification time - collastmod[i] = gravtk.utilities.get_unix_time(lastmod, - format='%Y-%m-%d %H:%M') + collastmod[i] = gravtk.utilities.get_unix_time( + lastmod, format='%Y-%m-%d %H:%M' + ) # reduce using regular expression pattern if pattern: - i = [i for i,f in enumerate(colnames) if re.search(pattern, f)] + i = [i for i, f in enumerate(colnames) if re.search(pattern, f)] # reduce list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # sort the list if sort: - i = [i for i,j in sorted(enumerate(colnames), key=lambda i: i[1])] + i = [i for i, j in sorted(enumerate(colnames), key=lambda i: i[1])] # sort list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # return the list of column names and last modified times return (colnames, collastmod) + # PURPOSE: pull file from a remote host checking if file exists locally # and if the remote file is newer than the local file -def http_pull_file(remote_path, remote_mtime, local_file, - TIMEOUT=0, LIST=False, CLOBBER=False, MODE=0o775): +def http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=0, + LIST=False, + CLOBBER=False, + MODE=0o775, +): # verify inputs for remote http host if isinstance(remote_path, str): remote_path = gravtk.utilities.url_split(remote_path) @@ -241,8 +296,9 @@ def http_pull_file(remote_path, remote_mtime, local_file, # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -258,8 +314,9 @@ def http_pull_file(remote_path, remote_mtime, local_file, # Create and submit request. There are a wide range of exceptions # that can be thrown here, including HTTPError and URLError. request = gravtk.utilities.urllib2.Request(remote_file) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=TIMEOUT + ) # chunked transfer encoding size CHUNK = 16 * 1024 # copy contents to local file using chunked transfer encoding @@ -270,6 +327,7 @@ def http_pull_file(remote_path, remote_mtime, local_file, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -279,64 +337,113 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 b641e5aa..1106da52 100644 --- a/access/gfz_isdc_grace_sync.py +++ b/access/gfz_isdc_grace_sync.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gfz_isdc_grace_sync.py Written by Tyler Sutterley (10/2025) Syncs GRACE/GRACE-FO data from the GFZ Information System and Data Center (ISDC) @@ -59,6 +59,7 @@ added GRACE Follow-On data sync Written 08/2018 """ + from __future__ import print_function import sys @@ -74,11 +75,20 @@ import posixpath import gravity_toolkit as gravtk -# PURPOSE: sync local GRACE/GRACE-FO files with GFZ ISDC server -def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], - NEWSLETTERS=False, TIMEOUT=None, LOG=False, LIST=False, - CLOBBER=False, MODE=None): +# PURPOSE: sync local GRACE/GRACE-FO files with GFZ ISDC server +def gfz_isdc_grace_sync( + DIRECTORY, + PROC=[], + DREL=[], + VERSION=[], + NEWSLETTERS=False, + TIMEOUT=None, + LOG=False, + LIST=False, + CLOBBER=False, + MODE=None, +): # check if directory exists and recursively create if not DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) @@ -86,7 +96,7 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # GFZ ISDC https host HOST = 'https://isdc-data.gfz.de/' # mission shortnames - shortname = {'grace':'GRAC', 'grace-fo':'GRFO'} + shortname = {'grace': 'GRAC', 'grace-fo': 'GRFO'} # datasets for each processing center DSET = {} DSET['CSR'] = ['GAC', 'GAD', 'GSM'] @@ -97,7 +107,7 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], if LOG: # output to log file # format: GFZ_ISDC_sync_2002-04-01.log - today = time.strftime('%Y-%m-%d',time.localtime()) + today = time.strftime('%Y-%m-%d', time.localtime()) LOGFILE = DIRECTORY.joinpath(f'GFZ_ISDC_sync_{today}.log') logging.basicConfig(filename=LOGFILE, level=logging.INFO) logging.info(f'GFZ ISDC Sync Log ({today})') @@ -116,51 +126,78 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # compile regular expression operator for remote files R1 = re.compile(r'TN-13_GEOC_(CSR|GFZ|JPL)_(.*?).txt$', re.VERBOSE) # get filenames from remote directory - remote_files,remote_mtimes = http_list( - [HOST,'grace-fo','DOCUMENTS','TECHNICAL_NOTES'], - timeout=TIMEOUT, pattern=R1, sort=True) + remote_files, remote_mtimes = http_list( + [HOST, 'grace-fo', 'DOCUMENTS', 'TECHNICAL_NOTES'], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) # for each file on the remote server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,'grace-fo','DOCUMENTS','TECHNICAL_NOTES',fi] + remote_path = [HOST, 'grace-fo', 'DOCUMENTS', 'TECHNICAL_NOTES', fi] local_file = local_dir.joinpath(fi) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # SLR C2,0 coefficients logging.info('C2,0 Coefficients:') # compile regular expression operator for remote files R1 = re.compile(r'TN-(05|07|11)_C20_SLR_RL(.*?).txt$', re.VERBOSE) # get filenames from remote directory - remote_files,remote_mtimes = http_list( - [HOST,'grace','DOCUMENTS','TECHNICAL_NOTES'], - timeout=TIMEOUT, pattern=R1, sort=True) + remote_files, remote_mtimes = http_list( + [HOST, 'grace', 'DOCUMENTS', 'TECHNICAL_NOTES'], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) # for each file on the remote server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,'grace','DOCUMENTS','TECHNICAL_NOTES',fi] - local_file = DIRECTORY.joinpath(re.sub(r'(_RL.*?).txt','.txt',fi)) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + remote_path = [HOST, 'grace', 'DOCUMENTS', 'TECHNICAL_NOTES', fi] + local_file = DIRECTORY.joinpath(re.sub(r'(_RL.*?).txt', '.txt', fi)) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # SLR C3,0 coefficients logging.info('C3,0 Coefficients:') # compile regular expression operator for remote files R1 = re.compile(r'TN-(14)_C30_C20_SLR_GSFC.txt$', re.VERBOSE) # get filenames from remote directory - remote_files,remote_mtimes = http_list( - [HOST,'grace-fo','DOCUMENTS','TECHNICAL_NOTES'], - timeout=TIMEOUT, pattern=R1, sort=True) + remote_files, remote_mtimes = http_list( + [HOST, 'grace-fo', 'DOCUMENTS', 'TECHNICAL_NOTES'], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) # for each file on the remote server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,'grace-fo','DOCUMENTS','TECHNICAL_NOTES',fi] - local_file = DIRECTORY.joinpath(re.sub(r'(SLR_GSFC)','GSFC_SLR',fi)) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + remote_path = [HOST, 'grace-fo', 'DOCUMENTS', 'TECHNICAL_NOTES', fi] + local_file = DIRECTORY.joinpath(re.sub(r'(SLR_GSFC)', 'GSFC_SLR', fi)) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # TN-08 GAE, TN-09 GAF and TN-10 GAG ECMWF atmosphere correction products logging.info('TN-08 GAE, TN-09 GAF and TN-10 GAG products:') @@ -171,17 +208,26 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # compile regular expression operator for remote files R1 = re.compile(r'({0}|{1}|{2})'.format(*ECMWF_files), re.VERBOSE) # get filenames from remote directory - remote_files,remote_mtimes = http_list( - [HOST,'grace','DOCUMENTS','TECHNICAL_NOTES'], - timeout=TIMEOUT, pattern=R1, sort=True) + remote_files, remote_mtimes = http_list( + [HOST, 'grace', 'DOCUMENTS', 'TECHNICAL_NOTES'], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) # for each file on the remote server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,'grace','DOCUMENTS','TECHNICAL_NOTES',fi] + remote_path = [HOST, 'grace', 'DOCUMENTS', 'TECHNICAL_NOTES', fi] local_file = DIRECTORY.joinpath(fi) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # GRACE and GRACE-FO newsletters if NEWSLETTERS: @@ -190,30 +236,41 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # check if newsletters directory exists and recursively create if not local_dir.mkdir(mode=MODE, parents=True, exist_ok=True) # for each satellite mission (grace, grace-fo) - for i,mi in enumerate(['grace','grace-fo']): + for i, mi in enumerate(['grace', 'grace-fo']): logging.info(f'{mi} Newsletters:') # compile regular expression operator for remote files - NAME = mi.upper().replace('-','_') + NAME = mi.upper().replace('-', '_') R1 = re.compile(rf'{NAME}_SDS_NL_(\d+).pdf', re.VERBOSE) # find years for GRACE/GRACE-FO newsletters - years,_ = http_list([HOST,mi,'DOCUMENTS','NEWSLETTER'], - timeout=TIMEOUT, pattern=r'\d+', - sort=True) + years, _ = http_list( + [HOST, mi, 'DOCUMENTS', 'NEWSLETTER'], + timeout=TIMEOUT, + pattern=r'\d+', + sort=True, + ) # for each year of GRACE/GRACE-FO newsletters for Y in years: # find GRACE/GRACE-FO newsletters - remote_files,remote_mtimes = http_list( - [HOST,mi,'DOCUMENTS','NEWSLETTER',Y], - timeout=TIMEOUT, pattern=R1, - sort=True) + remote_files, remote_mtimes = http_list( + [HOST, mi, 'DOCUMENTS', 'NEWSLETTER', Y], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) # for each file on the remote server - for fi,remote_mtime in zip(remote_files,remote_mtimes): + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,mi,'DOCUMENTS','NEWSLETTER',Y,fi] + remote_path = [HOST, mi, 'DOCUMENTS', 'NEWSLETTER', Y, fi] local_file = local_dir.joinpath(fi) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # GRACE/GRACE-FO level-2 spherical harmonic products logging.info('GRACE/GRACE-FO L2 Global Spherical Harmonics:') @@ -230,9 +287,9 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # list of GRACE/GRACE-FO files for index grace_files = [] # for each satellite mission (grace, grace-fo) - for i,mi in enumerate(['grace','grace-fo']): + for i, mi in enumerate(['grace', 'grace-fo']): # modifiers for intermediate data releases - if (int(VERSION[i]) > 0): + if int(VERSION[i]) > 0: drel_str = f'{rl}.{VERSION[i]}' else: drel_str = copy.copy(rl) @@ -241,22 +298,37 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], # compile the regular expression operator to find files R1 = re.compile(rf'({ds}-(.*?)(gz|txt|dif))') # get filenames from remote directory - remote_files,remote_mtimes = http_list( - [HOST,mi,'Level-2',pr,drel_str], timeout=TIMEOUT, - pattern=R1, sort=True) - for fi,remote_mtime in zip(remote_files,remote_mtimes): + remote_files, remote_mtimes = http_list( + [HOST, mi, 'Level-2', pr, drel_str], + timeout=TIMEOUT, + pattern=R1, + sort=True, + ) + for fi, remote_mtime in zip(remote_files, remote_mtimes): # extract filename from regex object - remote_path = [HOST,mi,'Level-2',pr,drel_str,fi] + remote_path = [HOST, mi, 'Level-2', pr, drel_str, fi] local_file = local_dir.joinpath(fi) - http_pull_file(remote_path, remote_mtime, - local_file, TIMEOUT=TIMEOUT, LIST=LIST, - CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=TIMEOUT, + LIST=LIST, + CLOBBER=CLOBBER, + MODE=MODE, + ) # regular expression operator for data product rx = gravtk.utilities.compile_regex_pattern( - pr, rl, ds, mission=shortname[mi]) + pr, rl, ds, mission=shortname[mi] + ) # find local GRACE/GRACE-FO files to create index - granules = sorted([f.name for f in local_dir.iterdir() - if rx.match(f.name)]) + granules = sorted( + [ + f.name + for f in local_dir.iterdir() + if rx.match(f.name) + ] + ) # reduce list of GRACE/GRACE-FO files to unique dates granules = gravtk.time.reduce_by_date(granules) # extend list of GRACE/GRACE-FO files with granules @@ -274,14 +346,15 @@ def gfz_isdc_grace_sync(DIRECTORY, PROC=[], DREL=[], VERSION=[], if LOG: LOGFILE.chmod(mode=MODE) + # PURPOSE: list a directory on the GFZ https server def http_list( - HOST: str | list, - timeout: int | None = None, - context: ssl.SSLContext = gravtk.utilities._default_ssl_context, - pattern: str | re.Pattern = '', - sort: bool = False - ): + HOST: str | list, + timeout: int | None = None, + context: ssl.SSLContext = gravtk.utilities._default_ssl_context, + pattern: str | re.Pattern = '', + sort: bool = False, +): """ List a directory on the GFZ https Server @@ -315,8 +388,9 @@ def http_list( try: # Create and submit request. request = gravtk.utilities.urllib2.Request(posixpath.join(*HOST)) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=timeout, context=context) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=timeout, context=context + ) except Exception as exc: raise Exception('List error from {0}'.format(posixpath.join(*HOST))) # read the directory listing @@ -324,32 +398,41 @@ def http_list( # read and parse request for files (column names and modified times) lines = [l for l in contents if rx.search(l.decode('utf-8'))] # column names and last modified times - colnames = [None]*len(lines) - collastmod = [None]*len(lines) + colnames = [None] * len(lines) + collastmod = [None] * len(lines) for i, l in enumerate(lines): colnames[i], lastmod = rx.findall(l.decode('utf-8')).pop() # get the Unix timestamp value for a modification time - collastmod[i] = gravtk.utilities.get_unix_time(lastmod, - format='%Y-%m-%d %H:%M') + collastmod[i] = gravtk.utilities.get_unix_time( + lastmod, format='%Y-%m-%d %H:%M' + ) # reduce using regular expression pattern if pattern: - i = [i for i,f in enumerate(colnames) if re.search(pattern, f)] + i = [i for i, f in enumerate(colnames) if re.search(pattern, f)] # reduce list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # sort the list if sort: - i = [i for i,j in sorted(enumerate(colnames), key=lambda i: i[1])] + i = [i for i, j in sorted(enumerate(colnames), key=lambda i: i[1])] # sort list of column names and last modified times colnames = [colnames[indice] for indice in i] collastmod = [collastmod[indice] for indice in i] # return the list of column names and last modified times return (colnames, collastmod) + # PURPOSE: pull file from a remote host checking if file exists locally # and if the remote file is newer than the local file -def http_pull_file(remote_path, remote_mtime, local_file, - TIMEOUT=0, LIST=False, CLOBBER=False, MODE=0o775): +def http_pull_file( + remote_path, + remote_mtime, + local_file, + TIMEOUT=0, + LIST=False, + CLOBBER=False, + MODE=0o775, +): # verify inputs for remote http host if isinstance(remote_path, str): remote_path = gravtk.utilities.url_split(remote_path) @@ -364,8 +447,9 @@ def http_pull_file(remote_path, remote_mtime, local_file, # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -381,8 +465,9 @@ def http_pull_file(remote_path, remote_mtime, local_file, # Create and submit request. There are a wide range of exceptions # that can be thrown here, including HTTPError and URLError. request = gravtk.utilities.urllib2.Request(remote_file) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen( + request, timeout=TIMEOUT + ) # chunked transfer encoding size CHUNK = 16 * 1024 # copy contents to local file using chunked transfer encoding @@ -393,6 +478,7 @@ def http_pull_file(remote_path, remote_mtime, local_file, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -402,69 +488,123 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 c765a4cd..ec3a9423 100755 --- a/access/itsg_graz_grace_sync.py +++ b/access/itsg_graz_grace_sync.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" itsg_graz_grace_sync.py -Written by Tyler Sutterley (05/2023) +Written by Tyler Sutterley (07/2026) Syncs GRACE/GRACE-FO and auxiliary data from the ITSG GRAZ server CALLING SEQUENCE: @@ -39,6 +39,7 @@ utilities.py: download and management utilities for syncing files UPDATE HISTORY: + Updated 07/2026: ITSG GRACE server moved from outgoing to pub Updated 05/2023: use pathlib to define and operate on paths Updated 12/2022: single implicit import of gravity toolkit Updated 11/2022: use f-strings for formatting verbose or ascii output @@ -46,6 +47,7 @@ Updated 10/2021: using python logging for handling verbose output Written 09/2021 """ + from __future__ import print_function import sys @@ -59,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) @@ -71,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})') @@ -82,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','outgoing','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 @@ -95,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 = {} @@ -113,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: @@ -171,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)' @@ -184,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: @@ -201,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 @@ -213,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( @@ -222,55 +262,98 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 dcae0784..a49f445c 100644 --- a/access/podaac_cumulus.py +++ b/access/podaac_cumulus.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" podaac_cumulus.py Written by Tyler Sutterley (11/2024) @@ -70,6 +70,7 @@ use argparse descriptions within sphinx documentation Written 03/2022 with release of PO.DAAC Cumulus """ + from __future__ import print_function import sys @@ -83,17 +84,28 @@ import argparse import gravity_toolkit as gravtk -# PURPOSE: sync local GRACE/GRACE-FO files with JPL PO.DAAC AWS S3 bucket -def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], - AOD1B=False, ENDPOINT='s3', TIMEOUT=None, GZIP=False, LOG=False, - CLOBBER=False, MODE=None): +# PURPOSE: sync local GRACE/GRACE-FO files with JPL PO.DAAC AWS S3 bucket +def podaac_cumulus( + client, + DIRECTORY, + PROC=[], + DREL=[], + VERSION=[], + AOD1B=False, + ENDPOINT='s3', + TIMEOUT=None, + GZIP=False, + LOG=False, + CLOBBER=False, + MODE=None, +): # check if directory exists and recursively create if not DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) # mission shortnames - shortname = {'grace':'GRAC', 'grace-fo':'GRFO'} + shortname = {'grace': 'GRAC', 'grace-fo': 'GRFO'} # default bucket for GRACE/GRACE-FO bucket bucket = gravtk.utilities._s3_buckets['podaac'] # datasets for each processing center @@ -135,13 +147,17 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], for version in set(VERSION): # query CMR for product metadata urls = gravtk.utilities.cmr_metadata( - mission='grace-fo', center=pr, release=rl, - version=version, provider='POCLOUD', - endpoint='documentation') + mission='grace-fo', + center=pr, + release=rl, + version=version, + provider='POCLOUD', + endpoint='documentation', + ) # TN-13 JPL degree 1 files try: - url, = [url for url in urls if R1.search(url)] + (url,) = [url for url in urls if R1.search(url)] except ValueError as exc: logging.info('No TN-13 Files Available') url = None @@ -150,18 +166,25 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], local_file = local_dir.joinpath(granule) # access auxiliary data from endpoint if (ENDPOINT == 'data') and (url is not None): - http_pull_file(url, mtime, local_file, - TIMEOUT=TIMEOUT, CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + url, + mtime, + local_file, + TIMEOUT=TIMEOUT, + CLOBBER=CLOBBER, + MODE=MODE, + ) elif (ENDPOINT == 's3') and (url is not None): bucket = gravtk.utilities.s3_bucket(url) key = gravtk.utilities.s3_key(url) response = client.get_object(Bucket=bucket, Key=key) - s3_pull_file(response, mtime, local_file, - CLOBBER=CLOBBER, MODE=MODE) + s3_pull_file( + response, mtime, local_file, CLOBBER=CLOBBER, MODE=MODE + ) # TN-14 SLR C2,0 and C3,0 files try: - url, = [url for url in urls if R2.search(url)] + (url,) = [url for url in urls if R2.search(url)] except ValueError as exc: logging.info('No TN-14 Files Available') url = None @@ -170,14 +193,21 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], local_file = DIRECTORY.joinpath(granule) # access auxiliary data from endpoint if (ENDPOINT == 'data') and (url is not None): - http_pull_file(url, mtime, local_file, - TIMEOUT=TIMEOUT, CLOBBER=CLOBBER, MODE=MODE) + http_pull_file( + url, + mtime, + local_file, + TIMEOUT=TIMEOUT, + CLOBBER=CLOBBER, + MODE=MODE, + ) elif (ENDPOINT == 's3') and (url is not None): bucket = gravtk.utilities.s3_bucket(url) key = gravtk.utilities.s3_key(url) response = client.get_object(Bucket=bucket, Key=key) - s3_pull_file(response, mtime, local_file, - CLOBBER=CLOBBER, MODE=MODE) + s3_pull_file( + response, mtime, local_file, CLOBBER=CLOBBER, MODE=MODE + ) # GRACE/GRACE-FO AOD1B dealiasing products if AOD1B: @@ -187,41 +217,56 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], # print string of exact data product logging.info(f'GFZ/AOD1B/{rl}') # local directory for exact data product - local_dir = DIRECTORY.joinpath('AOD1B',rl) + local_dir = DIRECTORY.joinpath('AOD1B', rl) # check if directory exists and recursively create if not local_dir.mkdir(mode=MODE, parents=True, exist_ok=True) # test connection to s3 bucket - if (ENDPOINT == 's3'): + if ENDPOINT == 's3': # get shortname for CMR query - cmr_shortname, = gravtk.utilities.cmr_product_shortname( - mission='grace', center='GFZ', release=rl, level='L1B') + (cmr_shortname,) = gravtk.utilities.cmr_product_shortname( + mission='grace', center='GFZ', release=rl, level='L1B' + ) # attempt to list objects in s3 bucket try: - objects = client.list_objects(Bucket=bucket, - Prefix=cmr_shortname) + objects = client.list_objects( + Bucket=bucket, Prefix=cmr_shortname + ) except Exception as exc: message = f'Error accessing S3 bucket {bucket}' raise Exception(message) from exc # query CMR for dataset - ids,urls,mtimes = gravtk.utilities.cmr( - mission='grace', level='L1B', center='GFZ', release=rl, - product='AOD1B', start_date='2002-01-01T00:00:00', - provider='POCLOUD', endpoint=ENDPOINT) + ids, urls, mtimes = gravtk.utilities.cmr( + mission='grace', + level='L1B', + center='GFZ', + release=rl, + product='AOD1B', + start_date='2002-01-01T00:00:00', + provider='POCLOUD', + endpoint=ENDPOINT, + ) # for each model id and url - for id,url,mtime in zip(ids,urls,mtimes): + for id, url, mtime in zip(ids, urls, mtimes): # retrieve GRACE/GRACE-FO files granule = gravtk.utilities.url_split(url)[-1] local_file = local_dir.joinpath(granule) # access data from endpoint - if (ENDPOINT == 'data'): - http_pull_file(url, mtime, local_file, - TIMEOUT=TIMEOUT, CLOBBER=CLOBBER, MODE=MODE) - elif (ENDPOINT == 's3'): + if ENDPOINT == 'data': + http_pull_file( + url, + mtime, + local_file, + TIMEOUT=TIMEOUT, + CLOBBER=CLOBBER, + MODE=MODE, + ) + elif ENDPOINT == 's3': bucket = gravtk.utilities.s3_bucket(url) key = gravtk.utilities.s3_key(url) response = client.get_object(Bucket=bucket, Key=key) - s3_pull_file(response, mtime, local_file, - CLOBBER=CLOBBER, MODE=MODE) + s3_pull_file( + response, mtime, local_file, CLOBBER=CLOBBER, MODE=MODE + ) # GRACE/GRACE-FO level-2 spherical harmonic products logging.info('GRACE/GRACE-FO L2 Global Spherical Harmonics:') @@ -238,49 +283,76 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], # list of GRACE/GRACE-FO files for index grace_files = [] # for each satellite mission (grace, grace-fo) - for i,mi in enumerate(['grace','grace-fo']): + for i, mi in enumerate(['grace', 'grace-fo']): # print string of exact data product logging.info(f'{mi} {pr}/{rl}/{ds}') # test connection to s3 bucket - if (ENDPOINT == 's3'): + if ENDPOINT == 's3': # get shortname for CMR query - cmr_shortname, = gravtk.utilities.cmr_product_shortname( - mission=mi, center=pr, release=rl, product=ds) + (cmr_shortname,) = ( + gravtk.utilities.cmr_product_shortname( + mission=mi, center=pr, release=rl, product=ds + ) + ) # attempt to list objects in s3 bucket try: - objects = client.list_objects(Bucket=bucket, - Prefix=cmr_shortname) + objects = client.list_objects( + Bucket=bucket, Prefix=cmr_shortname + ) except Exception as exc: message = f'Error accessing S3 bucket {bucket}' raise Exception(message) from exc # query CMR for dataset - ids,urls,mtimes = gravtk.utilities.cmr( - mission=mi, center=pr, release=rl, product=ds, - version=VERSION[i], provider='POCLOUD', - endpoint=ENDPOINT) + ids, urls, mtimes = gravtk.utilities.cmr( + mission=mi, + center=pr, + release=rl, + product=ds, + version=VERSION[i], + provider='POCLOUD', + endpoint=ENDPOINT, + ) # regular expression operator for data product rx = gravtk.utilities.compile_regex_pattern( - pr, rl, ds, mission=shortname[mi]) + pr, rl, ds, mission=shortname[mi] + ) # for each model id and url - for id,url,mtime in zip(ids,urls,mtimes): + for id, url, mtime in zip(ids, urls, mtimes): # retrieve GRACE/GRACE-FO files granule = gravtk.utilities.url_split(url)[-1] suffix = '.gz' if GZIP else '' local_file = local_dir.joinpath(f'{granule}{suffix}') # access data from endpoint - if (ENDPOINT == 'data'): - http_pull_file(url, mtime, local_file, - GZIP=GZIP, TIMEOUT=TIMEOUT, - CLOBBER=CLOBBER, MODE=MODE) - elif (ENDPOINT == 's3'): + if ENDPOINT == 'data': + http_pull_file( + url, + mtime, + local_file, + GZIP=GZIP, + TIMEOUT=TIMEOUT, + CLOBBER=CLOBBER, + MODE=MODE, + ) + elif ENDPOINT == 's3': bucket = gravtk.utilities.s3_bucket(url) key = gravtk.utilities.s3_key(url) response = client.get_object(Bucket=bucket, Key=key) - s3_pull_file(response, mtime, local_file, - GZIP=GZIP, CLOBBER=CLOBBER, MODE=MODE) + s3_pull_file( + response, + mtime, + local_file, + GZIP=GZIP, + CLOBBER=CLOBBER, + MODE=MODE, + ) # find local GRACE/GRACE-FO files to create index - granules = sorted([f.name for f in local_dir.iterdir() - if rx.match(f.name)]) + granules = sorted( + [ + f.name + for f in local_dir.iterdir() + if rx.match(f.name) + ] + ) # reduce list of GRACE/GRACE-FO files to unique dates granules = gravtk.time.reduce_by_date(granules) # extend list of GRACE/GRACE-FO files with granules @@ -298,10 +370,18 @@ def podaac_cumulus(client, DIRECTORY, PROC=[], DREL=[], VERSION=[], if LOG: LOGFILE.chmod(mode=MODE) + # PURPOSE: pull file from a remote host checking if file exists locally # and if the remote file is newer than the local file -def http_pull_file(remote_file, remote_mtime, local_file, - GZIP=False, TIMEOUT=120, CLOBBER=False, MODE=0o775): +def http_pull_file( + remote_file, + remote_mtime, + local_file, + GZIP=False, + TIMEOUT=120, + CLOBBER=False, + MODE=0o775, +): # if file exists in file system: check if remote file is newer TEST = False OVERWRITE = ' (clobber)' @@ -311,8 +391,9 @@ def http_pull_file(remote_file, remote_mtime, local_file, # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -329,8 +410,7 @@ def http_pull_file(remote_file, remote_mtime, local_file, # There are a range of exceptions that can be thrown here # including HTTPError and URLError. request = gravtk.utilities.urllib2.Request(remote_file) - response = gravtk.utilities.urllib2.urlopen(request, - timeout=TIMEOUT) + response = gravtk.utilities.urllib2.urlopen(request, timeout=TIMEOUT) # copy remote file contents to local file if GZIP: with gzip.GzipFile(local_file, 'wb', 9, None, remote_mtime) as f: @@ -342,10 +422,12 @@ def http_pull_file(remote_file, remote_mtime, local_file, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: pull file from AWS s3 bucket checking if file exists locally # and if the remote file is newer than the local file -def s3_pull_file(response, remote_mtime, local_file, - GZIP=False, CLOBBER=False, MODE=0o775): +def s3_pull_file( + response, remote_mtime, local_file, GZIP=False, CLOBBER=False, MODE=0o775 +): # if file exists in file system: check if remote file is newer TEST = False OVERWRITE = ' (clobber)' @@ -355,8 +437,9 @@ def s3_pull_file(response, remote_mtime, local_file, # check last modification time of local file local_mtime = local_file.stat().st_mtime # if remote file is newer: overwrite the local file - if (gravtk.utilities.even(remote_mtime) > - gravtk.utilities.even(local_mtime)): + if gravtk.utilities.even(remote_mtime) > gravtk.utilities.even( + local_mtime + ): TEST = True OVERWRITE = ' (overwrite)' else: @@ -379,6 +462,7 @@ def s3_pull_file(response, remote_mtime, local_file, os.utime(local_file, (local_file.stat().st_atime, remote_mtime)) local_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -388,71 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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' @@ -460,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 f763322a..c96f1440 100644 --- a/dealiasing/aod1b_geocenter.py +++ b/dealiasing/aod1b_geocenter.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" aod1b_geocenter.py Written by Tyler Sutterley (05/2023) Contributions by Hugo Lecomte (03/2021) @@ -57,6 +57,7 @@ Updated 05-06/2016: oba=ocean bottom pressure, absolute import of shutil Written 05/2016 """ + from __future__ import print_function, division import sys @@ -69,12 +70,9 @@ import numpy as np import gravity_toolkit as gravtk + # program module to read the degree 1 coefficients of the AOD1b data -def aod1b_geocenter(base_dir, - DREL='', - DSET='', - CLOBBER=False, - MODE=0o775): +def aod1b_geocenter(base_dir, DREL='', DSET='', CLOBBER=False, MODE=0o775): """ Creates monthly files of geocenter variations at 6-hour or 3-hour intervals from GRACE/GRACE-FO level-1b dealiasing data files @@ -113,7 +111,7 @@ def aod1b_geocenter(base_dir, # set number of hours in a file # set the atmospheric and ocean model for a given release # set the maximum degree and order of a release - if DREL in ('RL01','RL02','RL03','RL04','RL05'): + if DREL in ('RL01', 'RL02', 'RL03', 'RL04', 'RL05'): # for 00, 06, 12 and 18 n_time = 4 ATMOSPHERE = 'ECMWF' @@ -128,7 +126,7 @@ def aod1b_geocenter(base_dir, else: raise ValueError('Invalid data release') # Calculating the number of cos and sin harmonics up to LMAX - n_harm = (LMAX**2 + 3*LMAX)//2 + 1 + n_harm = (LMAX**2 + 3 * LMAX) // 2 + 1 # AOD1B data products product = {} @@ -139,7 +137,7 @@ def aod1b_geocenter(base_dir, # AOD1B directory and output geocenter directory base_dir = pathlib.Path(base_dir).expanduser().absolute() - grace_dir = base_dir.joinpath('AOD1B',DREL) + grace_dir = base_dir.joinpath('AOD1B', DREL) output_dir = grace_dir.joinpath('geocenter') output_dir.mkdir(mode=MODE, parents=True, exist_ok=True) @@ -149,8 +147,8 @@ def aod1b_geocenter(base_dir, # for each tar file for input_file in sorted(input_tar_files): # extract the year and month from the file - YY,MM,SFX = tx.findall(input_file.name).pop() - YY,MM = np.array([YY, MM], dtype=np.int64) + YY, MM, SFX = tx.findall(input_file.name).pop() + YY, MM = np.array([YY, MM], dtype=np.int64) # output monthly geocenter file FILE = f'AOD1B_{DREL}_{DSET}_{YY:4d}_{MM:02d}.txt' output_file = output_dir.joinpath(FILE) @@ -163,7 +161,7 @@ def aod1b_geocenter(base_dir, input_mtime = input_file.stat().st_mtime output_mtime = output_file.stat().st_mtime # if input tar file is newer: overwrite the output file - if (input_mtime > output_mtime): + if input_mtime > output_mtime: TEST = True OVERWRITE = ' (overwrite)' else: @@ -179,7 +177,7 @@ def aod1b_geocenter(base_dir, args = ('Geocenter time series', DREL, DSET) print('# {0} from {1} AOD1b {2} Product'.format(*args), file=f) print('# {0}'.format(product[DSET]), file=f) - args = ('ISO-Time','X','Y','Z') + args = ('ISO-Time', 'X', 'Y', 'Z') print('# {0:^15} {1:^12} {2:^12} {3:^12}'.format(*args), file=f) # open the AOD1B monthly tar file @@ -190,10 +188,10 @@ def aod1b_geocenter(base_dir, # track tar file members logging.debug(member.name) # get calendar day from file - DD,SFX = fx.findall(member.name).pop() + DD, SFX = fx.findall(member.name).pop() DD = np.int64(DD) # open data file for day - if (SFX == '.gz'): + if SFX == '.gz': fid = gzip.GzipFile(fileobj=tar.extractfile(member)) else: fid = tar.extractfile(member) @@ -207,7 +205,7 @@ def aod1b_geocenter(base_dir, # create counter for hour in dataset c = 0 # while loop ends when dataset is read - while (c < n_time): + while c < n_time: # read line file_contents = fid.readline().decode('ISO-8859-1') # find file header for data product @@ -215,10 +213,10 @@ def aod1b_geocenter(base_dir, # track file header lines logging.debug(file_contents) # extract hour from header and convert to float - HH, = re.findall(r'(\d+):\d+:\d+',file_contents) + (HH,) = re.findall(r'(\d+):\d+:\d+', file_contents) hours[c] = np.int64(HH) # read each line of spherical harmonics - for k in range(0,n_harm): + for k in range(0, n_harm): file_contents = fid.readline().decode('ISO-8859-1') # find numerical instances in the data line line_contents = rx.findall(file_contents) @@ -237,8 +235,8 @@ def aod1b_geocenter(base_dir, # convert from spherical harmonics into geocenter DEG1.to_cartesian() # write to file for each hour (iterates each 6-hour block) - for h,X,Y,Z in zip(hours,DEG1.X,DEG1.Y,DEG1.Z): - print(fstr.format(YY,MM,DD,h,X,Y,Z), file=f) + for h, X, Y, Z in zip(hours, DEG1.X, DEG1.Y, DEG1.Z): + print(fstr.format(YY, MM, DD, h, X, Y, Z), file=f) # close the tar file tar.close() @@ -247,49 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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]) @@ -297,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 047d2cac..a4fbff82 100644 --- a/dealiasing/aod1b_oblateness.py +++ b/dealiasing/aod1b_oblateness.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" aod1b_oblateness.py Written by Tyler Sutterley (05/2023) Contributions by Hugo Lecomte (03/2021) @@ -57,6 +57,7 @@ Updated 05-06/2016: oba=ocean bottom pressure, absolute import of shutil Written 05/2016 """ + from __future__ import print_function, division import sys @@ -69,12 +70,9 @@ import numpy as np import gravity_toolkit as gravtk + # program module to read the C20 coefficients of the AOD1b data -def aod1b_oblateness(base_dir, - DREL='', - DSET='', - CLOBBER=False, - MODE=0o775): +def aod1b_oblateness(base_dir, DREL='', DSET='', CLOBBER=False, MODE=0o775): """ Creates monthly files of oblateness (C20) variations at 6-hour intervals from GRACE/GRACE-FO level-1b dealiasing data files @@ -114,7 +112,7 @@ def aod1b_oblateness(base_dir, # set number of hours in a file # set the atmospheric and ocean model for a given release # set the maximum degree and order of a release - if DREL in ('RL01','RL02','RL03','RL04','RL05'): + if DREL in ('RL01', 'RL02', 'RL03', 'RL04', 'RL05'): # for 00, 06, 12 and 18 n_time = 4 ATMOSPHERE = 'ECMWF' @@ -129,7 +127,7 @@ def aod1b_oblateness(base_dir, else: raise ValueError('Invalid data release') # Calculating the number of cos and sin harmonics up to LMAX - n_harm = (LMAX**2 + 3*LMAX)//2 + 1 + n_harm = (LMAX**2 + 3 * LMAX) // 2 + 1 # AOD1B data products product = {} @@ -140,7 +138,7 @@ def aod1b_oblateness(base_dir, # AOD1B directory and output oblateness directory base_dir = pathlib.Path(base_dir).expanduser().absolute() - grace_dir = base_dir.joinpath('AOD1B',DREL) + grace_dir = base_dir.joinpath('AOD1B', DREL) output_dir = grace_dir.joinpath('oblateness') output_dir.mkdir(mode=MODE, parents=True, exist_ok=True) @@ -150,8 +148,8 @@ def aod1b_oblateness(base_dir, # for each tar file for input_file in sorted(input_tar_files): # extract the year and month from the file - YY,MM,SFX = tx.findall(input_file.name).pop() - YY,MM = np.array([YY, MM], dtype=np.int64) + YY, MM, SFX = tx.findall(input_file.name).pop() + YY, MM = np.array([YY, MM], dtype=np.int64) # output monthly oblateness file FILE = f'AOD1B_{DREL}_{DSET}_{YY:4d}_{MM:02d}.txt' output_file = output_dir.joinpath(FILE) @@ -164,7 +162,7 @@ def aod1b_oblateness(base_dir, input_mtime = input_file.stat().st_mtime output_mtime = output_file.stat().st_mtime # if input tar file is newer: overwrite the output file - if (input_mtime > output_mtime): + if input_mtime > output_mtime: TEST = True OVERWRITE = ' (overwrite)' else: @@ -177,10 +175,10 @@ def aod1b_oblateness(base_dir, logging.info(f'{str(output_file)}{OVERWRITE}') # open output monthly oblateness file f = output_file.open(mode='w', encoding='utf8') - args = ('Oblateness time series',DREL,DSET) + args = ('Oblateness time series', DREL, DSET) print('# {0} from {1} AOD1b {2} Product'.format(*args), file=f) print('# {0}'.format(product[DSET]), file=f) - print('# {0:^15} {1:^15}'.format('ISO-Time','C20'), file=f) + print('# {0:^15} {1:^15}'.format('ISO-Time', 'C20'), file=f) # open the AOD1B monthly tar file tar = tarfile.open(name=str(input_file), mode='r:gz') @@ -190,21 +188,21 @@ def aod1b_oblateness(base_dir, # track tar file members logging.debug(member.name) # get calendar day from file - DD,SFX = fx.findall(member.name).pop() + DD, SFX = fx.findall(member.name).pop() DD = np.int64(DD) # open datafile for day - if (SFX == '.gz'): + if SFX == '.gz': fid = gzip.GzipFile(fileobj=tar.extractfile(member)) else: fid = tar.extractfile(member) # C20 spherical harmonics for day and hours C20 = np.zeros((n_time)) - hours = np.zeros((n_time),dtype=np.int64) + hours = np.zeros((n_time), dtype=np.int64) # create counter for hour in dataset c = 0 # while loop ends when dataset is read - while (c < n_time): + while c < n_time: # read line file_contents = fid.readline().decode('ISO-8859-1') # find file header for data product @@ -212,10 +210,10 @@ def aod1b_oblateness(base_dir, # track file header lines logging.debug(file_contents) # extract hour from header and convert to float - HH, = re.findall(r'(\d+):\d+:\d+',file_contents) + (HH,) = re.findall(r'(\d+):\d+:\d+', file_contents) hours[c] = np.int64(HH) # read each line of spherical harmonics - for k in range(0,n_harm): + for k in range(0, n_harm): file_contents = fid.readline().decode('ISO-8859-1') # find numerical instances in the data line line_contents = rx.findall(file_contents) @@ -230,7 +228,7 @@ def aod1b_oblateness(base_dir, fid.close() # write to file for each hour for h in range(4): - print(fstr.format(YY,MM,DD,hours[h],C20[h]),file=f) + print(fstr.format(YY, MM, DD, hours[h], C20[h]), file=f) # close the tar file tar.close() @@ -239,49 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -290,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 fd6fa239..ca5f4597 100644 --- a/dealiasing/dealiasing_global_uplift.py +++ b/dealiasing/dealiasing_global_uplift.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" dealiasing_global_uplift.py Written by Tyler Sutterley (05/2023) @@ -71,6 +71,7 @@ Updated 03/2023: attributes from units class for output netCDF4/HDF5 files Written 03/2023 """ + from __future__ import print_function, division import sys @@ -86,6 +87,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -95,9 +97,11 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: estimates global elastic uplift due to changes in atmospheric # and oceanic loading -def dealiasing_global_uplift(base_dir, +def dealiasing_global_uplift( + base_dir, DREL=None, DSET=None, YEAR=None, @@ -108,8 +112,8 @@ def dealiasing_global_uplift(base_dir, BOUNDS=None, DATAFORM=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # input directory setup base_dir = pathlib.Path(base_dir).expanduser().absolute() grace_dir = base_dir.joinpath('AOD1B', DREL) @@ -124,7 +128,7 @@ def dealiasing_global_uplift(base_dir, # set number of hours in a file for a release # set the atmospheric and ocean model for a given release # set the maximum degree and order of a release - if DREL in ('RL01','RL02','RL03','RL04','RL05'): + if DREL in ('RL01', 'RL02', 'RL03', 'RL04', 'RL05'): # for 00, 06, 12 and 18 nt = 4 ATMOSPHERE = 'ECMWF' @@ -139,7 +143,7 @@ def dealiasing_global_uplift(base_dir, else: raise ValueError('Invalid data release') # Calculating the number of cos and sin harmonics up to LMAX - n_harm = (LMAX**2 + 3*LMAX)//2 + 1 + n_harm = (LMAX**2 + 3 * LMAX) // 2 + 1 # AOD1B data products product = {} @@ -156,10 +160,11 @@ def dealiasing_global_uplift(base_dir, attributes['ROOT']['project_version'] = DREL attributes['ROOT']['product_name'] = DSET attributes['ROOT']['product_type'] = 'gravity_field' - attributes['ROOT']['reference'] = \ + attributes['ROOT']['reference'] = ( f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # output suffix for data formats - suffix = dict(ascii='txt',netCDF4='nc',HDF5='H5') + suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # compile regular expressions operators for file dates # will extract the year and month from the tar file (.tar.gz) @@ -179,33 +184,34 @@ def dealiasing_global_uplift(base_dir, input_tar_files = [tf for tf in grace_dir.iterdir() if tx.match(tf.name)] # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output spatial data grid = gravtk.spatial() # Output Degree Interval - if (INTERVAL == 1): + if INTERVAL == 1: # (0:360,90:-90) - n_lon = np.int64((360.0/dlon)+1.0) - n_lat = np.int64((180.0/dlat)+1.0) - grid.lon = dlon*np.arange(0,n_lon) - grid.lat = 90.0 - dlat*np.arange(0,n_lat) - elif (INTERVAL == 2): + n_lon = np.int64((360.0 / dlon) + 1.0) + n_lat = np.int64((180.0 / dlat) + 1.0) + grid.lon = dlon * np.arange(0, n_lon) + grid.lat = 90.0 - dlat * np.arange(0, n_lat) + elif INTERVAL == 2: # (Degree spacing)/2 - grid.lon = np.arange(dlon/2.0,360+dlon/2.0,dlon) - grid.lat = np.arange(90.0-dlat/2.0,-90.0-dlat/2.0,-dlat) + grid.lon = np.arange(dlon / 2.0, 360 + dlon / 2.0, dlon) + grid.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat) n_lon = len(grid.lon) n_lat = len(grid.lat) - elif (INTERVAL == 3): + elif INTERVAL == 3: # non-global grid set with BOUNDS parameter - minlon,maxlon,minlat,maxlat = BOUNDS.copy() - grid.lon = np.arange(minlon+dlon/2.0, maxlon+dlon/2.0, dlon) - grid.lat = np.arange(maxlat-dlat/2.0, minlat-dlat/2.0, -dlat) + minlon, maxlon, minlat, maxlat = BOUNDS.copy() + grid.lon = np.arange(minlon + dlon / 2.0, maxlon + dlon / 2.0, dlon) + grid.lat = np.arange(maxlat - dlat / 2.0, minlat - dlat / 2.0, -dlat) n_lon = len(grid.lon) n_lat = len(grid.lat) # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth parameters attributes['ROOT']['earth_model'] = LOVE.model attributes['ROOT']['earth_love_numbers'] = LOVE.citation @@ -239,13 +245,13 @@ def dealiasing_global_uplift(base_dir, attributes['time']['standard_name'] = 'time' # Computing plms for converting to spatial domain - theta = (90.0 - grid.lat)*np.pi/180.0 + theta = np.radians(90.0 - grid.lat) PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(theta)) # 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,81 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') - parser.add_argument('--output-directory','-O', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Output directory for spatial files') + 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', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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]) @@ -491,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, @@ -502,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 d5976757..0860272f 100755 --- a/dealiasing/dealiasing_monthly_mean.py +++ b/dealiasing/dealiasing_monthly_mean.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" dealiasing_monthly_mean.py Written by Tyler Sutterley (05/2023) @@ -74,6 +74,7 @@ Updated 03/2018: copy date file from input GSM directory to output directory Written 03/2018 """ + from __future__ import print_function, division import sys @@ -88,25 +89,39 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: calculate the Julian day from the year and the day of the year # http://scienceworld.wolfram.com/astronomy/JulianDate.html def calc_julian_day(YEAR, DAY_OF_YEAR): - JD = 367.0*YEAR - np.floor(7.0*(YEAR + np.floor(10.0/12.0))/4.0) - \ - np.floor(3.0*(np.floor((YEAR + 8.0/7.0)/100.0) + 1.0)/4.0) + \ - np.floor(275.0/9.0) + np.float64(DAY_OF_YEAR) + 1721028.5 + JD = ( + 367.0 * YEAR + - np.floor(7.0 * (YEAR + np.floor(10.0 / 12.0)) / 4.0) + - np.floor(3.0 * (np.floor((YEAR + 8.0 / 7.0) / 100.0) + 1.0) / 4.0) + + np.floor(275.0 / 9.0) + + np.float64(DAY_OF_YEAR) + + 1721028.5 + ) return JD -# PURPOSE: reads the AOD1B data and outputs a monthly mean -def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, - LMAX=None, DATAFORM=None, CLOBBER=False, MODE=0o775): +# PURPOSE: reads the AOD1B data and outputs a monthly mean +def dealiasing_monthly_mean( + base_dir, + PROC=None, + DREL=None, + DSET=None, + LMAX=None, + DATAFORM=None, + CLOBBER=False, + MODE=0o775, +): # output data suffix suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # aod1b data products - aod1b_products = dict(GAA='atm',GAB='ocn',GAC='glo',GAD='oba') + aod1b_products = dict(GAA='atm', GAB='ocn', GAC='glo', GAD='oba') # compile regular expressions operator for the clm/slm headers # for the specific AOD1b product - hx = re.compile(fr'^DATA.*SET.*{aod1b_products[DSET]}',re.VERBOSE) + hx = re.compile(rf'^DATA.*SET.*{aod1b_products[DSET]}', re.VERBOSE) # compile regular expression operator to find numerical instances # will extract the data from the file regex_pattern = r'[-+]?(?:(?:\d*\.\d+)|(?:\d+\.?))(?:[Ee][+-]?\d+)?' @@ -114,7 +129,7 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # set number of hours in a file # set the ocean model for a given release - if DREL in ('RL01','RL02','RL03','RL04','RL05'): + if DREL in ('RL01', 'RL02', 'RL03', 'RL04', 'RL05'): # for 00, 06, 12 and 18 nt = 4 ATMOSPHERE = 'ECMWF' @@ -133,7 +148,7 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # Maximum spherical harmonic degree (LMAX) LMAX = default_lmax if not LMAX else LMAX # Calculating the number of cos and sin harmonics up to d/o of file - n_harm = (default_lmax**2 + 3*default_lmax)//2 + 1 + n_harm = (default_lmax**2 + 3 * default_lmax) // 2 + 1 # AOD1B data products product = {} @@ -155,11 +170,11 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # file formatting string if outputting to SHM format shm = '{0}-2_{1:4.0f}{2:03.0f}-{3:4.0f}{4:03.0f}_{5}_{6}_{7}_{8}00.gz' # center name if outputting to SHM format - if (PROC == 'CSR'): + if PROC == 'CSR': CENTER = 'UTCSR' - elif (PROC == 'GFZ'): + elif PROC == 'GFZ': CENTER = default_center - elif (PROC == 'JPL'): + elif PROC == 'JPL': CENTER = 'JPLEM' else: CENTER = default_center @@ -167,9 +182,9 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # read input DATE file from GSM data product grace_date_file = f'{PROC}_{DREL}_DATES.txt' # names and formats of GRACE/GRACE-FO date ascii file - names = ('t','mon','styr','stday','endyr','endday','total') - formats = ('f','i','i','i','i','i','i') - dtype = np.dtype({'names':names, 'formats':formats}) + names = ('t', 'mon', 'styr', 'stday', 'endyr', 'endday', 'total') + formats = ('f', 'i', 'i', 'i', 'i', 'i', 'i') + dtype = np.dtype({'names': names, 'formats': formats}) input_date_file = grace_dir.joinpath('GSM', grace_date_file) date_input = np.loadtxt(input_date_file, skiprows=1, dtype=dtype) tdec = date_input['t'] @@ -183,49 +198,73 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, output_date_file = grace_dir.joinpath(DSET, grace_date_file) f_out = output_date_file.open(mode='w', encoding='utf8') # date file header information - args = ('Mid-date','Month','Start_Day','End_Day','Total_Days') + args = ('Mid-date', 'Month', 'Start_Day', 'End_Day', 'Total_Days') print('{0} {1:>10} {2:>11} {3:>10} {4:>13}'.format(*args), file=f_out) # for each GRACE/GRACE-FO month - for t,gm in enumerate(grace_month): + for t, gm in enumerate(grace_month): # check if GRACE/GRACE-FO month crosses years - if (start_yr[t] != end_yr[t]): + if start_yr[t] != end_yr[t]: # check if start_yr is a Leap Year or Standard Year dpy = gravtk.time.calendar_days(start_yr[t]).sum() # list of Julian Days to read from both start and end year julian_days_to_read = [] # add days to read from start and end years - julian_days_to_read.extend([calc_julian_day(start_yr[t],D) - for D in range(start_day[t],dpy+1)]) - julian_days_to_read.extend([calc_julian_day(end_yr[t],D) - for D in range(1,end_day[t]+1)]) + julian_days_to_read.extend( + [ + calc_julian_day(start_yr[t], D) + for D in range(start_day[t], dpy + 1) + ] + ) + julian_days_to_read.extend( + [ + calc_julian_day(end_yr[t], D) + for D in range(1, end_day[t] + 1) + ] + ) else: # Julian Days to read going from start_day to end_day - julian_days_to_read = [calc_julian_day(start_yr[t],D) - for D in range(start_day[t],end_day[t]+1)] + julian_days_to_read = [ + calc_julian_day(start_yr[t], D) + for D in range(start_day[t], end_day[t] + 1) + ] # output filename for GRACE/GRACE-FO month - if (DATAFORM == 'SHM'): + if DATAFORM == 'SHM': MISSION = 'GRAC' if (gm <= 186) else 'GRFO' - FILE = shm.format(DSET.upper(),start_yr[t],start_day[t], - end_yr[t],end_day[t],MISSION,CENTER,'BC01',DREL[2:]) + FILE = shm.format( + DSET.upper(), + start_yr[t], + start_day[t], + end_yr[t], + end_day[t], + MISSION, + CENTER, + 'BC01', + DREL[2:], + ) else: - args = (PROC,DREL,DSET.upper(),LMAX,gm,suffix[DATAFORM]) + args = (PROC, DREL, DSET.upper(), LMAX, gm, suffix[DATAFORM]) FILE = '{0}_{1}_{2}_CLM_L{3:d}_{4:03d}.{5}'.format(*args) # complete path to output filename OUTPUT_FILE = grace_dir.joinpath(DSET, FILE) # calendar dates to read JD = np.array(julian_days_to_read) - Y,M,D,h,m,s = gravtk.time.convert_julian(JD, - astype='i', format='tuple') + Y, M, D, h, m, s = gravtk.time.convert_julian( + JD, astype='i', format='tuple' + ) # find unique year and month pairs to read - rx1='|'.join(['{0:d}-{1:02d}'.format(*p) for p in set(zip(Y,M))]) - rx2='|'.join(['{0:0d}-{1:02d}-{2:02d}'.format(*p) for p in set(zip(Y,M,D))]) + rx1 = '|'.join(['{0:d}-{1:02d}'.format(*p) for p in set(zip(Y, M))]) + rx2 = '|'.join( + ['{0:0d}-{1:02d}-{2:02d}'.format(*p) for p in set(zip(Y, M, D))] + ) # compile regular expressions operators for finding tar files tx = re.compile(rf'AOD1B_({rx1})_\d+.(tar.gz|tgz)$', re.VERBOSE) # finding all of the tar files in the AOD1b directory - input_tar_files = [tf for tf in aod1b_dir.iterdir() if tx.match(tf.name)] + input_tar_files = [ + tf for tf in aod1b_dir.iterdir() if tx.match(tf.name) + ] # compile regular expressions operators for file dates # will extract year and month and calendar day from the ascii file fx = re.compile(rf'AOD1B_({rx2})_X_\d+.asc(.gz)?$', re.VERBOSE) @@ -265,10 +304,15 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # print GRACE/GRACE-FO dates if there is a complete month of AOD if COMPLETE: # print GRACE/GRACE-FO dates to file - print((f'{tdec[t]:13.8f} {gm:03d} ' - f'{start_yr[t]:8.0f} {start_day[t]:03d} ' - f'{end_yr[t]:8.0f} {end_day[t]:03d} ' - f'{total_days[t]:8.0f}'), file=f_out) + print( + ( + f'{tdec[t]:13.8f} {gm:03d} ' + f'{start_yr[t]:8.0f} {start_day[t]:03d} ' + f'{end_yr[t]:8.0f} {end_day[t]:03d} ' + f'{total_days[t]:8.0f}' + ), + file=f_out, + ) # if there are new files, files to be rewritten or clobbered if COMPLETE and (TEST or CLOBBER): @@ -277,10 +321,11 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # allocate for the mean output harmonics Ylms = gravtk.harmonics(lmax=LMAX, mmax=LMAX) # number of time points - n_time = len(julian_days_to_read)*nt + n_time = len(julian_days_to_read) * nt # flattened harmonics object - YLMS = gravtk.harmonics(lmax=default_lmax, mmax=default_lmax, - flattened=True) + YLMS = gravtk.harmonics( + lmax=default_lmax, mmax=default_lmax, flattened=True + ) YLMS.l = np.zeros((n_harm), dtype=int) YLMS.m = np.zeros((n_harm), dtype=int) YLMS.clm = np.zeros((n_harm, n_time)) @@ -296,7 +341,9 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # open the AOD1B monthly tar file tar = tarfile.open(name=str(input_file), mode='r:gz') # for each ascii file within the tar file that matches fx - monthly_members=[m for m in tar.getmembers() if fx.match(m.name)] + monthly_members = [ + m for m in tar.getmembers() if fx.match(m.name) + ] for member in monthly_members: # track tar file members logging.debug(member.name) @@ -304,38 +351,44 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, YMD, SFX = fx.findall(member.name).pop() YY, MM, DD = re.findall(r'\d+', YMD) # open datafile for day - if (SFX == '.gz'): + if SFX == '.gz': fid = gzip.GzipFile(fileobj=tar.extractfile(member)) else: fid = tar.extractfile(member) # create counters for hour in dataset c = 0 # while loop ends when dataset is read - while (c < nt): + while c < nt: # read line - file_contents=fid.readline().decode('ISO-8859-1') + file_contents = fid.readline().decode('ISO-8859-1') # find file header for data product if bool(hx.search(file_contents)): # track file header lines logging.debug(file_contents) # extract hour from header and convert to float - HH, = re.findall(r'(\d+):\d+:\d+',file_contents) + (HH,) = re.findall(r'(\d+):\d+:\d+', file_contents) # convert dates to int and save to arrays years[count] = np.int64(YY) months[count] = np.int64(MM) days[count] = np.int64(DD) hours[count] = np.int64(HH) # read each line of spherical harmonics - for k in range(0,n_harm): - file_contents=fid.readline().decode('ISO-8859-1') + for k in range(0, n_harm): + file_contents = fid.readline().decode( + 'ISO-8859-1' + ) # find numerical instances in the data line line_contents = rx.findall(file_contents) # spherical harmonic degree and order YLMS.l[k] = np.int64(line_contents[0]) YLMS.m[k] = np.int64(line_contents[1]) # extract spherical harmonics - YLMS.clm[k,count] = np.float64(line_contents[2]) - YLMS.slm[k,count] = np.float64(line_contents[3]) + YLMS.clm[k, count] = np.float64( + line_contents[2] + ) + YLMS.slm[k, count] = np.float64( + line_contents[3] + ) # add 1 to hour counter c += 1 count += 1 @@ -344,7 +397,8 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # calculate times for flattened harmonics YLMS.time = gravtk.time.convert_calendar_decimal( - years, months, day=days, hour=hours) + years, months, day=days, hour=hours + ) YLMS.month = gravtk.time.calendar_to_grace(YLMS.time) # convert to expanded form and truncate to LMAX Ylms = YLMS.expand(date=True).truncate(LMAX) @@ -359,25 +413,29 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, mean_Ylms.product = DSET # start and end time for month start_time = gravtk.time.convert_julian(np.min(JD)) - mean_Ylms.start_time = [f"{start_time['year']:4.0f}", - f"{start_time['month']:02.0f}", - f"{start_time['day']:02.0f}"] + mean_Ylms.start_time = [ + f'{start_time["year"]:4.0f}', + f'{start_time["month"]:02.0f}', + f'{start_time["day"]:02.0f}', + ] end_time = gravtk.time.convert_julian(np.max(JD)) - mean_Ylms.end_time = [f"{end_time['year']:4.0f}", - f"{end_time['month']:02.0f}", - f"{end_time['day']:02.0f}"] + mean_Ylms.end_time = [ + f'{end_time["year"]:4.0f}', + f'{end_time["month"]:02.0f}', + f'{end_time["day"]:02.0f}', + ] # output mean Ylms to file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) mean_Ylms.to_ascii(OUTPUT_FILE) - elif (DATAFORM == 'netCDF4'): + elif DATAFORM == 'netCDF4': # netcdf (.nc) mean_Ylms.to_netCDF4(OUTPUT_FILE, **attributes) - elif (DATAFORM == 'HDF5'): + elif DATAFORM == 'HDF5': # HDF5 (.H5) mean_Ylms.to_HDF5(OUTPUT_FILE, **attributes) - elif (DATAFORM == 'SHM'): + elif DATAFORM == 'SHM': mean_Ylms.to_SHM(OUTPUT_FILE, gzip=True) # set the permissions mode of the output file OUTPUT_FILE.chmod(mode=MODE) @@ -386,10 +444,13 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, logging.info(f'File {FILE} not output (incomplete)') # if outputting as spherical harmonic model files - if (DATAFORM == 'SHM'): + if DATAFORM == 'SHM': # Create an index file for the output GRACE product - grace_files = [f.name for f in grace_dir.joinpath(DSET).iterdir() if - re.match(rf'{DSET}-2(.*?)\.gz', f.name)] + grace_files = [ + f.name + for f in grace_dir.joinpath(DSET).iterdir() + if re.match(rf'{DSET}-2(.*?)\.gz', f.name) + ] # outputting GRACE filenames to index grace_index_file = grace_dir.joinpath(DSET, 'index.txt') with grace_index_file.open(mode='w', encoding='utf8') as fid: @@ -403,16 +464,17 @@ def dealiasing_monthly_mean(base_dir, PROC=None, DREL=None, DSET=None, # close the output date file f_out.close() + # PURPOSE: additional routines for the harmonics module class dealiasing(gravtk.harmonics): def __init__(self, **kwargs): super().__init__(**kwargs) - self.center=None - self.release='RLxx' - self.product=None - self.start_time=[None]*3 - self.end_time=[None]*3 - self.gzip=True + self.center = None + self.release = 'RLxx' + self.product = None + self.start_time = [None] * 3 + self.end_time = [None] * 3 + self.gzip = True def from_harmonics(self, temp): """ @@ -420,8 +482,18 @@ def from_harmonics(self, temp): """ self = dealiasing(lmax=temp.lmax, mmax=temp.mmax) # try to assign variables to self - for key in ['clm','slm','time','month','filename', - 'center','release','product','start_time','end_time']: + for key in [ + 'clm', + 'slm', + 'time', + 'month', + 'filename', + 'center', + 'release', + 'product', + 'start_time', + 'end_time', + ]: try: val = getattr(temp, key) setattr(self, key, np.copy(val)) @@ -441,7 +513,7 @@ def to_SHM(self, filename, **kwargs): """ self.filename = pathlib.Path(filename).expanduser().absolute() # set default verbosity - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) logging.info(str(self.filename)) # open the output file if self.gzip: @@ -452,18 +524,30 @@ def to_SHM(self, filename, **kwargs): self.print_header(fid) self.print_harmonic(fid) self.print_global(fid) - self.print_variables(fid,'double precision') + self.print_variables(fid, 'double precision') # output file format - file_format = ('{0:6} {1:4d} {2:4d} {3:+18.12E} {4:+18.12E} ' - '{5:10.4E} {6:10.4E} {7} {8} {9}') + file_format = ( + '{0:6} {1:4d} {2:4d} {3:+18.12E} {4:+18.12E} ' + '{5:10.4E} {6:10.4E} {7} {8} {9}' + ) # start and end time in line format start_date = '{0}{1}{2}.0000'.format(*self.start_time) end_date = '{0}{1}{2}.0000'.format(*self.end_time) # write to file for each spherical harmonic degree and order - for m in range(0, self.mmax+1): - for l in range(m, self.lmax+1): - args = ('GRCOF2', l, m, self.clm[l,m], self.slm[l,m], - 0, 0, start_date, end_date, 'nnnn') + for m in range(0, self.mmax + 1): + for l in range(m, self.lmax + 1): + args = ( + 'GRCOF2', + l, + m, + self.clm[l, m], + self.slm[l, m], + 0, + 0, + start_date, + end_date, + 'nnnn', + ) print(file_format.format(*args), file=fid) # close the output file fid.close() @@ -474,8 +558,8 @@ def print_header(self, fid): fid.write('{0}:\n'.format('header')) # data dimensions fid.write(' {0}:\n'.format('dimensions')) - fid.write(' {0:22}: {1:d}\n'.format('degree',self.lmax)) - fid.write(' {0:22}: {1:d}\n'.format('order',self.lmax)) + fid.write(' {0:22}: {1:d}\n'.format('degree', self.lmax)) + fid.write(' {0:22}: {1:d}\n'.format('order', self.lmax)) fid.write('\n') # PURPOSE: print spherical harmonic attributes to YAML header @@ -484,84 +568,96 @@ def print_harmonic(self, fid): fid.write(' {0}:\n'.format('non-standard_attributes')) # product id product_id = '{0}-2'.format(self.product) - fid.write(' {0:22}: {1}\n'.format('product_id',product_id)) + fid.write(' {0:22}: {1}\n'.format('product_id', product_id)) # format id fid.write(' {0:22}:\n'.format('format_id')) short_name = 'SHM' - fid.write(' {0:20}: {1}\n'.format('short_name',short_name)) + fid.write(' {0:20}: {1}\n'.format('short_name', short_name)) long_name = 'Earth Gravity Spherical Harmonic Model Format' - fid.write(' {0:20}: {1}\n'.format('long_name',long_name)) + fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) # harmonic normalization normalization = 'fully normalized' - fid.write(' {0:22}: {1}\n'.format('normalization', - normalization)) + fid.write(' {0:22}: {1}\n'.format('normalization', normalization)) # earth parameters # gravitational constant fid.write(' {0:22}:\n'.format('earth_gravity_param')) long_name = 'gravitational constant times mass of Earth' - fid.write(' {0:20}: {1}\n'.format('long_name',long_name)) + fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) units = 'm3/s2' - fid.write(' {0:20}: {1}\n'.format('units',units)) + fid.write(' {0:20}: {1}\n'.format('units', units)) value = '3.9860044180E+14' - fid.write(' {0:20}: {1}\n'.format('value',value)) + fid.write(' {0:20}: {1}\n'.format('value', value)) # equatorial radius fid.write(' {0:22}:\n'.format('mean_equator_radius')) long_name = 'mean equator radius' - fid.write(' {0:20}: {1}\n'.format('long_name',long_name)) + fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) units = 'meters' - fid.write(' {0:20}: {1}\n'.format('units',units)) + fid.write(' {0:20}: {1}\n'.format('units', units)) value = '6.3781366000E+06' - fid.write(' {0:20}: {1}\n'.format('value',value)) + fid.write(' {0:20}: {1}\n'.format('value', value)) fid.write('\n') # PURPOSE: print global attributes to YAML header def print_global(self, fid): fid.write(' {0}:\n'.format('global_attributes')) # product title - if (self.month <= 186): + if self.month <= 186: MISSION = 'GRACE' PROJECT = 'NASA Gravity Recovery And Climate Experiment (GRACE)' - ACKNOWLEDGEMENT = ('GRACE is a joint mission of NASA (USA) and ' - 'DLR (Germany).') + ACKNOWLEDGEMENT = ( + 'GRACE is a joint mission of NASA (USA) and DLR (Germany).' + ) else: MISSION = 'GRACE-FO' - PROJECT = ('NASA Gravity Recovery And Climate Experiment ' - 'Follow-On (GRACE-FO)') - ACKNOWLEDGEMENT = ('GRACE-FO is a joint mission of the US National ' + PROJECT = ( + 'NASA Gravity Recovery And Climate Experiment ' + 'Follow-On (GRACE-FO)' + ) + ACKNOWLEDGEMENT = ( + 'GRACE-FO is a joint mission of the US National ' 'Aeronautics and Space Administration and the German Research ' - 'Center for Geosciences.') - args = (MISSION,self.product,self.center,self.release) + 'Center for Geosciences.' + ) + args = (MISSION, self.product, self.center, self.release) title = '{0} Geopotential {1} Coefficients {2} {3}'.format(*args) - fid.write(' {0:22}: {1}\n'.format('title',title)) + fid.write(' {0:22}: {1}\n'.format('title', title)) # product summaries summaries = {} - summaries['GAA'] = ("Spherical harmonic coefficients that represent " + summaries['GAA'] = ( + 'Spherical harmonic coefficients that represent ' "anomalous contributions of the non-tidal atmosphere to the Earth's " - "mean gravity field during the specified timespan. This includes the " - "contribution of atmospheric surface pressure over the continents, " - "the static contribution of atmospheric pressure to ocean bottom " - "pressure elsewhere, and the contribution of upper-air density " - "anomalies above both the continents and the oceans.") - summaries['GAB'] = ("Spherical harmonic coefficients that represent " - "anomalous contributions of the non-tidal dynamic ocean to ocean " - "bottom pressure during the specified timespan.") - summaries['GAC'] = ("Spherical harmonic coefficients that represent " - "the sum of the ATM (or GAA) and OCN (or GAB) coefficients during " - "the specified timespan. These coefficients represent anomalous " - "contributions of the non-tidal dynamic ocean to ocean bottom " - "pressure, the non-tidal atmospheric surface pressure over the " - "continents, the static contribution of atmospheric pressure to " - "ocean bottom pressure, and the upper-air density anomalies above " - "both the continents and the oceans.") - summaries['GAD'] = ("Spherical harmonic coefficients that are zero " - "over the continents, and provide the anomalous simulated ocean " - "bottom pressure that includes non-tidal air and water " - "contributions elsewhere during the specified timespan. These " - "coefficients differ from GLO (or GAC) coefficients over the " - "ocean domain by disregarding upper air density anomalies.") + 'mean gravity field during the specified timespan. This includes the ' + 'contribution of atmospheric surface pressure over the continents, ' + 'the static contribution of atmospheric pressure to ocean bottom ' + 'pressure elsewhere, and the contribution of upper-air density ' + 'anomalies above both the continents and the oceans.' + ) + summaries['GAB'] = ( + 'Spherical harmonic coefficients that represent ' + 'anomalous contributions of the non-tidal dynamic ocean to ocean ' + 'bottom pressure during the specified timespan.' + ) + summaries['GAC'] = ( + 'Spherical harmonic coefficients that represent ' + 'the sum of the ATM (or GAA) and OCN (or GAB) coefficients during ' + 'the specified timespan. These coefficients represent anomalous ' + 'contributions of the non-tidal dynamic ocean to ocean bottom ' + 'pressure, the non-tidal atmospheric surface pressure over the ' + 'continents, the static contribution of atmospheric pressure to ' + 'ocean bottom pressure, and the upper-air density anomalies above ' + 'both the continents and the oceans.' + ) + summaries['GAD'] = ( + 'Spherical harmonic coefficients that are zero ' + 'over the continents, and provide the anomalous simulated ocean ' + 'bottom pressure that includes non-tidal air and water ' + 'contributions elsewhere during the specified timespan. These ' + 'coefficients differ from GLO (or GAC) coefficients over the ' + 'ocean domain by disregarding upper air density anomalies.' + ) summary = summaries[self.product] - fid.write(' {0:22}: {1}\n'.format('summary',''.join(summary))) - fid.write(' {0:22}: {1}\n'.format('project',PROJECT)) + fid.write(' {0:22}: {1}\n'.format('summary', ''.join(summary))) + fid.write(' {0:22}: {1}\n'.format('project', PROJECT)) keywords = [] keywords.append('GRACE') keywords.append('GRACE-FO') if (self.month > 186) else None @@ -585,32 +681,38 @@ def print_global(self, fid): keywords.append('Atmosphere') keywords.append('Non-tidal Atmosphere') keywords.append('Dealiasing Product') - fid.write(' {0:22}: {1}\n'.format('keywords',', '.join(keywords))) - vocabulary = 'NASA Global Change Master Directory (GCMD) Science Keywords' - fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary',vocabulary)) - if (self.center == 'CSR'): + fid.write(' {0:22}: {1}\n'.format('keywords', ', '.join(keywords))) + vocabulary = ( + 'NASA Global Change Master Directory (GCMD) Science Keywords' + ) + fid.write(' {0:22}: {1}\n'.format('keywords_vocabulary', vocabulary)) + if self.center == 'CSR': institution = 'UT-AUSTIN/CSR' - elif (self.center == 'GFZ'): + elif self.center == 'GFZ': institution = 'GFZ German Research Centre for Geosciences' - elif (self.center == 'JPL'): + elif self.center == 'JPL': institution = 'NASA/JPL' else: # default to GFZ institution = 'GFZ German Research Centre for Geosciences' - fid.write(' {0:22}: {1}\n'.format('institution',institution)) + fid.write(' {0:22}: {1}\n'.format('institution', institution)) src = 'All data from AOD1B {0}'.format(self.release) - fid.write(' {0:22}: {1}\n'.format('source',src)) - fid.write(' {0:22}: {1:d}\n'.format('processing_level',2)) - fid.write(' {0:22}: {1}\n'.format('acknowledgement',ACKNOWLEDGEMENT)) + fid.write(' {0:22}: {1}\n'.format('source', src)) + fid.write(' {0:22}: {1:d}\n'.format('processing_level', 2)) + fid.write( + ' {0:22}: {1}\n'.format('acknowledgement', ACKNOWLEDGEMENT) + ) PRODUCT_VERSION = 'Release-{0}'.format(self.release[2:]) - fid.write(' {0:22}: {1}\n'.format('product_version',PRODUCT_VERSION)) + fid.write( + ' {0:22}: {1}\n'.format('product_version', PRODUCT_VERSION) + ) fid.write(' {0:22}:\n'.format('references')) # date range and date created start_date = '{0}-{1}-{2}'.format(*self.start_time) - fid.write(' {0:22}: {1}\n'.format('time_coverage_start',start_date)) + fid.write(' {0:22}: {1}\n'.format('time_coverage_start', start_date)) end_date = '{0}-{1}-{2}'.format(*self.end_time) - fid.write(' {0:22}: {1}\n'.format('time_coverage_end',end_date)) - today = time.strftime('%Y-%m-%d',time.localtime()) + fid.write(' {0:22}: {1}\n'.format('time_coverage_end', end_date)) + today = time.strftime('%Y-%m-%d', time.localtime()) fid.write(' {0:22}: {1}\n'.format('date_created', today)) fid.write('\n') @@ -638,7 +740,9 @@ def print_variables(self, fid, data_precision): fid.write(' {0:20}: {1}\n'.format('comment', '3rd column')) # clm fid.write(' {0:22}:\n'.format('clm')) - long_name = 'Clm coefficient; cosine coefficient for degree l and order m' + long_name = ( + 'Clm coefficient; cosine coefficient for degree l and order m' + ) fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) fid.write(' {0:20}: {1}\n'.format('data_type', data_precision)) fid.write(' {0:20}: {1}\n'.format('comment', '4th column')) @@ -678,8 +782,11 @@ def print_variables(self, fid, data_precision): fid.write(' {0:22}:\n'.format('solution_flags')) long_name = 'Coefficient adjustment and a priori flags' fid.write(' {0:20}: {1}\n'.format('long_name', long_name)) - fid.write(' {0:20}: {1}\n'.format('coverage_content_type', - 'auxiliaryInformation')) + fid.write( + ' {0:20}: {1}\n'.format( + 'coverage_content_type', 'auxiliaryInformation' + ) + ) fid.write(' {0:20}: {1}\n'.format('data_type', 'byte')) fid.write(' {0:20}:\n'.format('flag_meanings')) # solution flag meanings @@ -688,12 +795,13 @@ def print_variables(self, fid, data_precision): m.append('Slm adjusted, y for yes and n for no') m.append('stochastic a priori info for Clm, y for yes and n for no') m.append('stochastic a priori info for Slm, y for yes and n for no') - for i,meaning in enumerate(m): + for i, meaning in enumerate(m): fid.write(' - char {0:d} = {1}\n'.format(i, meaning)) fid.write(' {0:20}: {1}\n'.format('comment', '10th column')) # end of header fid.write('\n\n# End of YAML header\n') + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -701,56 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -758,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/_assets/geoid_height.svg b/doc/source/_assets/geoid_height.svg index d8cc9ee7..997e42da 100644 --- a/doc/source/_assets/geoid_height.svg +++ b/doc/source/_assets/geoid_height.svg @@ -60,7 +60,7 @@ Geoid `_ - Creates an index file for each data product diff --git a/doc/source/api_reference/access/esa_costg_swarm_sync.rst b/doc/source/api_reference/access/esa_costg_swarm_sync.rst index eef1663b..f2ac4918 100644 --- a/doc/source/api_reference/access/esa_costg_swarm_sync.rst +++ b/doc/source/api_reference/access/esa_costg_swarm_sync.rst @@ -1,6 +1,6 @@ -======================= -esa_costg_swarm_sync.py -======================= +=========================== +``esa_costg_swarm_sync.py`` +=========================== - Syncs Swarm gravity field products from the `ESA Swarm Science Server `_ - Creates an index file for each data product diff --git a/doc/source/api_reference/access/gfz_icgem_costg_ftp.rst b/doc/source/api_reference/access/gfz_icgem_costg_ftp.rst index 6dd2edaa..47bfb931 100644 --- a/doc/source/api_reference/access/gfz_icgem_costg_ftp.rst +++ b/doc/source/api_reference/access/gfz_icgem_costg_ftp.rst @@ -1,6 +1,6 @@ -====================== -gfz_icgem_costg_ftp.py -====================== +========================== +``gfz_icgem_costg_ftp.py`` +========================== - Syncs GRACE/GRACE-FO/Swarm COST-G data from the `GFZ International Centre for Global Earth Models (ICGEM) `_ - Creates an index file for each data product diff --git a/doc/source/api_reference/access/gfz_isdc_dealiasing_sync.rst b/doc/source/api_reference/access/gfz_isdc_dealiasing_sync.rst index da1867d6..dbcbe802 100644 --- a/doc/source/api_reference/access/gfz_isdc_dealiasing_sync.rst +++ b/doc/source/api_reference/access/gfz_isdc_dealiasing_sync.rst @@ -1,6 +1,6 @@ -=========================== -gfz_isdc_dealiasing_sync.py -=========================== +=============================== +``gfz_isdc_dealiasing_sync.py`` +=============================== - Syncs GRACE Level-1b dealiasing products from the `GFZ Information System and Data Center (ISDC) `_ - Optionally outputs as monthly tar files diff --git a/doc/source/api_reference/access/gfz_isdc_grace_sync.rst b/doc/source/api_reference/access/gfz_isdc_grace_sync.rst index 303391fa..ac1e543a 100644 --- a/doc/source/api_reference/access/gfz_isdc_grace_sync.rst +++ b/doc/source/api_reference/access/gfz_isdc_grace_sync.rst @@ -1,6 +1,6 @@ -====================== -gfz_isdc_grace_sync.py -====================== +========================== +``gfz_isdc_grace_sync.py`` +========================== - Syncs GRACE/GRACE-FO and auxiliary data from the `GFZ Information System and Data Center (ISDC) `_ - Syncs CSR/GFZ/JPL Level-2 spherical harmonic files diff --git a/doc/source/api_reference/access/itsg_graz_grace_sync.rst b/doc/source/api_reference/access/itsg_graz_grace_sync.rst index 65fcfd9a..fac46cb5 100644 --- a/doc/source/api_reference/access/itsg_graz_grace_sync.rst +++ b/doc/source/api_reference/access/itsg_graz_grace_sync.rst @@ -1,6 +1,6 @@ -======================= -itsg_graz_grace_sync.py -======================= +=========================== +``itsg_graz_grace_sync.py`` +=========================== - Syncs GRACE/GRACE-FO and auxiliary data from the `ITSG GRAZ server `_ - Creates an index file for each data product diff --git a/doc/source/api_reference/access/podaac_cumulus.rst b/doc/source/api_reference/access/podaac_cumulus.rst index 274b5f56..095abbb5 100644 --- a/doc/source/api_reference/access/podaac_cumulus.rst +++ b/doc/source/api_reference/access/podaac_cumulus.rst @@ -1,6 +1,6 @@ -================= -podaac_cumulus.py -================= +===================== +``podaac_cumulus.py`` +===================== - Syncs GRACE/GRACE-FO data from `NASA JPL PO.DAAC Cumulus AWS S3 bucket `_ - S3 Cumulus syncs are only available in AWS instances in ``us-west-2`` diff --git a/doc/source/api_reference/associated_legendre.rst b/doc/source/api_reference/associated_legendre.rst index d198e54e..d608c7ba 100644 --- a/doc/source/api_reference/associated_legendre.rst +++ b/doc/source/api_reference/associated_legendre.rst @@ -1,6 +1,6 @@ -=================== -associated_legendre -=================== +======================= +``associated_legendre`` +======================= - Computes fully-normalized associated Legendre Polynomials and their first derivative for a vector of ``x`` values diff --git a/doc/source/api_reference/clenshaw_summation.rst b/doc/source/api_reference/clenshaw_summation.rst index e6ff5c99..7acb2bf4 100644 --- a/doc/source/api_reference/clenshaw_summation.rst +++ b/doc/source/api_reference/clenshaw_summation.rst @@ -1,6 +1,6 @@ -================== -clenshaw_summation -================== +====================== +``clenshaw_summation`` +====================== - Returns the spatial field for a series of spherical harmonics at a sequence of ungridded points - Uses a Clenshaw summation to calculate the spherical harmonic summation diff --git a/doc/source/api_reference/dealiasing/aod1b_geocenter.rst b/doc/source/api_reference/dealiasing/aod1b_geocenter.rst index 14ca6606..ad215f02 100644 --- a/doc/source/api_reference/dealiasing/aod1b_geocenter.rst +++ b/doc/source/api_reference/dealiasing/aod1b_geocenter.rst @@ -1,6 +1,6 @@ -================== -aod1b_geocenter.py -================== +====================== +``aod1b_geocenter.py`` +====================== - Reads GRACE/GRACE-FO level-1b dealiasing data files for a specific product diff --git a/doc/source/api_reference/dealiasing/aod1b_oblateness.rst b/doc/source/api_reference/dealiasing/aod1b_oblateness.rst index 8fceda7d..69df766f 100644 --- a/doc/source/api_reference/dealiasing/aod1b_oblateness.rst +++ b/doc/source/api_reference/dealiasing/aod1b_oblateness.rst @@ -1,6 +1,6 @@ -=================== -aod1b_oblateness.py -=================== +======================= +``aod1b_oblateness.py`` +======================= - Reads GRACE/GRACE-FO level-1b dealiasing data files for a specific product diff --git a/doc/source/api_reference/dealiasing/dealiasing_global_uplift.rst b/doc/source/api_reference/dealiasing/dealiasing_global_uplift.rst index 0705b115..154e0444 100644 --- a/doc/source/api_reference/dealiasing/dealiasing_global_uplift.rst +++ b/doc/source/api_reference/dealiasing/dealiasing_global_uplift.rst @@ -1,6 +1,6 @@ -=========================== -dealiasing_global_uplift.py -=========================== +=============================== +``dealiasing_global_uplift.py`` +=============================== - Reads GRACE/GRACE-FO level-1b dealiasing data files for global atmospheric and oceanic loading and estimates anomalies in elastic crustal uplift :cite:p:`Davis:2004il,Wahr:1998hy` diff --git a/doc/source/api_reference/dealiasing/dealiasing_monthly_mean.rst b/doc/source/api_reference/dealiasing/dealiasing_monthly_mean.rst index ba27f356..0e68d36b 100644 --- a/doc/source/api_reference/dealiasing/dealiasing_monthly_mean.rst +++ b/doc/source/api_reference/dealiasing/dealiasing_monthly_mean.rst @@ -1,6 +1,6 @@ -========================== -dealiasing_monthly_mean.py -========================== +============================== +``dealiasing_monthly_mean.py`` +============================== - Reads GRACE/GRACE-FO level-1b dealiasing data files for a specific product and outputs monthly the mean for a specific GRACE/GRACE-FO processing center and data release diff --git a/doc/source/api_reference/degree_amplitude.rst b/doc/source/api_reference/degree_amplitude.rst index b041e0d2..aad8df00 100644 --- a/doc/source/api_reference/degree_amplitude.rst +++ b/doc/source/api_reference/degree_amplitude.rst @@ -1,6 +1,6 @@ -================ -degree_amplitude -================ +==================== +``degree_amplitude`` +==================== - Calculates the amplitude of each spherical harmonic degree diff --git a/doc/source/api_reference/destripe_harmonics.rst b/doc/source/api_reference/destripe_harmonics.rst index ce876194..ae5990d2 100644 --- a/doc/source/api_reference/destripe_harmonics.rst +++ b/doc/source/api_reference/destripe_harmonics.rst @@ -1,6 +1,6 @@ -================== -destripe_harmonics -================== +====================== +``destripe_harmonics`` +====================== - Filters spherical harmonic coefficients for correlated "striping" errors following :cite:t:`Swenson:2006hu` diff --git a/doc/source/api_reference/fourier_legendre.rst b/doc/source/api_reference/fourier_legendre.rst index 196f9849..d11a1823 100644 --- a/doc/source/api_reference/fourier_legendre.rst +++ b/doc/source/api_reference/fourier_legendre.rst @@ -1,6 +1,6 @@ -================ -fourier_legendre -================ +==================== +``fourier_legendre`` +==================== - Computes Fourier coefficients of the associated Legendre functions diff --git a/doc/source/api_reference/gauss_weights.rst b/doc/source/api_reference/gauss_weights.rst index e438a439..fab531a4 100644 --- a/doc/source/api_reference/gauss_weights.rst +++ b/doc/source/api_reference/gauss_weights.rst @@ -1,6 +1,6 @@ -============= -gauss_weights -============= +================= +``gauss_weights`` +================= - Computes the Gaussian weights as a function of degree - A normalized version of Christopher Jekeli's Gaussian averaging function diff --git a/doc/source/api_reference/gen_averaging_kernel.rst b/doc/source/api_reference/gen_averaging_kernel.rst index 13dde4e2..ccd33d06 100644 --- a/doc/source/api_reference/gen_averaging_kernel.rst +++ b/doc/source/api_reference/gen_averaging_kernel.rst @@ -1,6 +1,6 @@ -==================== -gen_averaging_kernel -==================== +======================== +``gen_averaging_kernel`` +======================== - Generates averaging kernel coefficients which minimize the total error diff --git a/doc/source/api_reference/gen_disc_load.rst b/doc/source/api_reference/gen_disc_load.rst index c801d298..72ee294f 100644 --- a/doc/source/api_reference/gen_disc_load.rst +++ b/doc/source/api_reference/gen_disc_load.rst @@ -1,6 +1,6 @@ -============= -gen_disc_load -============= +================= +``gen_disc_load`` +================= - Calculates gravitational spherical harmonic coefficients for a uniform disc load diff --git a/doc/source/api_reference/gen_harmonics.rst b/doc/source/api_reference/gen_harmonics.rst index 428def38..d0b619f3 100644 --- a/doc/source/api_reference/gen_harmonics.rst +++ b/doc/source/api_reference/gen_harmonics.rst @@ -1,6 +1,6 @@ -============= -gen_harmonics -============= +================= +``gen_harmonics`` +================= - Converts data from the spatial domain to spherical harmonic coefficients - Does not compute the solid Earth elastic response or convert units diff --git a/doc/source/api_reference/gen_point_load.rst b/doc/source/api_reference/gen_point_load.rst index b8679a50..74d0ce8c 100644 --- a/doc/source/api_reference/gen_point_load.rst +++ b/doc/source/api_reference/gen_point_load.rst @@ -1,6 +1,6 @@ -============== -gen_point_load -============== +================== +``gen_point_load`` +================== - Calculates gravitational spherical harmonic coefficients for point masses @@ -18,4 +18,4 @@ Calling Sequence .. autofunction:: gravity_toolkit.gen_point_load -.. autofunction:: gravity_toolkit.gen_point_load.spherical_harmonic_matrix +.. autofunction:: gravity_toolkit.gen_point_load._complex_harmonics diff --git a/doc/source/api_reference/gen_spherical_cap.rst b/doc/source/api_reference/gen_spherical_cap.rst index 069c0f28..e404f608 100644 --- a/doc/source/api_reference/gen_spherical_cap.rst +++ b/doc/source/api_reference/gen_spherical_cap.rst @@ -1,6 +1,6 @@ -================= -gen_spherical_cap -================= +===================== +``gen_spherical_cap`` +===================== - Calculates gravitational spherical harmonic coefficients for a spherical cap diff --git a/doc/source/api_reference/gen_stokes.rst b/doc/source/api_reference/gen_stokes.rst index 460b40fa..635eeab9 100644 --- a/doc/source/api_reference/gen_stokes.rst +++ b/doc/source/api_reference/gen_stokes.rst @@ -1,6 +1,6 @@ -========== -gen_stokes -========== +============== +``gen_stokes`` +============== - Converts data from the spatial domain to spherical harmonic coefficients diff --git a/doc/source/api_reference/geocenter.rst b/doc/source/api_reference/geocenter.rst index 5a3e5cb2..001c6fda 100644 --- a/doc/source/api_reference/geocenter.rst +++ b/doc/source/api_reference/geocenter.rst @@ -1,9 +1,15 @@ -========= -geocenter -========= +============= +``geocenter`` +============= Data class for reading and processing geocenter data + - Can read geocenter files from data providers + - Can merge a list of :py:class:`geocenter` objects into a single object + - Can subset to a list of GRACE/GRACE-FO months + - Can output :py:class:`geocenter` objects to ascii and netCDF4 files + + `Source code`__ .. __: https://github.com/tsutterley/gravity-toolkit/blob/main/gravity_toolkit/geocenter.py diff --git a/doc/source/api_reference/geocenter/calc_degree_one.rst b/doc/source/api_reference/geocenter/calc_degree_one.rst index 0692a5d9..9d382047 100644 --- a/doc/source/api_reference/geocenter/calc_degree_one.rst +++ b/doc/source/api_reference/geocenter/calc_degree_one.rst @@ -1,6 +1,6 @@ -================== -calc_degree_one.py -================== +====================== +``calc_degree_one.py`` +====================== - Calculates degree 1 variations using GRACE/GRACE-FO coefficients of degree 2 and greater, and modeled ocean bottom pressure variations :cite:p:`Swenson:2008cr,Sutterley:2019bx`. diff --git a/doc/source/api_reference/geocenter/monte_carlo_degree_one.rst b/doc/source/api_reference/geocenter/monte_carlo_degree_one.rst index 17810600..951eefac 100644 --- a/doc/source/api_reference/geocenter/monte_carlo_degree_one.rst +++ b/doc/source/api_reference/geocenter/monte_carlo_degree_one.rst @@ -1,6 +1,6 @@ -========================= -monte_carlo_degree_one.py -========================= +============================= +``monte_carlo_degree_one.py`` +============================= - Estimates uncertainties in degree 1 using GRACE/GRACE-FO coefficients of degree 2 and greater, and modeled ocean bottom pressure variations in a Monte Carlo scheme :cite:p:`Swenson:2008cr,Sutterley:2019bx`. - Calculates the estimated spherical harmonic errors following :cite:t:`Wahr:2006bx` diff --git a/doc/source/api_reference/grace_date.rst b/doc/source/api_reference/grace_date.rst index e4c846ca..3cb7f773 100644 --- a/doc/source/api_reference/grace_date.rst +++ b/doc/source/api_reference/grace_date.rst @@ -1,6 +1,6 @@ -========== -grace_date -========== +============== +``grace_date`` +============== - Reads GRACE/GRACE-FO index file from `podaac_cumulus.py` or `gfz_isdc_grace_ftp.py` - Parses dates of each GRACE/GRACE-FO file and assigns the month number diff --git a/doc/source/api_reference/grace_find_months.rst b/doc/source/api_reference/grace_find_months.rst index 28baf31c..8bbd568d 100644 --- a/doc/source/api_reference/grace_find_months.rst +++ b/doc/source/api_reference/grace_find_months.rst @@ -1,6 +1,6 @@ -================= -grace_find_months -================= +===================== +``grace_find_months`` +===================== - Finds the months available for a GRACE/GRACE-FO/Swarm product - Finds the all months missing from the product diff --git a/doc/source/api_reference/grace_input_months.rst b/doc/source/api_reference/grace_input_months.rst index e168253b..89206109 100644 --- a/doc/source/api_reference/grace_input_months.rst +++ b/doc/source/api_reference/grace_input_months.rst @@ -1,6 +1,6 @@ -================== -grace_input_months -================== +====================== +``grace_input_months`` +====================== - Reads GRACE/GRACE-FO/Swarm files for a specified spherical harmonic degree and order and for a specified date range diff --git a/doc/source/api_reference/grace_months_index.rst b/doc/source/api_reference/grace_months_index.rst index f39478d8..3f2ca68d 100644 --- a/doc/source/api_reference/grace_months_index.rst +++ b/doc/source/api_reference/grace_months_index.rst @@ -1,6 +1,6 @@ -================== -grace_months_index -================== +====================== +``grace_months_index`` +====================== - Creates an index of dates for all GRACE/GRACE-FO processing centers diff --git a/doc/source/api_reference/harmonic_gradients.rst b/doc/source/api_reference/harmonic_gradients.rst index 59bcf431..711ba520 100644 --- a/doc/source/api_reference/harmonic_gradients.rst +++ b/doc/source/api_reference/harmonic_gradients.rst @@ -1,6 +1,6 @@ -================== -harmonic_gradients -================== +====================== +``harmonic_gradients`` +====================== - Calculates the zonal and meridional gradients of a scalar field from a series of spherical harmonics diff --git a/doc/source/api_reference/harmonic_summation.rst b/doc/source/api_reference/harmonic_summation.rst index 317483fc..7a70b1f4 100644 --- a/doc/source/api_reference/harmonic_summation.rst +++ b/doc/source/api_reference/harmonic_summation.rst @@ -1,6 +1,6 @@ -================== -harmonic_summation -================== +====================== +``harmonic_summation`` +====================== - Returns the spatial field for a series of spherical harmonics diff --git a/doc/source/api_reference/harmonics.rst b/doc/source/api_reference/harmonics.rst index ed10f90c..ae152401 100644 --- a/doc/source/api_reference/harmonics.rst +++ b/doc/source/api_reference/harmonics.rst @@ -1,16 +1,16 @@ -========= -harmonics -========= +============= +``harmonics`` +============= Spherical harmonic data class for processing GRACE/GRACE-FO Level-2 data - Can read ascii, netCDF4, HDF5 files - Can read from an index of the above file types - - Can merge a list of ``harmonics`` objects into a single object + - Can merge a list of :py:class:`harmonics` objects into a single object - Can subset to a list of GRACE/GRACE-FO months - - Can calculate the mean field of a ``harmonics`` object - - Can filter ``harmonics`` for correlated "striping" errors - - Can output ``harmonics`` objects to ascii, netCDF4 or HDF5 files + - Can calculate the mean field of a :py:class:`harmonics` object + - Can filter :py:class:`harmonics` for correlated "striping" errors + - Can output :py:class:`harmonics` objects to ascii, netCDF4 or HDF5 files Calling Sequence ================ diff --git a/doc/source/api_reference/legendre.rst b/doc/source/api_reference/legendre.rst index 011fd089..4736c781 100644 --- a/doc/source/api_reference/legendre.rst +++ b/doc/source/api_reference/legendre.rst @@ -1,6 +1,6 @@ -======== -legendre -======== +============ +``legendre`` +============ - Computes associated Legendre functions of degree ``l`` evaluated for elements ``x`` - ``l`` must be a scalar integer and ``x`` must contain real values ranging -1 <= ``x`` <= 1 diff --git a/doc/source/api_reference/legendre_polynomials.rst b/doc/source/api_reference/legendre_polynomials.rst index 6fda4075..a62f495a 100644 --- a/doc/source/api_reference/legendre_polynomials.rst +++ b/doc/source/api_reference/legendre_polynomials.rst @@ -1,6 +1,6 @@ -==================== -legendre_polynomials -==================== +======================== +``legendre_polynomials`` +======================== - Computes fully-normalized Legendre polynomials for an array of ``x`` values and their first derivative - Calculates Legendre polynomials for zonal harmonics (order 0) diff --git a/doc/source/api_reference/mapping/plot_AIS_GrIS_maps.rst b/doc/source/api_reference/mapping/plot_AIS_GrIS_maps.rst index 7d8e0028..a51e0363 100644 --- a/doc/source/api_reference/mapping/plot_AIS_GrIS_maps.rst +++ b/doc/source/api_reference/mapping/plot_AIS_GrIS_maps.rst @@ -1,6 +1,6 @@ -===================== -plot_AIS_GrIS_maps.py -===================== +========================= +``plot_AIS_GrIS_maps.py`` +========================= - Creates GMT-like plots for the Greenland and Antarctic ice sheets diff --git a/doc/source/api_reference/mapping/plot_AIS_grid_3maps.rst b/doc/source/api_reference/mapping/plot_AIS_grid_3maps.rst index 47968ade..6c1d4e22 100644 --- a/doc/source/api_reference/mapping/plot_AIS_grid_3maps.rst +++ b/doc/source/api_reference/mapping/plot_AIS_grid_3maps.rst @@ -1,6 +1,6 @@ -====================== -plot_AIS_grid_3maps.py -====================== +========================== +``plot_AIS_grid_3maps.py`` +========================== - Creates 3 GMT-like plots for the Antarctic Ice Sheet on a polar stereographic south (3031) projection diff --git a/doc/source/api_reference/mapping/plot_AIS_grid_4maps.rst b/doc/source/api_reference/mapping/plot_AIS_grid_4maps.rst index 41b45f43..b6f6d3d6 100644 --- a/doc/source/api_reference/mapping/plot_AIS_grid_4maps.rst +++ b/doc/source/api_reference/mapping/plot_AIS_grid_4maps.rst @@ -1,6 +1,6 @@ -====================== -plot_AIS_grid_4maps.py -====================== +========================== +``plot_AIS_grid_4maps.py`` +========================== - Creates 4 GMT-like plots for the Antarctic Ice Sheet on a polar stereographic south (3031) projection diff --git a/doc/source/api_reference/mapping/plot_AIS_grid_maps.rst b/doc/source/api_reference/mapping/plot_AIS_grid_maps.rst index 6eba7179..7773f2c9 100644 --- a/doc/source/api_reference/mapping/plot_AIS_grid_maps.rst +++ b/doc/source/api_reference/mapping/plot_AIS_grid_maps.rst @@ -1,6 +1,6 @@ -===================== -plot_AIS_grid_maps.py -===================== +========================= +``plot_AIS_grid_maps.py`` +========================= - Creates GMT-like plots for the Antarctic Ice Sheet on a polar stereographic south (3031) projection diff --git a/doc/source/api_reference/mapping/plot_AIS_grid_movie.rst b/doc/source/api_reference/mapping/plot_AIS_grid_movie.rst index 83d41c15..e252b75d 100644 --- a/doc/source/api_reference/mapping/plot_AIS_grid_movie.rst +++ b/doc/source/api_reference/mapping/plot_AIS_grid_movie.rst @@ -1,6 +1,6 @@ -====================== -plot_AIS_grid_movie.py -====================== +========================== +``plot_AIS_grid_movie.py`` +========================== - Creates GMT-like anomations for the Antarctic Ice Sheet on a polar stereographic south (3031) projection diff --git a/doc/source/api_reference/mapping/plot_AIS_regional_maps.rst b/doc/source/api_reference/mapping/plot_AIS_regional_maps.rst index 2ecbceef..ca754533 100644 --- a/doc/source/api_reference/mapping/plot_AIS_regional_maps.rst +++ b/doc/source/api_reference/mapping/plot_AIS_regional_maps.rst @@ -1,6 +1,6 @@ -========================= -plot_AIS_regional_maps.py -========================= +============================= +``plot_AIS_regional_maps.py`` +============================= - Creates GMT-like plots for sub-regions of Antarctica on a polar stereographic south (3031) projection diff --git a/doc/source/api_reference/mapping/plot_AIS_regional_movie.rst b/doc/source/api_reference/mapping/plot_AIS_regional_movie.rst index 39ed1b42..bb6ca333 100644 --- a/doc/source/api_reference/mapping/plot_AIS_regional_movie.rst +++ b/doc/source/api_reference/mapping/plot_AIS_regional_movie.rst @@ -1,6 +1,6 @@ -========================== -plot_AIS_regional_movie.py -========================== +============================== +``plot_AIS_regional_movie.py`` +============================== - Creates GMT-like animations for sub-regions of Antarctica on a polar stereographic south (3031) projection diff --git a/doc/source/api_reference/mapping/plot_GrIS_grid_3maps.rst b/doc/source/api_reference/mapping/plot_GrIS_grid_3maps.rst index 729bf1c5..072ea029 100644 --- a/doc/source/api_reference/mapping/plot_GrIS_grid_3maps.rst +++ b/doc/source/api_reference/mapping/plot_GrIS_grid_3maps.rst @@ -1,6 +1,6 @@ -======================= -plot_GrIS_grid_3maps.py -======================= +=========================== +``plot_GrIS_grid_3maps.py`` +=========================== - Creates 3 GMT-like plots for the Greenland Ice Sheet on a NSIDC polar stereographic north (3413) projection diff --git a/doc/source/api_reference/mapping/plot_GrIS_grid_5maps.rst b/doc/source/api_reference/mapping/plot_GrIS_grid_5maps.rst index 6760dd29..44a09956 100644 --- a/doc/source/api_reference/mapping/plot_GrIS_grid_5maps.rst +++ b/doc/source/api_reference/mapping/plot_GrIS_grid_5maps.rst @@ -1,6 +1,6 @@ -======================= -plot_GrIS_grid_5maps.py -======================= +=========================== +``plot_GrIS_grid_5maps.py`` +=========================== - Creates 5 GMT-like plots for the Greenland Ice Sheet on a NSIDC polar stereographic north (3413) projection diff --git a/doc/source/api_reference/mapping/plot_GrIS_grid_maps.rst b/doc/source/api_reference/mapping/plot_GrIS_grid_maps.rst index 7103e776..94630749 100644 --- a/doc/source/api_reference/mapping/plot_GrIS_grid_maps.rst +++ b/doc/source/api_reference/mapping/plot_GrIS_grid_maps.rst @@ -1,6 +1,6 @@ -====================== -plot_GrIS_grid_maps.py -====================== +========================== +``plot_GrIS_grid_maps.py`` +========================== - Creates GMT-like plots for the Greenland Ice Sheet on a NSIDC polar stereographic north (3413) projection diff --git a/doc/source/api_reference/mapping/plot_GrIS_grid_movie.rst b/doc/source/api_reference/mapping/plot_GrIS_grid_movie.rst index c93ffb24..3294fe46 100644 --- a/doc/source/api_reference/mapping/plot_GrIS_grid_movie.rst +++ b/doc/source/api_reference/mapping/plot_GrIS_grid_movie.rst @@ -1,6 +1,6 @@ -======================= -plot_GrIS_grid_movie.py -======================= +=========================== +``plot_GrIS_grid_movie.py`` +=========================== - Creates GMT-like animations for the Greenland Ice Sheet on a NSIDC polar stereographic north (3413) projection diff --git a/doc/source/api_reference/mapping/plot_global_grid_3maps.rst b/doc/source/api_reference/mapping/plot_global_grid_3maps.rst index 9f01781a..d88a092b 100644 --- a/doc/source/api_reference/mapping/plot_global_grid_3maps.rst +++ b/doc/source/api_reference/mapping/plot_global_grid_3maps.rst @@ -1,6 +1,6 @@ -========================= -plot_global_grid_3maps.py -========================= +============================= +``plot_global_grid_3maps.py`` +============================= - Creates 3 GMT-like plots on a global Plate Carr\ |eacute|\e (Equirectangular) projection diff --git a/doc/source/api_reference/mapping/plot_global_grid_4maps.rst b/doc/source/api_reference/mapping/plot_global_grid_4maps.rst index 556d6f98..c93f94e3 100644 --- a/doc/source/api_reference/mapping/plot_global_grid_4maps.rst +++ b/doc/source/api_reference/mapping/plot_global_grid_4maps.rst @@ -1,6 +1,6 @@ -========================= -plot_global_grid_4maps.py -========================= +============================= +``plot_global_grid_4maps.py`` +============================= - Creates 4 GMT-like plots on a global Plate Carr\ |eacute|\e (Equirectangular) projection diff --git a/doc/source/api_reference/mapping/plot_global_grid_5maps.rst b/doc/source/api_reference/mapping/plot_global_grid_5maps.rst index b0538d79..602d5d4b 100644 --- a/doc/source/api_reference/mapping/plot_global_grid_5maps.rst +++ b/doc/source/api_reference/mapping/plot_global_grid_5maps.rst @@ -1,6 +1,6 @@ -========================= -plot_global_grid_5maps.py -========================= +============================= +``plot_global_grid_5maps.py`` +============================= - Creates 5 GMT-like plots on a global Plate Carr\ |eacute|\e (Equirectangular) projection diff --git a/doc/source/api_reference/mapping/plot_global_grid_9maps.rst b/doc/source/api_reference/mapping/plot_global_grid_9maps.rst index 1b8c55cb..36ebc1ed 100644 --- a/doc/source/api_reference/mapping/plot_global_grid_9maps.rst +++ b/doc/source/api_reference/mapping/plot_global_grid_9maps.rst @@ -1,6 +1,6 @@ -========================= -plot_global_grid_9maps.py -========================= +============================= +``plot_global_grid_9maps.py`` +============================= - Creates 9 GMT-like plots on a global Plate Carr\ |eacute|\e (Equirectangular) projection diff --git a/doc/source/api_reference/mapping/plot_global_grid_maps.rst b/doc/source/api_reference/mapping/plot_global_grid_maps.rst index a2286b4b..97217872 100644 --- a/doc/source/api_reference/mapping/plot_global_grid_maps.rst +++ b/doc/source/api_reference/mapping/plot_global_grid_maps.rst @@ -1,6 +1,6 @@ -======================== -plot_global_grid_maps.py -======================== +============================ +``plot_global_grid_maps.py`` +============================ - Creates GMT-like plots on a global Plate Carr\ |eacute|\e (Equirectangular) projection diff --git a/doc/source/api_reference/mapping/plot_global_grid_movie.rst b/doc/source/api_reference/mapping/plot_global_grid_movie.rst index a8818f5b..9d9af07f 100644 --- a/doc/source/api_reference/mapping/plot_global_grid_movie.rst +++ b/doc/source/api_reference/mapping/plot_global_grid_movie.rst @@ -1,6 +1,6 @@ -========================= -plot_global_grid_movie.py -========================= +============================= +``plot_global_grid_movie.py`` +============================= - Creates GMT-like animations on a global Plate Carr\ |eacute|\e (Equirectangular) projection diff --git a/doc/source/api_reference/mascons.rst b/doc/source/api_reference/mascons.rst index 1b10d56b..fa08aa9a 100644 --- a/doc/source/api_reference/mascons.rst +++ b/doc/source/api_reference/mascons.rst @@ -1,6 +1,6 @@ -======= -mascons -======= +=========== +``mascons`` +=========== Conversion routines for publicly available GRACE/GRACE-FO mascon solutions diff --git a/doc/source/api_reference/ocean_stokes.rst b/doc/source/api_reference/ocean_stokes.rst index 4c84ce82..14226181 100644 --- a/doc/source/api_reference/ocean_stokes.rst +++ b/doc/source/api_reference/ocean_stokes.rst @@ -1,6 +1,6 @@ -============ -ocean_stokes -============ +================ +``ocean_stokes`` +================ - Reads a land-sea mask and converts to a series of spherical harmonics - `netCDF4 land-sea mask files `_ from :cite:p:`Sutterley:2020js` diff --git a/doc/source/api_reference/read_GIA_model.rst b/doc/source/api_reference/read_GIA_model.rst index 31245941..d245141c 100755 --- a/doc/source/api_reference/read_GIA_model.rst +++ b/doc/source/api_reference/read_GIA_model.rst @@ -1,6 +1,6 @@ -============== -read_GIA_model -============== +================== +``read_GIA_model`` +================== - Reads Glacial Isostatic Adjustment (GIA) files for given modeling group formats - Outputs spherical harmonics for the GIA rates and the GIA model parameters diff --git a/doc/source/api_reference/read_GRACE_harmonics.rst b/doc/source/api_reference/read_GRACE_harmonics.rst index 4be343c8..1d4b026d 100644 --- a/doc/source/api_reference/read_GRACE_harmonics.rst +++ b/doc/source/api_reference/read_GRACE_harmonics.rst @@ -1,6 +1,6 @@ -==================== -read_GRACE_harmonics -==================== +======================== +``read_GRACE_harmonics`` +======================== - Reads GRACE/GRACE-FO files and extracts spherical harmonic data and drift rates (RL04) - Adds drift rates to clm and slm for Release-4 harmonics diff --git a/doc/source/api_reference/read_SLR_harmonics.rst b/doc/source/api_reference/read_SLR_harmonics.rst index d3802bad..d979f094 100644 --- a/doc/source/api_reference/read_SLR_harmonics.rst +++ b/doc/source/api_reference/read_SLR_harmonics.rst @@ -1,6 +1,6 @@ -================== -read_SLR_harmonics -================== +====================== +``read_SLR_harmonics`` +====================== - Reads 5\ |times|\ 5 spherical harmonic coefficients with 1 coefficient from degree 6 all calculated from satellite laser ranging (SLR) measurements - Calculated by the University of Texas Center for Space Research (CSR) and NASA Goddard Space Flight Center (GSFC) diff --git a/doc/source/api_reference/read_gfc_harmonics.rst b/doc/source/api_reference/read_gfc_harmonics.rst index 297bbc14..fef77560 100644 --- a/doc/source/api_reference/read_gfc_harmonics.rst +++ b/doc/source/api_reference/read_gfc_harmonics.rst @@ -1,6 +1,6 @@ -================== -read_gfc_harmonics -================== +====================== +``read_gfc_harmonics`` +====================== - Reads gfc files and extracts spherical harmonics for Swarm and GRAZ GRACE/GRACE-FO data - Parses date of GRACE/GRACE-FO data from filename diff --git a/doc/source/api_reference/read_love_numbers.rst b/doc/source/api_reference/read_love_numbers.rst index a087a6c9..712bdea0 100644 --- a/doc/source/api_reference/read_love_numbers.rst +++ b/doc/source/api_reference/read_love_numbers.rst @@ -1,6 +1,6 @@ -================= -read_love_numbers -================= +===================== +``read_love_numbers`` +===================== - Reads sets of load Love/Shida numbers computed using outputs from the Preliminary Reference Earth Model (PREM) or other Earth models - Linearly interpolates load Love/Shida numbers for missing degrees diff --git a/doc/source/api_reference/scripts/calc_harmonic_resolution.rst b/doc/source/api_reference/scripts/calc_harmonic_resolution.rst index 4926eb5d..c23fe1cc 100644 --- a/doc/source/api_reference/scripts/calc_harmonic_resolution.rst +++ b/doc/source/api_reference/scripts/calc_harmonic_resolution.rst @@ -1,6 +1,6 @@ -=========================== -calc_harmonic_resolution.py -=========================== +=============================== +``calc_harmonic_resolution.py`` +=============================== - Calculates the spatial resolution that can be resolved by the spherical harmonics of a certain degree :cite:p:`Barthelmes:2013fy,HofmannWellenhof:2006hy` - Default method uses the smallest half-wavelength that can be resolved diff --git a/doc/source/api_reference/scripts/calc_mascon.rst b/doc/source/api_reference/scripts/calc_mascon.rst index b41ef5b2..67ad6636 100644 --- a/doc/source/api_reference/scripts/calc_mascon.rst +++ b/doc/source/api_reference/scripts/calc_mascon.rst @@ -1,6 +1,6 @@ -============== -calc_mascon.py -============== +================== +``calc_mascon.py`` +================== - Reads in GRACE/GRACE-FO spherical harmonic coefficients - Correct spherical harmonics with the specified GIA model group diff --git a/doc/source/api_reference/scripts/calc_sensitivity_kernel.rst b/doc/source/api_reference/scripts/calc_sensitivity_kernel.rst index 72276dd7..0abfa608 100644 --- a/doc/source/api_reference/scripts/calc_sensitivity_kernel.rst +++ b/doc/source/api_reference/scripts/calc_sensitivity_kernel.rst @@ -1,6 +1,6 @@ -========================== -calc_sensitivity_kernel.py -========================== +============================== +``calc_sensitivity_kernel.py`` +============================== - Calculates spatial sensitivity kernels through a least-squares mascon procedure following :cite:t:`Tiwari:2009bx,Jacob:2012gv` diff --git a/doc/source/api_reference/scripts/combine_harmonics.rst b/doc/source/api_reference/scripts/combine_harmonics.rst index d7a383e4..d2481c33 100644 --- a/doc/source/api_reference/scripts/combine_harmonics.rst +++ b/doc/source/api_reference/scripts/combine_harmonics.rst @@ -1,6 +1,6 @@ -==================== -combine_harmonics.py -==================== +======================== +``combine_harmonics.py`` +======================== - Converts a file from the spherical harmonic domain into the spatial domain :cite:p:`Wahr:1998hy` diff --git a/doc/source/api_reference/scripts/convert_harmonics.rst b/doc/source/api_reference/scripts/convert_harmonics.rst index ca37c135..9e720050 100644 --- a/doc/source/api_reference/scripts/convert_harmonics.rst +++ b/doc/source/api_reference/scripts/convert_harmonics.rst @@ -1,6 +1,6 @@ -==================== -convert_harmonics.py -==================== +======================== +``convert_harmonics.py`` +======================== - Converts a file from the spatial domain into the spherical harmonic domain :cite:p:`Wahr:1998hy` diff --git a/doc/source/api_reference/scripts/grace_mean_harmonics.rst b/doc/source/api_reference/scripts/grace_mean_harmonics.rst index 91ab32c3..198d4608 100644 --- a/doc/source/api_reference/scripts/grace_mean_harmonics.rst +++ b/doc/source/api_reference/scripts/grace_mean_harmonics.rst @@ -1,6 +1,6 @@ -======================= -grace_mean_harmonics.py -======================= +=========================== +``grace_mean_harmonics.py`` +=========================== - Calculates the temporal mean of the GRACE/GRACE-FO spherical harmonics for a specified date range - Used to estimate the static gravitational field over a given date rage diff --git a/doc/source/api_reference/scripts/grace_raster_grids.rst b/doc/source/api_reference/scripts/grace_raster_grids.rst index 1867fb45..9cacebdc 100644 --- a/doc/source/api_reference/scripts/grace_raster_grids.rst +++ b/doc/source/api_reference/scripts/grace_raster_grids.rst @@ -1,6 +1,6 @@ -===================== -grace_raster_grids.py -===================== +========================= +``grace_raster_grids.py`` +========================= - Reads in GRACE/GRACE-FO spherical harmonic coefficients and exports projected spatial fields - Correct spherical harmonics with the specified GIA model group diff --git a/doc/source/api_reference/scripts/grace_spatial_error.rst b/doc/source/api_reference/scripts/grace_spatial_error.rst index 07f62f76..ca012ef2 100644 --- a/doc/source/api_reference/scripts/grace_spatial_error.rst +++ b/doc/source/api_reference/scripts/grace_spatial_error.rst @@ -1,6 +1,6 @@ -====================== -grace_spatial_error.py -====================== +========================== +``grace_spatial_error.py`` +========================== - Reads in GRACE/GRACE-FO spherical harmonic coefficients and exports spatial error field following :cite:t:`Wahr:2006bx` - Filters and smooths data with specified processing algorithms :cite:p:`Jekeli:1981vj,Swenson:2006hu` diff --git a/doc/source/api_reference/scripts/grace_spatial_maps.rst b/doc/source/api_reference/scripts/grace_spatial_maps.rst index 09dc7b98..e2614a09 100644 --- a/doc/source/api_reference/scripts/grace_spatial_maps.rst +++ b/doc/source/api_reference/scripts/grace_spatial_maps.rst @@ -1,6 +1,6 @@ -===================== -grace_spatial_maps.py -===================== +========================= +``grace_spatial_maps.py`` +========================= - Reads in GRACE/GRACE-FO spherical harmonic coefficients and exports monthly spatial fields - Correct spherical harmonics with the specified GIA model group diff --git a/doc/source/api_reference/scripts/mascon_reconstruct.rst b/doc/source/api_reference/scripts/mascon_reconstruct.rst index 39f33fc5..d1857f95 100644 --- a/doc/source/api_reference/scripts/mascon_reconstruct.rst +++ b/doc/source/api_reference/scripts/mascon_reconstruct.rst @@ -1,6 +1,6 @@ -===================== -mascon_reconstruct.py -===================== +========================= +``mascon_reconstruct.py`` +========================= - Calculates the equivalent spherical harmonics from a mascon time series diff --git a/doc/source/api_reference/scripts/piecewise_grace_maps.rst b/doc/source/api_reference/scripts/piecewise_grace_maps.rst index 611aef51..2fba64dd 100644 --- a/doc/source/api_reference/scripts/piecewise_grace_maps.rst +++ b/doc/source/api_reference/scripts/piecewise_grace_maps.rst @@ -1,6 +1,6 @@ -======================= -piecewise_grace_maps.py -======================= +=========================== +``piecewise_grace_maps.py`` +=========================== - Reads in GRACE/GRACE-FO spatial files and fits a piecewise regression model at each grid point for breakpoint analysis diff --git a/doc/source/api_reference/scripts/regress_grace_maps.rst b/doc/source/api_reference/scripts/regress_grace_maps.rst index 309c3480..14ead595 100644 --- a/doc/source/api_reference/scripts/regress_grace_maps.rst +++ b/doc/source/api_reference/scripts/regress_grace_maps.rst @@ -1,6 +1,6 @@ -===================== -regress_grace_maps.py -===================== +========================= +``regress_grace_maps.py`` +========================= - Reads in GRACE/GRACE-FO spatial files and fits a regression model at each grid point diff --git a/doc/source/api_reference/scripts/run_sea_level_equation.rst b/doc/source/api_reference/scripts/run_sea_level_equation.rst index 00b0dd04..2149087a 100644 --- a/doc/source/api_reference/scripts/run_sea_level_equation.rst +++ b/doc/source/api_reference/scripts/run_sea_level_equation.rst @@ -1,6 +1,6 @@ -========================= -run_sea_level_equation.py -========================= +============================= +``run_sea_level_equation.py`` +============================= - Solves the sea level equation with the option of including polar motion feedback :cite:p:`Farrell:1976hm,Kendall:2005ds,Mitrovica:2003cq` - Uses a Clenshaw summation to calculate the spherical harmonic summation :cite:p:`Holmes:2002ff,Tscherning:1982tu` diff --git a/doc/source/api_reference/scripts/scale_grace_maps.rst b/doc/source/api_reference/scripts/scale_grace_maps.rst index 8f1a19ea..1fb72a7b 100644 --- a/doc/source/api_reference/scripts/scale_grace_maps.rst +++ b/doc/source/api_reference/scripts/scale_grace_maps.rst @@ -1,6 +1,6 @@ -=================== -scale_grace_maps.py -=================== +======================= +``scale_grace_maps.py`` +======================= - Reads in GRACE/GRACE-FO spherical harmonic coefficients and exports scaled spatial fields - Correct spherical harmonics with the specified GIA model group diff --git a/doc/source/api_reference/sea_level_equation.rst b/doc/source/api_reference/sea_level_equation.rst index 0be5ac99..449dc782 100644 --- a/doc/source/api_reference/sea_level_equation.rst +++ b/doc/source/api_reference/sea_level_equation.rst @@ -1,6 +1,6 @@ -================== -sea_level_equation -================== +====================== +``sea_level_equation`` +====================== - Solves the sea level equation with the option of including polar motion feedback diff --git a/doc/source/api_reference/spatial.rst b/doc/source/api_reference/spatial.rst index d4b5c441..9048aeeb 100644 --- a/doc/source/api_reference/spatial.rst +++ b/doc/source/api_reference/spatial.rst @@ -1,15 +1,15 @@ -======= -spatial -======= +=========== +``spatial`` +=========== Spatial data class for reading, writing and processing spatial data - Can read ascii, netCDF4, HDF5 files - Can read from an index of the above file types - - Can merge a list of ``spatial`` objects into a single object + - Can merge a list of :py:class:`spatial` objects into a single object - Can subset to a list of GRACE/GRACE-FO months - - Can calculate the mean field of a ``spatial`` object - - Can output ``spatial`` objects to ascii, netCDF4 or HDF5 files + - Can calculate the mean field of a :py:class:`spatial` object + - Can output :py:class:`spatial` objects to ascii, netCDF4 or HDF5 files Calling Sequence ================ diff --git a/doc/source/api_reference/time.rst b/doc/source/api_reference/time.rst index 339666e9..26334198 100644 --- a/doc/source/api_reference/time.rst +++ b/doc/source/api_reference/time.rst @@ -1,6 +1,6 @@ -==== -time -==== +======== +``time`` +======== Utilities for calculating time operations diff --git a/doc/source/api_reference/time_series/amplitude.rst b/doc/source/api_reference/time_series/amplitude.rst index 062c336b..b9b93518 100644 --- a/doc/source/api_reference/time_series/amplitude.rst +++ b/doc/source/api_reference/time_series/amplitude.rst @@ -1,6 +1,6 @@ -===================== -time_series.amplitude -===================== +========================= +``time_series.amplitude`` +========================= - Calculate the amplitude and phase of a harmonic function from calculated sine and cosine of a series of measurements diff --git a/doc/source/api_reference/time_series/fit.rst b/doc/source/api_reference/time_series/fit.rst index e39c6328..359bb306 100644 --- a/doc/source/api_reference/time_series/fit.rst +++ b/doc/source/api_reference/time_series/fit.rst @@ -1,6 +1,6 @@ -=============== -time_series.fit -=============== +=================== +``time_series.fit`` +=================== - Utilities for fitting time-series data with regression models diff --git a/doc/source/api_reference/time_series/lomb_scargle.rst b/doc/source/api_reference/time_series/lomb_scargle.rst index f88f5a39..0f1aafeb 100644 --- a/doc/source/api_reference/time_series/lomb_scargle.rst +++ b/doc/source/api_reference/time_series/lomb_scargle.rst @@ -1,6 +1,6 @@ -======================== -time_series.lomb_scargle -======================== +============================ +``time_series.lomb_scargle`` +============================ - Wrapper function for computing Lomb-Scargle periodograms using ``scipy.signal.lombscargle`` diff --git a/doc/source/api_reference/time_series/piecewise.rst b/doc/source/api_reference/time_series/piecewise.rst index 58b43416..fc4f5bb9 100644 --- a/doc/source/api_reference/time_series/piecewise.rst +++ b/doc/source/api_reference/time_series/piecewise.rst @@ -1,6 +1,6 @@ -===================== -time_series.piecewise -===================== +========================= +``time_series.piecewise`` +========================= - Fits a synthetic signal to data over a time period by ordinary or weighted least-squares for breakpoint analysis diff --git a/doc/source/api_reference/time_series/regress.rst b/doc/source/api_reference/time_series/regress.rst index 75fc1612..67dee571 100644 --- a/doc/source/api_reference/time_series/regress.rst +++ b/doc/source/api_reference/time_series/regress.rst @@ -1,6 +1,6 @@ -=================== -time_series.regress -=================== +======================= +``time_series.regress`` +======================= - Fits a synthetic signal to data over a time period by ordinary or weighted least-squares diff --git a/doc/source/api_reference/time_series/savitzky_golay.rst b/doc/source/api_reference/time_series/savitzky_golay.rst index d50df073..92b133b2 100644 --- a/doc/source/api_reference/time_series/savitzky_golay.rst +++ b/doc/source/api_reference/time_series/savitzky_golay.rst @@ -1,6 +1,6 @@ -========================== -time_series.savitzky_golay -========================== +============================== +``time_series.savitzky_golay`` +============================== - Smooth and optionally differentiate data of non-uniform sampling with a Savitzky-Golay filter - A type of low-pass filter, particularly suited for smoothing noisy data diff --git a/doc/source/api_reference/time_series/smooth.rst b/doc/source/api_reference/time_series/smooth.rst index 093db7ee..7bfbf86c 100644 --- a/doc/source/api_reference/time_series/smooth.rst +++ b/doc/source/api_reference/time_series/smooth.rst @@ -1,6 +1,6 @@ -================== -time_series.smooth -================== +====================== +``time_series.smooth`` +====================== - Computes the moving average of a time-series diff --git a/doc/source/api_reference/tools.rst b/doc/source/api_reference/tools.rst index d3fdbf56..30100e10 100644 --- a/doc/source/api_reference/tools.rst +++ b/doc/source/api_reference/tools.rst @@ -1,6 +1,6 @@ -===== -tools -===== +========= +``tools`` +========= `User interface `_ and plotting tools for use in `Jupyter notebooks `_ diff --git a/doc/source/api_reference/units.rst b/doc/source/api_reference/units.rst index 851d5327..9f445f3a 100644 --- a/doc/source/api_reference/units.rst +++ b/doc/source/api_reference/units.rst @@ -1,6 +1,6 @@ -===== -units -===== +========= +``units`` +========= Class for converting GRACE/GRACE-FO Level-2 data to specific units diff --git a/doc/source/api_reference/utilities.rst b/doc/source/api_reference/utilities.rst index 001ccb0d..3ce419e2 100644 --- a/doc/source/api_reference/utilities.rst +++ b/doc/source/api_reference/utilities.rst @@ -1,6 +1,6 @@ -========= -utilities -========= +============= +``utilities`` +============= Download and management utilities for syncing time and auxiliary files @@ -18,6 +18,14 @@ General Methods .. autofunction:: gravity_toolkit.utilities.get_data_path +.. autofunction:: gravity_toolkit.utilities.get_cache_path + +.. autofunction:: gravity_toolkit.utilities.import_dependency + +.. autofunction:: gravity_toolkit.utilities.dependency_available + +.. autofunction:: gravity_toolkit.utilities.is_valid_url + .. autoclass:: gravity_toolkit.utilities.reify :members: diff --git a/doc/source/api_reference/utilities/make_grace_index.rst b/doc/source/api_reference/utilities/make_grace_index.rst index b0f732e6..ad052001 100644 --- a/doc/source/api_reference/utilities/make_grace_index.rst +++ b/doc/source/api_reference/utilities/make_grace_index.rst @@ -1,6 +1,6 @@ -=================== -make_grace_index.py -=================== +======================= +``make_grace_index.py`` +======================= - Creates index files of GRACE/GRACE-FO Level-2 spherical harmonic data files diff --git a/doc/source/api_reference/utilities/quick_mascon_plot.rst b/doc/source/api_reference/utilities/quick_mascon_plot.rst index da0911b2..676042a5 100644 --- a/doc/source/api_reference/utilities/quick_mascon_plot.rst +++ b/doc/source/api_reference/utilities/quick_mascon_plot.rst @@ -1,6 +1,6 @@ -==================== -quick_mascon_plot.py -==================== +======================== +``quick_mascon_plot.py`` +======================== - Plots a mascon time series file for a particular format diff --git a/doc/source/api_reference/utilities/quick_mascon_regress.rst b/doc/source/api_reference/utilities/quick_mascon_regress.rst index 5b951bcc..837412dd 100644 --- a/doc/source/api_reference/utilities/quick_mascon_regress.rst +++ b/doc/source/api_reference/utilities/quick_mascon_regress.rst @@ -1,6 +1,6 @@ -======================= -quick_mascon_regress.py -======================= +=========================== +``quick_mascon_regress.py`` +=========================== - Creates a regression summary file for a mascon time series file diff --git a/doc/source/api_reference/utilities/run_grace_date.rst b/doc/source/api_reference/utilities/run_grace_date.rst index 4638677f..2745c635 100644 --- a/doc/source/api_reference/utilities/run_grace_date.rst +++ b/doc/source/api_reference/utilities/run_grace_date.rst @@ -1,6 +1,6 @@ -================= -run_grace_date.py -================= +===================== +``run_grace_date.py`` +===================== - Wrapper program for running GRACE date and months programs - Reads GRACE/GRACE-FO index files diff --git a/doc/source/background/Background.rst b/doc/source/background/Background.rst index 7a68dab1..29713bd9 100644 --- a/doc/source/background/Background.rst +++ b/doc/source/background/Background.rst @@ -1,104 +1,81 @@ +.. _background: + ========== Background ========== -Measurement Principle -##################### +Background information on the theoretical concepts used in ``gravity-toolkit``. +*It is not necessary to read through this information* in order to use this library, but it may be helpful for understanding the outputs and assumptions. +The information is organized into separate sections, which can be read in any order. + +.. grid:: 2 2 4 4 + :padding: 0 + + .. grid-item-card:: Measurement Principle + :text-align: center + :link: ./Measurement-Principle.html + + :material-outlined:`scale;5em` + + .. grid-item-card:: Spherical Harmonics + :text-align: center + :link: ./Spherical-Harmonics.html + + :octicon:`globe;5em` + + .. grid-item-card:: Love Numbers + :text-align: center + :link: ./Love-Numbers.html + + :material-outlined:`favorite_border;5em` + + .. grid-item-card:: GRACE Data Products + :text-align: center + :link: ./GRACE-Data-Products.html + + :material-outlined:`data_object;5em` -GRACE and the GRACE Follow-on (GRACE-FO) missions each consist of twin satellites in similar low Earth orbits :cite:p:`Tapley:2019cm`. -The primary and secondary instrumentation onboard the GRACE/GRACE-FO satellites are the ranging instrument -(GRACE has a microwave ranging instrument, GRACE-FO has both a microwave ranging instrument and a laser interferometer), -the global positioning system (GPS), the accelerometers and the star cameras. -Data from these instruments are combined to estimate the distance between the two satellites, -the positions of the satellites in space, the pointing vector of the satellites and any non-gravitational -accelerations the satellites experience. +.. grid:: 2 2 4 4 + :padding: 0 -.. admonition:: The Big Idea + .. grid-item-card:: Geocenter Variations + :text-align: center + :link: ./Geocenter-Variations.html - GRACE/GRACE-FO senses changes in gravity by measuring the change in distance between the two twin satellites: + :material-outlined:`swap_vertical_circle;5em` - 1) As the satellites approach a mass anomaly: leading satellite "feels" a greater gravitational attraction and accelerates |rarr| **distance increases** - 2) As the trailing satellite approaches: greater gravitational attraction |rarr| accelerated by the mass anomaly |rarr| **distance decreases** - 3) Leading satellite passes the anomaly: gravitational attraction pulls backwards |rarr| decelerated by the mass anomaly |rarr| **distance decreases** - 4) When the trailing satellite passes the anomaly and leading satellite is far from the anomaly: trailing satellite decelerated by mass anomaly |rarr| **distance increases back to standard separation** + .. grid-item-card:: Spatial Maps + :text-align: center + :link: ./Spatial-Maps.html -All the onboard measurements are combined with estimates of the background gravity field, atmospheric and oceanic variability, -and tides to create the `Level-2 spherical harmonic product of GRACE and GRACE-FO`__. + :material-outlined:`travel_explore;5em` -.. __: https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-docs/gracefo/open/docs/GRACE-FO_L2_UserHandbook.pdf + .. grid-item-card:: Time Series Analysis + :text-align: center + :link: ./Time-Series-Analysis.html -Data Processing -############### + :material-outlined:`line_axis;5em` -There are three main processing centers that create the Level-2 spherical harmonic data as part of the GRACE/GRACE-FO Science Data System (SDS): -the `University of Texas Center for Space Research (CSR) `_, -the `German Research Centre for Geosciences (GeoForschungsZentrum, GFZ) `_ and -the `Jet Propulsion Laboratory (JPL) `_. + .. grid-item-card:: Glossary + :text-align: center + :link: ./Glossary.html -GRACE/GRACE-FO data is freely available in the US from -the `NASA Physical Oceanography Distributed Active Archive Center (PO.DAAC) `_ and -internationally from the `GFZ Information System and Data Center (ISDC) `_. + :material-outlined:`format_list_bulleted;5em` .. tip:: - There are programs within this repository that can sync with both of these data archives: - ``podaac_cumulus.py`` for `PO.DAAC AWS `_ and - ``gfz_isdc_grace_ftp.py`` for the `GFZ ISDC `_. - -Geoid Height -############ - -The Level-2 spherical harmonic product of GRACE and GRACE-FO provides monthly -estimates of the Earth's gravitational field [see :ref:`fig-sphharm`]. -The Earth's gravitational field varies in time as masses on and within the -Earth move and are exchanged between components of the Earth system :cite:p:`Wahr:1998hy`. -The instantaneous shape of the Earth's gravitational field can be described -in terms of an equipotential surface, a surface of constant potential energy -where the gravitational potential is constant :cite:p:`HofmannWellenhof:2006hy`. -The Earth's geoid is the equipotential surface that coincides with global mean -sea level if the oceans were at rest :cite:p:`HofmannWellenhof:2006hy,Wahr:1998hy`. -The distance between the geoid and an Earth reference ellipsoid is the -geoid height (:math:`N`), or the geoidal undulation :cite:p:`HofmannWellenhof:2006hy`. - -.. figure:: ../_assets/geoid_height.svg - :width: 400 - :align: center - - Relationship between ellipsoid height, geoid height, and topographic height :cite:p:`NRC:1997ea` - -In spherical coordinates, the change in the height of the geoid, -:math:`\Delta N(\theta,\phi)`, at colatitude :math:`\theta` and longitude :math:`\phi`, -can be estimated from a series of spherical harmonics as: - -.. math:: - :label: 1 - - \Delta N(\theta,\phi) = a\sum_{l=1}^{l_{max}}\sum_{m=0}^lP_{lm}(\cos\theta)\left[\Delta C_{lm}\cos{m\phi} + \Delta S_{lm}\sin{m\phi}\right] - -where :math:`a` is the average radius of the Earth, -:math:`P_{lm}(\cos\theta)` are the fully-normalized Legendre polynomials of degree :math:`l` and order :math:`m` for the cosine of colatitude :math:`\theta`, and -:math:`\Delta C_{lm}`, :math:`\Delta S_{lm}` are the changes in the cosine and sine spherical harmonics of degree :math:`l` and order :math:`m` :cite:p:`Chao:1987fq`. - -Surface Mass Density -#################### - -The radial component of a density change within the Earth cannot be uniquely -determined using satellite gravity observations alone :cite:p:`Wahr:1998hy`. -However, fluctuations in water storage and transport can be assumed to be largely -concentrated within a thin layer near the Earth's surface :cite:p:`Wahr:1998hy`. -With this assumption, the Earth's surface mass density -(:math:`\Delta\sigma(\theta,\phi)`), the integral of the density change -(:math:`\Delta\rho(r,\theta,\phi)`) through the thin surface layer, -can be estimated as the following: - -.. math:: - :label: 2 - - \Delta\sigma(\theta,\phi) = \frac{a\rho_{ave}}{3}\sum_{l=0}^{l_{max}}\sum_{m=0}^l\frac{2l+1}{1+k_l}P_{lm}(\cos\theta)\left[\Delta C_{lm}\cos{m\phi} + \Delta S_{lm}\sin{m\phi}\right] - -where :math:`\rho_{ave}` is the average density of the Earth, and -:math:`k_l` is the gravitational potential load Love number of degree :math:`l`. -Using this assumption, solid Earth variations occurring outside of this -thin layer, such as Glacial Isostatic Adjustment (GIA) effects, -must be independently estimated and removed. - -.. |rarr| unicode:: U+2192 .. RIGHTWARDS ARROW + For a more in-depth understanding of the underlying concepts: refer to the :ref:`original literature ` and the :ref:`project resources `. + +.. toctree:: + :hidden: + :maxdepth: 1 + :numbered: + :caption: Background + + ./Measurement-Principle.rst + ./Spherical-Harmonics.rst + ./Love-Numbers.rst + ./GRACE-Data-Products.rst + ./Geocenter-Variations.rst + ./Spatial-Maps.rst + ./Time-Series-Analysis.rst + ./Glossary.rst diff --git a/doc/source/getting_started/GRACE-Data-File-Formats.rst b/doc/source/background/GRACE-Data-Products.rst similarity index 82% rename from doc/source/getting_started/GRACE-Data-File-Formats.rst rename to doc/source/background/GRACE-Data-Products.rst index 66bd180d..b0266c5e 100644 --- a/doc/source/getting_started/GRACE-Data-File-Formats.rst +++ b/doc/source/background/GRACE-Data-Products.rst @@ -1,6 +1,23 @@ -================= -Data File Formats -================= +============= +Data Products +============= + +Data Processing +############### + +There are three main processing centers that create the Level-2 spherical harmonic data as part of the GRACE/GRACE-FO Science Data System (SDS): +the `University of Texas Center for Space Research (CSR) `_, +the `German Research Centre for Geosciences (GeoForschungsZentrum, GFZ) `_ and +the `Jet Propulsion Laboratory (JPL) `_. + +GRACE/GRACE-FO data is freely available in the US from +the `NASA Physical Oceanography Distributed Active Archive Center (PO.DAAC) `_ and +internationally from the `GFZ Information System and Data Center (ISDC) `_. + +.. tip:: + There are programs within this repository that can sync with both of these data archives: + :py:mod:`podaac_cumulus.py` for `PO.DAAC AWS `_ and + :py:mod:`gfz_isdc_grace_ftp.py` for the `GFZ ISDC `_. Product Identifier ################## diff --git a/doc/source/background/Geocenter-Variations.rst b/doc/source/background/Geocenter-Variations.rst index b6f3d548..f9cd5127 100644 --- a/doc/source/background/Geocenter-Variations.rst +++ b/doc/source/background/Geocenter-Variations.rst @@ -2,17 +2,10 @@ Geocenter Variations ==================== -Variations in the Earth's geocenter reflect the largest scale -variability of mass within the Earth system, and are essential -inclusions for the complete recovery of surface mass change from -time-variable gravity. -The Earth's geocenter is the translation between the Earth's -center of mass (CM) and center of figure (CF) reference frames. -Measurements of time-variable gravity from GRACE and GRACE Follow-On -(GRACE-FO) are set in an instantaneous center of mass (CM) reference frame. -For most science applications of time-variable gravity, a coordinate -system with an origin coinciding with the Earth's center of figure -(CF) is required. +Variations in the Earth's geocenter reflect the largest scale variability of mass within the Earth system, and are essential inclusions for the complete recovery of surface mass change from time-variable gravity. +The Earth's geocenter is the translation between the Earth's center of mass (CM) and center of figure (CF) reference frames. +Measurements of time-variable gravity from GRACE and GRACE Follow-On (GRACE-FO) are set in an instantaneous center of mass (CM) reference frame. +For most science applications of time-variable gravity, a coordinate system with an origin coinciding with the Earth's center of figure (CF) is required. Geocenter variations are represented by the degree one spherical harmonic terms. .. important:: @@ -21,13 +14,8 @@ Geocenter variations are represented by the degree one spherical harmonic terms. of ocean mass, ice sheet mass change, and terrestrial hydrology due to far-field signals leaking into each regional estimate :cite:p:`Velicogna:2009ft`. -``calc_degree_one.py`` calculates coefficients of degree one by combining -GRACE/GRACE-FO spherical harmonic products with estimates of -ocean bottom pressure (OBP) following :cite:t:`Swenson:2008cr,Sutterley:2019bx`. -The method assumes that the change in global surface mass density, -:math:`\Delta\sigma(\theta,\phi)`, can be separated into individual -land and ocean components using a land-function -:math:`\vartheta(\theta,\phi)` :cite:p:`Swenson:2008cr`. +:py:mod:`calc_degree_one.py` calculates coefficients of degree one by combining GRACE/GRACE-FO spherical harmonic products with estimates of ocean bottom pressure (OBP) following :cite:t:`Swenson:2008cr,Sutterley:2019bx`. +The method assumes that the change in global surface mass density, :math:`\Delta\sigma(\theta,\phi)`, can be separated into individual land and ocean components using a land-function :math:`\vartheta(\theta,\phi)` :cite:p:`Swenson:2008cr`. .. math:: :label: 4 @@ -35,30 +23,16 @@ land and ocean components using a land-function \Delta\sigma(\theta,\phi) &= \Delta\sigma_{land}(\theta,\phi) + \Delta\sigma_{ocean}(\theta,\phi)\\ \Delta\sigma_{ocean}(\theta,\phi) &= \vartheta(\theta,\phi)~\Delta\sigma(\theta,\phi) -The oceanic components of the change in degree one spherical harmonics -(:math:`\Delta C^{ocean}_{10}`, :math:`\Delta C^{ocean}_{11}`, and :math:`\Delta S^{ocean}_{11}`) -can then be calculated from the changes in ocean mass, -:math:`\Delta\sigma_{ocean}(\theta,\phi)` :cite:p:`Swenson:2008cr,Wahr:1998hy`. -If the oceanic contributions to degree one variability -(:math:`\Delta C^{ocean}_{10}`, :math:`\Delta C^{ocean}_{11}`, and :math:`\Delta S^{ocean}_{11}`) -can be estimated from an ocean model, then the unknown complete degree one terms -(:math:`\Delta C_{10}`, :math:`\Delta C_{11}`, and :math:`\Delta S_{11}`) can be -calculated from the residual between the oceanic degree one terms and the -measured mass change over the ocean calculated using all other degrees of -the global spherical harmonics from GRACE/GRACE-FO :cite:p:`Swenson:2008cr,Sutterley:2019bx`. - -The ``calc_degree_one.py`` program will output geocenter files in ascii format -for each GRACE/GRACE-FO month following :cite:t:`Sutterley:2019bx`. -Uncertainties in geocenter due to a combination of error sources can be -estimated using the ``monte_carlo_degree_one.py`` program. +The oceanic components of the change in degree one spherical harmonics (:math:`\Delta C^{ocean}_{10}`, :math:`\Delta C^{ocean}_{11}`, and :math:`\Delta S^{ocean}_{11}`) can then be calculated from the changes in ocean mass, :math:`\Delta\sigma_{ocean}(\theta,\phi)` :cite:p:`Swenson:2008cr,Wahr:1998hy`. +If the oceanic contributions to degree one variability (:math:`\Delta C^{ocean}_{10}`, :math:`\Delta C^{ocean}_{11}`, and :math:`\Delta S^{ocean}_{11}`) can be estimated from an ocean model, then the unknown complete degree one terms (:math:`\Delta C_{10}`, :math:`\Delta C_{11}`, and :math:`\Delta S_{11}`) can be calculated from the residual between the oceanic degree one terms and the measured mass change over the ocean calculated using all other degrees of the global spherical harmonics from GRACE/GRACE-FO :cite:p:`Swenson:2008cr,Sutterley:2019bx`. + +The :py:mod:`calc_degree_one.py` program will output geocenter files in ascii format for each GRACE/GRACE-FO month following :cite:t:`Sutterley:2019bx`. +Uncertainties in geocenter due to a combination of error sources can be estimated using the :py:mod:`monte_carlo_degree_one.py` program. Load Love Numbers ################# -The degree one Love number of gravitational potential :math:`k_1` is defined so -that the degree one terms describe the offset between the center of mass (CM) -of the combined surface mass and deformed solid Earth, and the center of figure (CF) -of the deformed solid Earth surface :cite:p:`Trupin:1992kp,Blewitt:2003bz`. +The degree one Love number of gravitational potential :math:`k_1` is defined so that the degree one terms describe the offset between the center of mass (CM) of the combined surface mass and deformed solid Earth, and the center of figure (CF) of the deformed solid Earth surface :cite:p:`Trupin:1992kp,Blewitt:2003bz`. For the CF coordinate system, this means .. math:: @@ -66,14 +40,12 @@ For the CF coordinate system, this means k_1 = -(h_1 + 2\ell_1)/3 -where :math:`h_1` and :math:`\ell_1` are the degree one vertical and -horizontal displacement Love numbers. +where :math:`h_1` and :math:`\ell_1` are the degree one vertical and horizontal displacement Love numbers. Geocenter and Degree One ######################## -Fully-normalized degree one variations can be converted to -cartesian geocenter variations using the following relation: +Fully-normalized degree one variations can be converted to cartesian geocenter variations using the following relation: .. math:: :label: 6 @@ -83,6 +55,4 @@ cartesian geocenter variations using the following relation: \Delta Z &= a\sqrt{3}~\Delta C_{10} -The ``geocenter`` class has utilities for converting between -spherical harmonics and geocenter variation along with -readers for different geocenter datasets. +The :py:class:`geocenter` class has utilities for converting between spherical harmonics and geocenter variation along with readers for different geocenter datasets. diff --git a/doc/source/background/Glossary.rst b/doc/source/background/Glossary.rst new file mode 100644 index 00000000..1bb7bd1e --- /dev/null +++ b/doc/source/background/Glossary.rst @@ -0,0 +1,51 @@ +.. _gravity-glossary: + +======== +Glossary +======== + +.. glossary:: + + Body Tide + see :term:`Solid Earth Tide` + + Chandler Wobble + small, semi-periodic deviations in the motion of the pole of rotation + + Epoch + fixed point in time used as a reference value + + Flattening + ratio of the difference between the semi-major and semi-minor axes of an ellipsoid to the semi-major axis + + Free Core Nutation + nearly diurnal deviations in the motion of the pole of rotation due to the resonant motion of the Earth's core relative to the mantle + + see :term:`Nutation` + + Geopotential + the Earth's gravitational potential + + Geoid + equipotential surface coinciding with the ocean surface in the absence of astronomical or dynamical effects + + Love and Shida Numbers + dimensionless parameters relating the vertical (`h`), horizontal (`l`) and gravitational (`k`) elastic responses to tidal loading + + Nutation + short-period oscillations in the motion of the pole of rotation of a freely rotating body + + Polar Motion + irregular motion of the Earth's pole of rotation relative to the Earth's crust + + Pole Tide + apparent tide due to variations in the Earth's axis of rotation about its mean + + Precession + regular conical motion of the pole of rotation of a freely rotating body + + Solid Earth Tide + deformation of the solid Earth due to gravitational forces + + Tilt Factor + Combination of :term:`Love/Shida numbers ` describing the displacement of the Earth's ocean surface with respect to the Earth's deformed crust diff --git a/doc/source/background/Love-Numbers.rst b/doc/source/background/Love-Numbers.rst new file mode 100644 index 00000000..9c1fb014 --- /dev/null +++ b/doc/source/background/Love-Numbers.rst @@ -0,0 +1,36 @@ +.. _love-and-shida-numbers: + +====================== +Love and Shida Numbers +====================== + +When the mass distribution on the surface of the Earth varies, the solid Earth deforms both elastically and inelastically. +This deformation has three distinct components: + +1. a vertical (radial) displacement of the surface +2. a horizontal (tangential) displacement of the surface +3. a change in the gravitational potential + +The magnitude of each individual component can be characterised by a dimensionless scaling factor :cite:p:`Love:1909eh,Shida:1912dj`. +These factors are collectively known as :term:`Love/Shida Numbers `, and are defined for each :ref:`spherical harmonic degree `. + +.. list-table:: + :header-rows: 1 + :align: center + + * - Symbol + - Name + * - :math:`h_l` + - Love number (vertical) + * - :math:`k_l` + - Love number (potential) + * - :math:`l_l` + - Shida number (horizontal) + +.. _load-love-numbers: + +Load Love Numbers +----------------- + +Load Love numbers describe the deformation of the solid Earth in response to a change in *surface mass load*. +The loading change acts upon the *surface of the Earth* :cite:p:`Wahr:1998hy`. diff --git a/doc/source/background/Measurement-Principle.rst b/doc/source/background/Measurement-Principle.rst new file mode 100644 index 00000000..0b6a8925 --- /dev/null +++ b/doc/source/background/Measurement-Principle.rst @@ -0,0 +1,26 @@ +Measurement Principle +##################### + +GRACE and the GRACE Follow-on (GRACE-FO) missions each consist of twin satellites in similar low Earth orbits :cite:p:`Tapley:2019cm`. +The primary and secondary instrumentation onboard the GRACE/GRACE-FO satellites are the ranging instrument +(GRACE has a microwave ranging instrument, GRACE-FO has both a microwave ranging instrument and a laser interferometer), +the global positioning system (GPS), the accelerometers and the star cameras. +Data from these instruments are combined to estimate the distance between the two satellites, +the positions of the satellites in space, the pointing vector of the satellites and any non-gravitational +accelerations the satellites experience. + +.. admonition:: The Big Idea + + GRACE/GRACE-FO senses changes in gravity by measuring the change in distance between the two twin satellites: + + 1) As the satellites approach a mass anomaly: leading satellite "feels" a greater gravitational attraction and accelerates |rarr| **distance increases** + 2) As the trailing satellite approaches: greater gravitational attraction |rarr| accelerated by the mass anomaly |rarr| **distance decreases** + 3) Leading satellite passes the anomaly: gravitational attraction pulls backwards |rarr| decelerated by the mass anomaly |rarr| **distance decreases** + 4) When the trailing satellite passes the anomaly and leading satellite is far from the anomaly: trailing satellite decelerated by mass anomaly |rarr| **distance increases back to standard separation** + +All the onboard measurements are combined with estimates of the background gravity field, atmospheric and oceanic variability, +and tides to create the `Level-2 spherical harmonic product of GRACE and GRACE-FO`__. + +.. __: https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-docs/gracefo/open/docs/GRACE-FO_L2_UserHandbook.pdf + +.. |rarr| unicode:: U+2192 .. RIGHTWARDS ARROW diff --git a/doc/source/background/Spatial-Maps.rst b/doc/source/background/Spatial-Maps.rst index f6126a5b..ba64ead7 100644 --- a/doc/source/background/Spatial-Maps.rst +++ b/doc/source/background/Spatial-Maps.rst @@ -11,7 +11,7 @@ remove unwanted sources of gravitational variability, and convert to appropriate .. tip:: - The ``grace_spatial_maps.py`` program will output spatial files in ascii, netCDF4 or HDF5 format + The :py:mod:`grace_spatial_maps.py` program will output spatial files in ascii, netCDF4 or HDF5 format for each GRACE/GRACE-FO month. Load Love Numbers @@ -29,7 +29,7 @@ removed from the spherical harmonic coefficients :cite:p:`Wahr:1998hy`. Here, we use load Love and Shida numbers with parameters calculated from the Preliminary Reference Earth model (PREM) :cite:p:`Farrell:1972cm,Dziewonski:1981bz`. In order to help estimate the uncertainty in elastic deformation, -``grace_spatial_maps.py`` can use different sets of load Love numbers by adjusting the +:py:mod:`grace_spatial_maps.py` can use different sets of load Love numbers by adjusting the ``--love`` command line option. Reference Frames @@ -44,13 +44,13 @@ Applications set in a center of figure (CF) reference frame, such as the recovery of mass variations of the oceans, hydrosphere and cryosphere, require the inclusion of degree one terms to be fully accurate :cite:p:`Swenson:2008cr`. -``grace_spatial_maps.py`` has geocenter options to select the degree one product to +:py:mod:`grace_spatial_maps.py` has geocenter options to select the degree one product to include with the GRACE/GRACE-FO derived harmonics. There are options for using measurements from satellite laser ranging :cite:p:`Cheng:2013tz` and calculations from time-variable gravity and ocean model outputs :cite:p:`Swenson:2008cr,Sutterley:2019bx`. If including degree one harmonics and changing the reference frame, the reference frame for the load Love numbers needs to be updated accordingly :cite:p:`Blewitt:2003bz`. -In ``grace_spatial_maps.py`` and other GRACE/GRACE-FO programs, the reference frame for the load Love numbers +In :py:mod:`grace_spatial_maps.py` and other GRACE/GRACE-FO programs, the reference frame for the load Love numbers is adjusted by setting the ``--reference`` command line option to ``'CF'``. Low-Degree Harmonics @@ -83,7 +83,7 @@ The figure axis harmonics (:math:`C_{21}` and :math:`S_{21}`) may also be contam by noise during the single-accelerometer months in the GFZ products :cite:p:`Dahle:2019jf`. Measurements from satellite laser ranging (SLR) can provide an independent assessment for some low degree and order spherical harmonics. -``grace_spatial_maps.py`` has options for replacing +:py:mod:`grace_spatial_maps.py` has options for replacing :math:`C_{20}`, :math:`C_{21}`, :math:`S_{21}`, diff --git a/doc/source/background/Spherical-Harmonics.rst b/doc/source/background/Spherical-Harmonics.rst index ad5d1617..d8a4ea06 100644 --- a/doc/source/background/Spherical-Harmonics.rst +++ b/doc/source/background/Spherical-Harmonics.rst @@ -1,9 +1,51 @@ -:orphan: +.. _spherical-harmonics: + +Spherical Harmonics +=================== + +Geoid Height +------------ + +The Level-2 spherical harmonic product of GRACE and GRACE-FO provides monthly estimates of the Earth's gravitational field [see :ref:`fig-sphharm`]. +The Earth's gravitational field varies in time as masses on and within the Earth move and are exchanged between components of the Earth system :cite:p:`Wahr:1998hy`. +The instantaneous shape of the Earth's gravitational field can be described in terms of an equipotential surface, a surface of constant potential energy where the gravitational potential is constant :cite:p:`HofmannWellenhof:2006hy`. +The Earth's geoid is the equipotential surface that coincides with global mean sea level if the oceans were at rest :cite:p:`HofmannWellenhof:2006hy,Wahr:1998hy`. +The distance between the geoid and an Earth reference ellipsoid is the geoid height (:math:`N`), or the geoidal undulation :cite:p:`HofmannWellenhof:2006hy`. + +.. figure:: ../_assets/geoid_height.svg + :width: 400 + :align: center + + Relationship between ellipsoid height, geoid height, and topographic height :cite:p:`NRC:1997ea` + +In spherical coordinates, the change in the height of the geoid, :math:`\Delta N(\theta,\phi)`, at colatitude :math:`\theta` and longitude :math:`\phi`, can be estimated from a series of spherical harmonics as: + +.. math:: + :label: 1 + + \Delta N(\theta,\phi) = a\sum_{l=1}^{l_{max}}\sum_{m=0}^lP_{lm}(\cos\theta)\left[\Delta C_{lm}\cos{m\phi} + \Delta S_{lm}\sin{m\phi}\right] + +where :math:`a` is the average radius of the Earth, :math:`P_{lm}(\cos\theta)` are the fully-normalized Legendre polynomials of degree :math:`l` and order :math:`m` for the cosine of colatitude :math:`\theta`, and :math:`\Delta C_{lm}`, :math:`\Delta S_{lm}` are the changes in the cosine and sine spherical harmonics of degree :math:`l` and order :math:`m` :cite:p:`Chao:1987fq`. + +Surface Mass Density +-------------------- + +The radial component of a density change within the Earth cannot be uniquely determined using satellite gravity observations alone :cite:p:`Wahr:1998hy`. +However, fluctuations in water storage and transport can be assumed to be largely concentrated within a thin layer near the Earth's surface :cite:p:`Wahr:1998hy`. +With this assumption, the Earth's surface mass density (:math:`\Delta\sigma(\theta,\phi)`), the integral of the density change (:math:`\Delta\rho(r,\theta,\phi)`) through the thin surface layer, can be estimated as the following: + +.. math:: + :label: 2 + + \Delta\sigma(\theta,\phi) = \frac{a\rho_{ave}}{3}\sum_{l=0}^{l_{max}}\sum_{m=0}^l\frac{2l+1}{1+k_l}P_{lm}(\cos\theta)\left[\Delta C_{lm}\cos{m\phi} + \Delta S_{lm}\sin{m\phi}\right] + +where :math:`\rho_{ave}` is the average density of the Earth, and :math:`k_l` is the gravitational potential load Love number of degree :math:`l`. +Using this assumption, solid Earth variations occurring outside of this thin layer, such as Glacial Isostatic Adjustment (GIA) effects, must be independently estimated and removed. .. _fig-sphharm: -Spherical Harmonics -------------------- +Low-Degree Harmonics +-------------------- .. plot:: ./background/sphharm.py :show-source-link: False diff --git a/doc/source/background/Time-Series-Analysis.rst b/doc/source/background/Time-Series-Analysis.rst index 86f5234e..d60fa7ac 100644 --- a/doc/source/background/Time-Series-Analysis.rst +++ b/doc/source/background/Time-Series-Analysis.rst @@ -58,10 +58,10 @@ Getting the kernels "just right" in order to isolate regions of interest takes s The set of least-squares mascon programs have been used in :cite:p:`Velicogna:2014km` and other publications for regional time series analysis. -The ``calc_mascon.py`` program additionally calculates the GRACE/GRACE-FO error +The :py:mod:`calc_mascon.py` program additionally calculates the GRACE/GRACE-FO error harmonics following :cite:t:`Wahr:2006bx`. -The ``calc_mascon.py`` program will output a text file of the time series for each mascon +The :py:mod:`calc_mascon.py` program will output a text file of the time series for each mascon with columns: GRACE/GRACE-FO month, mid-month date in decimal-year format, estimated monthly mass anomaly [Gt], estimated monthly error [Gt], and mascon area [km\ :sup:`2`]. diff --git a/doc/source/background/sphharm.py b/doc/source/background/sphharm.py index c7cca1b6..d340da92 100644 --- a/doc/source/background/sphharm.py +++ b/doc/source/background/sphharm.py @@ -6,8 +6,8 @@ # latitude and longitude dlon, dlat = 0.625, 0.5 -lat = np.arange(-90 + dlat/2.0, 90 + dlat/2.0, dlat) -lon = np.arange(0 + dlon/2.0, 360 + dlon/2.0, dlon) +lat = np.arange(-90 + dlat / 2.0, 90 + dlat / 2.0, dlat) +lon = np.arange(0 + dlon / 2.0, 360 + dlon / 2.0, dlon) gridlon, gridlat = np.meshgrid(lon, lat) nlat, nlon = gridlat.shape # colatitude and longitude in radians @@ -18,33 +18,39 @@ lmin, lmax = (1, 4) # number of rows and columns for subplots nrows = (lmax - lmin) + 1 -ncols = 2*lmax + 1 -# compute associated Legendre functions +ncols = 2 * lmax + 1 +# compute associated Legendre functions Plm, dPlm = gravtk.associated_legendre(lmax, np.cos(theta)) # reshape to [l,m,lat,lon] -Plm = Plm.reshape((lmax+1, lmax+1, nlat, nlon)) +Plm = Plm.reshape((lmax + 1, lmax + 1, nlat, nlon)) # projection for the plots projection = ccrs.Orthographic(central_longitude=0.0, central_latitude=0.0) # plot spherical harmonics -fig = plt.figure(num=1, figsize=(12,7)) -patch = mpatches.Rectangle((0, 0), 0.445, 1, color='0.975', - 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 de2d6649..5c47d145 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -11,25 +11,50 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import os + # import sys +import logging import datetime +import warnings + # sys.path.insert(0, os.path.abspath('.')) import importlib.metadata # -- Project information ----------------------------------------------------- +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', '') + suppress_warning = ( + 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') # -- General configuration --------------------------------------------------- @@ -37,23 +62,37 @@ # 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', } -nb_execution_mode = "off" +# execute notebooks on build +if on_rtd: + nb_execution_mode = 'auto' + nb_execution_excludepatterns = [ + 'notebooks/*.ipynb', + ] + nb_output_stderr = 'remove-warn' +elif on_github: + nb_execution_mode = 'off' +else: + nb_execution_mode = 'auto' + nb_execution_excludepatterns = [ + 'notebooks/*.ipynb', + ] + nb_output_stderr = 'remove-warn' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -80,38 +119,62 @@ # -- 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:', +} + # 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_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/logo.png" +html_logo = '_assets/gravity_logo.png' html_static_path = ['_static'] -repository_url = f"https://github.com/tsutterley/gravity-toolkit" +# fetch the project urls +project_urls = {} +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('/') +# add html context html_context = { - "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', - repository_url, + github_url, ), ( ' License', - f"{repository_url}/blob/main/LICENSE", + f'{github_url}/blob/main/LICENSE', + ), + ( + ' 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 new file mode 100644 index 00000000..ff800b0b --- /dev/null +++ b/doc/source/getting_started/Install.ipynb @@ -0,0 +1,181 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2e01086c", + "metadata": {}, + "source": [ + "# Setup and Installation\n", + "\n", + "## Installation\n", + "\n", + "`gravity-toolkit` is available for download from the [GitHub repository](https://github.com/tsutterley/gravity-toolkit), the [Python Package Index (pypi)](https://pypi.org/project/gravity-toolkit/), and from [conda-forge](https://anaconda.org/conda-forge/gravity-toolkit).\n", + "\n", + "\n", + "The simplest installation for most users will likely be using `conda` or `mamba`:\n", + "\n", + "```bash\n", + "conda install -c conda-forge gravity-toolkit\n", + "```\n", + "\n", + "`conda` installed versions of `gravity-toolkit` can be upgraded to the latest stable release:\n", + "\n", + "```bash\n", + "conda update gravity-toolkit\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "af65ece3", + "metadata": {}, + "source": [ + "(development-install)=\n", + "\n", + "## Development Install\n", + "\n", + "To use the development repository, please fork `gravity-toolkit` into your own account and then clone onto your system:\n", + "\n", + "```bash\n", + "git clone https://github.com/tsutterley/gravity-toolkit.git\n", + "```\n", + "\n", + "`gravity-toolkit` can then be installed within the package directory using `pip`:\n", + "\n", + "```bash\n", + "python3 -m pip install --user .\n", + "```\n", + "\n", + "To include all optional dependencies:\n", + "\n", + "```bash\n", + "python3 -m pip install --user .[all]\n", + "```\n", + "\n", + "The development version of `gravity-toolkit` can also be installed directly from GitHub using `pip`:\n", + "\n", + "```bash\n", + "python3 -m pip install --user git+https://github.com/tsutterley/gravity-toolkit.git\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "14ae498c", + "metadata": {}, + "source": [ + "## Package Management with ``pixi``\n", + "\n", + "Alternatively `pixi` can be used to create a [streamlined environment](https://pixi.sh/) after cloning the repository:\n", + "\n", + "```bash\n", + "pixi install\n", + "```\n", + "\n", + "`pixi` maintains isolated environments for each project, allowing for different versions of `gravity-toolkit` and its dependencies to be used without conflict.\n", + "The `pixi.lock` file within the repository defines the required packages and versions for the environment.\n", + "\n", + "`pixi` can also create shells for running programs within the environment:\n", + "\n", + "```bash\n", + "pixi shell\n", + "```\n", + "\n", + "To see the available tasks within the `gravity-toolkit` workspace:\n", + "\n", + "```bash\n", + "pixi task list\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15f4d37e", + "metadata": { + "tags": [ + "remove-input" + ] + }, + "outputs": [], + "source": [ + "! pixi task list" + ] + }, + { + "cell_type": "markdown", + "id": "158990b9", + "metadata": {}, + "source": [ + "```{note}\n", + "`pixi` is under active development and may change in future releases\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "id": "286e9898", + "metadata": {}, + "source": [ + "## Verifying Installation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dbeee9a1", + "metadata": {}, + "outputs": [], + "source": [ + "import gravity_toolkit as gravtk\n", + "\n", + "print(f'gravity-toolkit version: {gravtk.__version__}')" + ] + }, + { + "cell_type": "markdown", + "id": "1c110ff4", + "metadata": {}, + "source": [ + "## Configuration\n", + "\n", + "The `gravity-toolkit` cache directory is the default location for downloaded GRACE/GRACE-FO and auxiliary data.\n", + "The default cache directory is platform-specific and set by the `platformdirs` package.\n", + "This parametrized path can be _persistently_ overridden on a machine by setting the `GRAVTK_CACHE_DIR` environment variable.\n", + "\n", + "The location of the cache directory used by `gravity-toolkit` can be checked by:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad800ca5", + "metadata": {}, + "outputs": [], + "source": [ + "gravtk.utilities.get_cache_path()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "py13", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/doc/source/getting_started/Install.rst b/doc/source/getting_started/Install.rst deleted file mode 100644 index 2390efb8..00000000 --- a/doc/source/getting_started/Install.rst +++ /dev/null @@ -1,76 +0,0 @@ -============ -Installation -============ - -``gravity-toolkit`` is available for download from the `GitHub repository `_, -the `Python Package Index (pypi) `_, -and from `conda-forge `_. - - -The simplest installation for most users will likely be using ``conda`` or ``mamba``: - -.. code-block:: bash - - conda install -c conda-forge gravity-toolkit - -``conda`` installed versions of ``gravity-toolkit`` can be upgraded to the latest stable release: - -.. code-block:: bash - - conda update gravity-toolkit - -Development Install -################### - -To use the development repository, please fork ``gravity-toolkit`` into your own account and then clone onto your system: - -.. code-block:: bash - - git clone https://github.com/tsutterley/gravity-toolkit.git - -``gravity-toolkit`` can then be installed within the package directory using ``pip``: - -.. code-block:: bash - - python3 -m pip install --user . - -To include all optional dependencies: - -.. code-block:: bash - - python3 -m pip install --user .[all] - -The development version of ``gravity-toolkit`` can also be installed directly from GitHub using ``pip``: - -.. code-block:: bash - - python3 -m pip install --user git+https://github.com/tsutterley/gravity-toolkit.git - -Package Management with ``pixi`` -################################ - -Alternatively ``pixi`` can be used to create a `streamlined environment `_ after cloning the repository: - -.. code-block:: bash - - pixi install - -``pixi`` maintains isolated environments for each project, allowing for different versions of -``gravity-toolkit`` and its dependencies to be used without conflict. The ``pixi.lock`` file within the -repository defines the required packages and versions for the environment. - -``pixi`` can also create shells for running programs within the environment: - -.. code-block:: bash - - pixi shell - -To see the available tasks within the ``gravity-toolkit`` workspace: - -.. code-block:: bash - - pixi task list - -.. note:: - - ``pixi`` is under active development and may change in future releases diff --git a/doc/source/getting_started/NASA-Earthdata.rst b/doc/source/getting_started/NASA-Earthdata.rst deleted file mode 100644 index 441ec213..00000000 --- a/doc/source/getting_started/NASA-Earthdata.rst +++ /dev/null @@ -1,74 +0,0 @@ -============== -NASA Earthdata -============== - -NASA Data Distribution Centers -############################## - -The NASA Earth Science Data Information Systems Project funds and operates -`12 Distributed Active Archive Centers (DAACs) `_ throughout the United States. -These centers have recently transitioned from ftp to https servers. -The https updates are designed to increase performance and improve security during data retrieval. -NASA Earthdata uses `OAuth2 `_, an approach to authentication that protects your personal information. - -- https://urs.earthdata.nasa.gov/documentation -- https://wiki.earthdata.nasa.gov/display/EL/Knowledge+Base - -PO.DAAC -####### -The `Physical Oceanography Distributed Active Archive Center (PO.DAAC) `_ -provides data and related information pertaining to the physical processes and conditions of the global oceans, -including measurements of ocean winds, temperature, topography, salinity, circulation and currents, and sea ice. -PO.DAAC hosts - -PO.DAAC has `migrated its data archive to the Earthdata Cloud `_, -which is hosted in Amazon Web Services (AWS). - -.. tip:: - - If any problems contact JPL PO.DAAC support at `podaac@podaac.jpl.nasa.gov `_ - or the NASA EOSDIS support team `support@earthdata.nasa.gov `_. - -Steps to Sync from PO.DAAC --------------------------- - -1. `Register with NASA Earthdata Login system `_ -2. `Sync time-variable gravity data using your Earthdata credentials `_ - -Can also create a ``.netrc`` file for permanently storing NASA Earthdata credentials: - -.. code-block:: bash - - echo "machine urs.earthdata.nasa.gov login password " >> ~/.netrc - chmod 0600 ~/.netrc - -Or set environmental variables for your NASA Earthdata credentials: - -.. code-block:: bash - - export EARTHDATA_USERNAME= - export EARTHDATA_PASSWORD= - -NASA Common Metadata Repository -############################### - -The NASA Common Metadata Repository (CMR) is a catalog of all data -and service metadata records contained as part of NASA's Earth -Observing System Data and Information System (EOSDIS). -Querying the CMR system is a way of quickly performing a search -through the NASA Earthdata archive. -Basic queries for the granule names, PO.DAAC URLs and modification times -of GRACE/GRACE-FO data are available through the ``cmr`` routine in the -``utilities`` module. -For AWS instances in ``us-west-2``, CMR queries can access urls for S3 endpoints. - -.. code-block:: python - - ids,urls,mtimes = gravity_toolkit.utilities.cmr(mission='grace-fo', - center='JPL', release='RL06', level='L2', product='GSM', - solution='BA01', provider='POCLOUD', endpoint='s3', verbose=False) - -Other Data Access Examples -########################## -- `Curl and Wget `_ -- `Python `_ diff --git a/doc/source/getting_started/Resources.rst b/doc/source/getting_started/Resources.rst index ff632027..6d1c64de 100644 --- a/doc/source/getting_started/Resources.rst +++ b/doc/source/getting_started/Resources.rst @@ -1,3 +1,5 @@ +.. _resources: + ========= Resources ========= @@ -31,6 +33,7 @@ Product Information Processing Standards #################### + - `GRACE-FO CSR Level-2 Processing Standards Document `_ - `GRACE-FO GFZ Level-2 Processing Standards Document `_ - `GRACE-FO JPL Level-2 Processing Standards Document `_ @@ -44,4 +47,4 @@ Software - `frommle: python/C++ software suite for geodesy and Earth Sciences `_ - `geoid-toolkit: python utilities for calculating geoid heights from static gravity field coefficients `_ - `model-harmonics: python tools for working with model synthetic spherical harmonic coefficients `_ -- `dynamic_mascons: 3D global variable-size mascon development `_ \ No newline at end of file +- `dynamic_mascons: 3D global variable-size mascon development `_ diff --git a/doc/source/index.rst b/doc/source/index.rst index 60440b96..526b3c3d 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -9,9 +9,9 @@ coefficients from the NASA/DLR Gravity Recovery and Climate Experiment (GRACE) a the NASA/GFZ Gravity Recovery and Climate Experiment Follow-On (GRACE-FO) missions. Introduction ------------- +============ -.. grid:: 2 2 4 4 +.. grid:: 2 2 2 2 :padding: 0 .. grid-item-card:: Installation @@ -26,12 +26,30 @@ Introduction :material-outlined:`hiking;5em` +User Guide +========== + +.. grid:: 2 2 4 4 + :padding: 0 + + .. grid-item-card:: API Reference + :text-align: center + :link: ./api_reference/API-Reference.html + + :material-outlined:`list_alt;5em` + .. grid-item-card:: Background :text-align: center :link: ./background/Background.html :material-outlined:`library_books;5em` + .. grid-item-card:: NASA Earthdata + :text-align: center + :link: ./user_guide/NASA-Earthdata.html + + :material-outlined:`satellite_alt;5em` + .. grid-item-card:: Examples :text-align: center :link: ./user_guide/Examples.html @@ -39,7 +57,7 @@ Introduction :material-outlined:`apps;5em` Contribute ----------- +========== .. grid:: 2 2 4 4 :padding: 0 @@ -62,6 +80,36 @@ Contribute :material-outlined:`forum;5em` + .. grid-item-card:: Issues + :text-align: center + :link: https://github.com/tsutterley/gravity-toolkit/issues + + :material-outlined:`bug_report;5em` + +Project Details +=============== + +.. grid:: 2 2 4 4 + :padding: 0 + + .. grid-item-card:: Release Notes + :text-align: center + :link: ./release_notes/Release-Notes.html + + :material-outlined:`edit_note;5em` + + .. grid-item-card:: Contributors + :text-align: center + :link: ./project/Contributors.html + + :material-outlined:`diversity_1;5em` + + .. grid-item-card:: License + :text-align: center + :link: ./project/Licenses.html + + :material-outlined:`balance;5em` + .. grid-item-card:: Citation Information :text-align: center :link: ./project/Citations.html @@ -73,168 +121,22 @@ Contribute :hidden: :caption: Getting Started - getting_started/Install.rst + getting_started/Install.ipynb getting_started/Getting-Started.rst - getting_started/NASA-Earthdata.rst - getting_started/GRACE-Data-File-Formats.rst getting_started/Contributing.rst getting_started/Code-of-Conduct.rst getting_started/Resources.rst -.. toctree:: - :maxdepth: 2 - :hidden: - :caption: Background - - background/Background.rst - background/Spatial-Maps.rst - background/Time-Series-Analysis.rst - background/Geocenter-Variations.rst - .. toctree:: :maxdepth: 1 :hidden: :caption: User Guide + api_reference/API-Reference.rst + background/Background.rst + user_guide/NASA-Earthdata.ipynb user_guide/Examples.rst -.. toctree:: - :maxdepth: 1 - :hidden: - :caption: API Reference - - api_reference/associated_legendre.rst - api_reference/clenshaw_summation.rst - api_reference/degree_amplitude.rst - api_reference/destripe_harmonics.rst - api_reference/fourier_legendre.rst - api_reference/gauss_weights.rst - api_reference/gen_averaging_kernel.rst - api_reference/gen_disc_load.rst - api_reference/gen_harmonics.rst - api_reference/gen_point_load.rst - api_reference/gen_spherical_cap.rst - api_reference/gen_stokes.rst - api_reference/geocenter.rst - api_reference/grace_date.rst - api_reference/grace_find_months.rst - api_reference/grace_input_months.rst - api_reference/grace_months_index.rst - api_reference/harmonic_gradients.rst - api_reference/harmonic_summation.rst - api_reference/harmonics.rst - api_reference/legendre.rst - api_reference/legendre_polynomials.rst - api_reference/mascons.rst - api_reference/ocean_stokes.rst - api_reference/read_gfc_harmonics.rst - api_reference/read_GIA_model.rst - api_reference/read_GRACE_harmonics.rst - api_reference/read_love_numbers.rst - api_reference/read_SLR_harmonics.rst - api_reference/sea_level_equation.rst - api_reference/SLR/C20.rst - api_reference/SLR/CS2.rst - api_reference/SLR/C30.rst - api_reference/SLR/C40.rst - api_reference/SLR/C50.rst - api_reference/spatial.rst - api_reference/time.rst - api_reference/time_series/amplitude.rst - api_reference/time_series/fit.rst - api_reference/time_series/lomb_scargle.rst - api_reference/time_series/piecewise.rst - api_reference/time_series/regress.rst - api_reference/time_series/savitzky_golay.rst - api_reference/time_series/smooth.rst - api_reference/tools.rst - api_reference/units.rst - api_reference/utilities.rst - -.. toctree:: - :maxdepth: 1 - :hidden: - :caption: Access - - api_reference/access/cnes_grace_sync.rst - api_reference/access/esa_costg_swarm_sync.rst - api_reference/access/gfz_icgem_costg_ftp.rst - api_reference/access/gfz_isdc_dealiasing_sync.rst - api_reference/access/gfz_isdc_grace_sync.rst - api_reference/access/itsg_graz_grace_sync.rst - api_reference/access/podaac_cumulus.rst - -.. toctree:: - :maxdepth: 1 - :hidden: - :caption: Dealiasing - - api_reference/dealiasing/aod1b_geocenter.rst - api_reference/dealiasing/aod1b_oblateness.rst - api_reference/dealiasing/dealiasing_global_uplift.rst - api_reference/dealiasing/dealiasing_monthly_mean.rst - -.. toctree:: - :maxdepth: 1 - :hidden: - :caption: Geocenter - - api_reference/geocenter/calc_degree_one.rst - api_reference/geocenter/monte_carlo_degree_one.rst - -.. toctree:: - :maxdepth: 1 - :hidden: - :caption: Use Cases - - api_reference/scripts/calc_mascon.rst - api_reference/scripts/calc_harmonic_resolution.rst - api_reference/scripts/calc_sensitivity_kernel.rst - api_reference/scripts/combine_harmonics.rst - api_reference/scripts/convert_harmonics.rst - api_reference/scripts/grace_mean_harmonics.rst - api_reference/scripts/grace_raster_grids.rst - api_reference/scripts/grace_spatial_error.rst - api_reference/scripts/grace_spatial_maps.rst - api_reference/scripts/mascon_reconstruct.rst - api_reference/scripts/piecewise_grace_maps.rst - api_reference/scripts/regress_grace_maps.rst - api_reference/scripts/run_sea_level_equation.rst - api_reference/scripts/scale_grace_maps.rst - -.. toctree:: - :maxdepth: 1 - :hidden: - :caption: Mapping - - api_reference/mapping/plot_AIS_grid_maps.rst - api_reference/mapping/plot_AIS_grid_3maps.rst - api_reference/mapping/plot_AIS_grid_4maps.rst - api_reference/mapping/plot_AIS_grid_movie.rst - api_reference/mapping/plot_AIS_GrIS_maps.rst - api_reference/mapping/plot_AIS_regional_maps.rst - api_reference/mapping/plot_AIS_regional_movie.rst - api_reference/mapping/plot_global_grid_maps.rst - api_reference/mapping/plot_global_grid_3maps.rst - api_reference/mapping/plot_global_grid_4maps.rst - api_reference/mapping/plot_global_grid_5maps.rst - api_reference/mapping/plot_global_grid_9maps.rst - api_reference/mapping/plot_global_grid_movie.rst - api_reference/mapping/plot_GrIS_grid_maps.rst - api_reference/mapping/plot_GrIS_grid_3maps.rst - api_reference/mapping/plot_GrIS_grid_5maps.rst - api_reference/mapping/plot_GrIS_grid_movie.rst - -.. toctree:: - :maxdepth: 1 - :hidden: - :caption: Utilities - - api_reference/utilities/make_grace_index.rst - api_reference/utilities/quick_mascon_plot.rst - api_reference/utilities/quick_mascon_regress.rst - api_reference/utilities/run_grace_date.rst - .. toctree:: :maxdepth: 1 :hidden: @@ -245,6 +147,13 @@ Contribute project/Testing.rst project/Citations.rst +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: Release Notes + + release_notes/Release-Notes.rst + .. toctree:: :maxdepth: 1 :hidden: diff --git a/doc/source/notebooks/GRACE-Geostrophic-Maps.ipynb b/doc/source/notebooks/GRACE-Geostrophic-Maps.ipynb index a147a48d..0221b80e 100644 --- a/doc/source/notebooks/GRACE-Geostrophic-Maps.ipynb +++ b/doc/source/notebooks/GRACE-Geostrophic-Maps.ipynb @@ -26,9 +26,10 @@ "source": [ "import numpy as np\n", "import matplotlib\n", + "\n", "matplotlib.rcParams['mathtext.default'] = 'regular'\n", - "matplotlib.rcParams[\"animation.html\"] = \"jshtml\"\n", - "matplotlib.rcParams[\"animation.embed_limit\"] = 50\n", + "matplotlib.rcParams['animation.html'] = 'jshtml'\n", + "matplotlib.rcParams['animation.embed_limit'] = 50\n", "import matplotlib.pyplot as plt\n", "import matplotlib.animation as animation\n", "import matplotlib.offsetbox as offsetbox\n", @@ -59,11 +60,7 @@ "# set the directory with GRACE/GRACE-FO data\n", "# update local data with PO.DAAC https servers\n", "widgets = gravtk.tools.widgets()\n", - "ipywidgets.VBox([\n", - " widgets.directory,\n", - " widgets.update,\n", - " widgets.endpoint\n", - "])" + "ipywidgets.VBox([widgets.directory, widgets.update, widgets.endpoint])" ] }, { @@ -121,12 +118,9 @@ "# update widgets\n", "widgets.select_product()\n", "# display widgets for setting GRACE/GRACE-FO parameters\n", - "ipywidgets.VBox([\n", - " widgets.center,\n", - " widgets.release,\n", - " widgets.product,\n", - " widgets.months\n", - "])" + "ipywidgets.VBox(\n", + " [widgets.center, widgets.release, widgets.product, widgets.months]\n", + ")" ] }, { @@ -156,19 +150,21 @@ "# update widgets\n", "widgets.select_options()\n", "# display widgets for setting GRACE/GRACE-FO read parameters\n", - "ipywidgets.VBox([\n", - " widgets.lmax,\n", - " widgets.mmax,\n", - " widgets.geocenter,\n", - " widgets.C20,\n", - " widgets.CS21,\n", - " widgets.CS22,\n", - " widgets.C30,\n", - " widgets.C40,\n", - " widgets.C50,\n", - " widgets.pole_tide,\n", - " widgets.atm\n", - "])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.lmax,\n", + " widgets.mmax,\n", + " widgets.geocenter,\n", + " widgets.C20,\n", + " widgets.CS21,\n", + " widgets.CS22,\n", + " widgets.C30,\n", + " widgets.C40,\n", + " widgets.C50,\n", + " widgets.pole_tide,\n", + " widgets.atm,\n", + " ]\n", + ")" ] }, { @@ -206,11 +202,27 @@ "# read GRACE/GRACE-FO data for parameters\n", "start_mon = np.min(months)\n", "end_mon = np.max(months)\n", - "missing = sorted(set(np.arange(start_mon,end_mon+1)) - set(months))\n", - "Ylms = gravtk.grace_input_months(widgets.base_directory, PROC, DREL, DSET,\n", - " LMAX, start_mon, end_mon, missing, SLR_C20, DEG1, MMAX=MMAX,\n", - " SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40,\n", - " SLR_C50=SLR_C50, POLE_TIDE=POLE_TIDE, ATM=ATM)\n", + "missing = sorted(set(np.arange(start_mon, end_mon + 1)) - set(months))\n", + "Ylms = gravtk.grace_input_months(\n", + " widgets.base_directory,\n", + " PROC,\n", + " DREL,\n", + " DSET,\n", + " LMAX,\n", + " start_mon,\n", + " end_mon,\n", + " missing,\n", + " SLR_C20,\n", + " DEG1,\n", + " MMAX=MMAX,\n", + " SLR_21=SLR_21,\n", + " SLR_22=SLR_22,\n", + " SLR_C30=SLR_C30,\n", + " SLR_C40=SLR_C40,\n", + " SLR_C50=SLR_C50,\n", + " POLE_TIDE=POLE_TIDE,\n", + " ATM=ATM,\n", + ")\n", "# create harmonics object and remove mean\n", "GRACE_Ylms = gravtk.harmonics().from_dict(Ylms)\n", "GRACE_Ylms.mean(apply=True)\n", @@ -310,15 +322,19 @@ "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])" + "widgets.gaussian.value = 600.0\n", + "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", + ")" ] }, { @@ -339,7 +355,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -350,43 +366,52 @@ "\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", "\n", "# Computing plms for converting to spatial domain\n", - "theta = (90.0 - grid.lat)*np.pi/180.0\n", - "PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(theta))\n", + "theta = np.radians(90.0 - grid.lat)\n", + "PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(theta[valid]))\n", "RAD = widgets.gaussian.value\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", - "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", @@ -397,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", @@ -407,50 +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", - "# mask equatorial regions due to hydrostrophic inaccuracies\n", - "valid, = np.nonzero((np.abs(grid.lat) > 10))\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", @@ -462,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();" ] @@ -490,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])" ] }, { @@ -500,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", @@ -551,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", @@ -575,7 +654,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3.8.10 64-bit", + "display_name": "py13", "language": "python", "name": "python3" }, @@ -589,12 +668,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" - }, - "vscode": { - "interpreter": { - "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" - } + "version": "3.13.0" } }, "nbformat": 4, diff --git a/doc/source/notebooks/GRACE-Harmonic-Plots.ipynb b/doc/source/notebooks/GRACE-Harmonic-Plots.ipynb index 8b3bbba6..6f83b711 100644 --- a/doc/source/notebooks/GRACE-Harmonic-Plots.ipynb +++ b/doc/source/notebooks/GRACE-Harmonic-Plots.ipynb @@ -26,9 +26,10 @@ "source": [ "import numpy as np\n", "import matplotlib\n", + "\n", "matplotlib.rcParams['mathtext.default'] = 'regular'\n", - "matplotlib.rcParams[\"animation.html\"] = \"jshtml\"\n", - "matplotlib.rcParams[\"animation.embed_limit\"] = 50\n", + "matplotlib.rcParams['animation.html'] = 'jshtml'\n", + "matplotlib.rcParams['animation.embed_limit'] = 50\n", "import matplotlib.pyplot as plt\n", "import matplotlib.animation as animation\n", "import ipywidgets\n", @@ -57,11 +58,7 @@ "# set the directory with GRACE/GRACE-FO data\n", "# update local data with PO.DAAC https servers\n", "widgets = gravtk.tools.widgets()\n", - "ipywidgets.VBox([\n", - " widgets.directory,\n", - " widgets.update,\n", - " widgets.endpoint\n", - "])" + "ipywidgets.VBox([widgets.directory, widgets.update, widgets.endpoint])" ] }, { @@ -122,12 +119,9 @@ "# update widgets\n", "widgets.select_product()\n", "# display widgets for setting GRACE/GRACE-FO parameters\n", - "ipywidgets.VBox([\n", - " widgets.center,\n", - " widgets.release,\n", - " widgets.product,\n", - " widgets.months\n", - "])" + "ipywidgets.VBox(\n", + " [widgets.center, widgets.release, widgets.product, widgets.months]\n", + ")" ] }, { @@ -157,19 +151,21 @@ "# update widgets\n", "widgets.select_options()\n", "# display widgets for setting GRACE/GRACE-FO read parameters\n", - "ipywidgets.VBox([\n", - " widgets.lmax,\n", - " widgets.mmax,\n", - " widgets.geocenter,\n", - " widgets.C20,\n", - " widgets.CS21,\n", - " widgets.CS22,\n", - " widgets.C30,\n", - " widgets.C40,\n", - " widgets.C50,\n", - " widgets.pole_tide,\n", - " widgets.atm\n", - "])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.lmax,\n", + " widgets.mmax,\n", + " widgets.geocenter,\n", + " widgets.C20,\n", + " widgets.CS21,\n", + " widgets.CS22,\n", + " widgets.C30,\n", + " widgets.C40,\n", + " widgets.C50,\n", + " widgets.pole_tide,\n", + " widgets.atm,\n", + " ]\n", + ")" ] }, { @@ -207,11 +203,27 @@ "# read GRACE/GRACE-FO data for parameters\n", "start_mon = np.min(months)\n", "end_mon = np.max(months)\n", - "missing = sorted(set(np.arange(start_mon,end_mon+1)) - set(months))\n", - "Ylms = gravtk.grace_input_months(widgets.base_directory, PROC, DREL, DSET,\n", - " LMAX, start_mon, end_mon, missing, SLR_C20, DEG1, MMAX=MMAX,\n", - " SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40,\n", - " SLR_C50=SLR_C50, POLE_TIDE=POLE_TIDE, ATM=ATM)\n", + "missing = sorted(set(np.arange(start_mon, end_mon + 1)) - set(months))\n", + "Ylms = gravtk.grace_input_months(\n", + " widgets.base_directory,\n", + " PROC,\n", + " DREL,\n", + " DSET,\n", + " LMAX,\n", + " start_mon,\n", + " end_mon,\n", + " missing,\n", + " SLR_C20,\n", + " DEG1,\n", + " MMAX=MMAX,\n", + " SLR_21=SLR_21,\n", + " SLR_22=SLR_22,\n", + " SLR_C30=SLR_C30,\n", + " SLR_C40=SLR_C40,\n", + " SLR_C50=SLR_C50,\n", + " POLE_TIDE=POLE_TIDE,\n", + " ATM=ATM,\n", + ")\n", "# create harmonics object and remove mean\n", "GRACE_Ylms = gravtk.harmonics().from_dict(Ylms)\n", "GRACE_Ylms.mean(apply=True)\n", @@ -247,16 +259,19 @@ "widgets.select_corrections(units=['cmwe', 'mmGH'])\n", "widgets.select_output()\n", "# display widgets for setting GRACE/GRACE-FO corrections parameters\n", - "ipywidgets.VBox([\n", - " widgets.GIA_file,\n", - " widgets.GIA,\n", - " widgets.remove_file,\n", - " widgets.remove_format,\n", - " widgets.redistribute_removed,\n", - " widgets.mask,\n", - " widgets.gaussian,\n", - " widgets.destripe,\n", - " widgets.units])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.GIA_file,\n", + " widgets.GIA,\n", + " widgets.remove_file,\n", + " widgets.remove_format,\n", + " widgets.redistribute_removed,\n", + " widgets.mask,\n", + " widgets.gaussian,\n", + " widgets.destripe,\n", + " widgets.units,\n", + " ]\n", + ")" ] }, { @@ -281,22 +296,28 @@ "# read load love numbers file\n", "# PREM outputs from Han and Wahr (1995)\n", "# https://doi.org/10.1111/j.1365-246X.1995.tb01819.x\n", - "love_numbers_file = gravtk.utilities.get_data_path(['data','love_numbers'])\n", + "love_numbers_file = gravtk.utilities.get_data_path(['data', 'love_numbers'])\n", "header = 2\n", - "columns = ['l','hl','kl','ll']\n", + "columns = ['l', 'hl', 'kl', 'll']\n", "# LMAX of load love numbers from Han and Wahr (1995) is 696.\n", "# from Wahr (2007) linearly interpolating kl works\n", "# however, as we are linearly extrapolating out, do not make\n", "# LMAX too much larger than 696\n", "# read arrays of kl, hl, and ll Love Numbers\n", - "hl,kl,ll = gravtk.read_love_numbers(love_numbers_file,\n", - " LMAX=LMAX, HEADER=header, COLUMNS=columns,\n", - " REFERENCE='CF', FORMAT='tuple')\n", + "hl, kl, ll = gravtk.read_love_numbers(\n", + " love_numbers_file,\n", + " LMAX=LMAX,\n", + " HEADER=header,\n", + " COLUMNS=columns,\n", + " REFERENCE='CF',\n", + " FORMAT='tuple',\n", + ")\n", "\n", "# read GIA data\n", "GIA = widgets.GIA.value\n", - "GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA(widgets.GIA_model,\n", - " GIA=GIA, mmax=MMAX)\n", + "GIA_Ylms_rate = gravtk.gia(lmax=LMAX).from_GIA(\n", + " widgets.GIA_model, GIA=GIA, mmax=MMAX\n", + ")\n", "gia_str = '' if (GIA == '[None]') else f'_{GIA_Ylms_rate.title}'\n", "# calculate the monthly mass change from GIA\n", "# monthly GIA calculated by gia_rate*time elapsed\n", @@ -307,9 +328,10 @@ "# if redistributing removed mass over the ocean\n", "if widgets.redistribute_removed.value:\n", " # read Land-Sea Mask and convert to spherical harmonics\n", - " ocean_Ylms = gravtk.ocean_stokes(widgets.landmask, LMAX,\n", - " MMAX=MMAX, LOVE=(hl,kl,ll))\n", - " \n", + " ocean_Ylms = gravtk.ocean_stokes(\n", + " widgets.landmask, LMAX, MMAX=MMAX, LOVE=(hl, kl, ll)\n", + " )\n", + "\n", "# read data to be removed from GRACE/GRACE-FO monthly harmonics\n", "remove_Ylms = GRACE_Ylms.zeros_like()\n", "remove_Ylms.time[:] = np.copy(GRACE_Ylms.time)\n", @@ -317,45 +339,45 @@ "# If there are files to be removed from the GRACE/GRACE-FO data\n", "# for each file separated by commas\n", "for f in widgets.remove_files:\n", - " if (widgets.remove_format.value == 'netCDF4'):\n", + " if widgets.remove_format.value == 'netCDF4':\n", " # read netCDF4 file\n", " Ylms = gravtk.harmonics().from_netCDF4(f)\n", - " elif (widgets.remove_format.value == 'HDF5'):\n", + " elif widgets.remove_format.value == 'HDF5':\n", " # read HDF5 file\n", " Ylms = gravtk.harmonics().from_HDF5(f)\n", - " elif (widgets.remove_format.value == 'index (ascii)'):\n", + " elif widgets.remove_format.value == 'index (ascii)':\n", " # read index of ascii files\n", - " Ylms = gravtk.harmonics().from_index(f,format='ascii')\n", - " elif (widgets.remove_format.value == 'index (netCDF4)'):\n", + " Ylms = gravtk.harmonics().from_index(f, format='ascii')\n", + " elif widgets.remove_format.value == 'index (netCDF4)':\n", " # read index of netCDF4 files\n", - " Ylms = gravtk.harmonics().from_index(f,format='netCDF4')\n", - " elif (widgets.remove_format.value == 'index (HDF5)'):\n", + " Ylms = gravtk.harmonics().from_index(f, format='netCDF4')\n", + " elif widgets.remove_format.value == 'index (HDF5)':\n", " # read index of HDF5 files\n", - " Ylms = gravtk.harmonics().from_index(f,format='HDF5')\n", + " Ylms = gravtk.harmonics().from_index(f, format='HDF5')\n", " # reduce to months of interest and truncate to range\n", - " Ylms = Ylms.subset(months).truncate(LMAX,mmax=MMAX)\n", + " Ylms = Ylms.subset(months).truncate(LMAX, mmax=MMAX)\n", " # redistribute removed mass over the ocean\n", " if widgets.redistribute_removed.value:\n", " # calculate ratio between total removed mass and\n", " # a uniformly distributed cm of water over the ocean\n", - " ratio = Ylms.clm[0,0,:]/ocean_Ylms.clm[0,0]\n", + " ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0]\n", " # for each spherical harmonic\n", - " for m in range(0,MMAX+1):\n", - " for l in range(m,LMAX+1):\n", + " for m in range(0, MMAX + 1):\n", + " for l in range(m, LMAX + 1):\n", " # remove the ratio*ocean Ylms from Ylms\n", - " Ylms.clm[l,m,:]-=ratio*ocean_Ylms.clm[l,m]\n", - " Ylms.slm[l,m,:]-=ratio*ocean_Ylms.slm[l,m]\n", + " Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m]\n", + " Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m]\n", " # add the harmonics to be removed to the total\n", " remove_Ylms.add(Ylms)\n", "\n", "# gaussian smoothing radius in km (Jekeli, 1981)\n", "RAD = widgets.gaussian.value\n", - "if (RAD != 0):\n", - " wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX)\n", + "if RAD != 0:\n", + " wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX)\n", " gw_str = f'_r{RAD:0.0f}km'\n", "else:\n", " # else = 1\n", - " wt = np.ones((LMAX+1))\n", + " wt = np.ones((LMAX + 1))\n", " gw_str = ''\n", "\n", "# destriping the GRACE/GRACE-FO harmonics\n", @@ -365,13 +387,13 @@ "UNITS = widgets.unit_index\n", "# dfactor is the degree dependent coefficients\n", "# for specific spherical harmonic output units\n", - "factors = gravtk.units(lmax=LMAX).harmonic(hl,kl,ll)\n", + "factors = gravtk.units(lmax=LMAX).harmonic(hl, kl, ll)\n", "# 1: cmwe, centimeters water equivalent\n", "# 2: mmGH, millimeters geoid height\n", "dfactor = factors.get(gravtk.units.bycode(UNITS))\n", "# units strings for output files and plots\n", "unit_label = ['cm', 'mm']\n", - "unit_name = ['Equivalent Water Thickness','Geoid Height']\n", + "unit_name = ['Equivalent Water Thickness', 'Geoid Height']\n", "\n", "# converting harmonics to truncated, smoothed coefficients in units\n", "if widgets.destripe.value:\n", @@ -383,7 +405,7 @@ "# Remove GIA estimate for month\n", "Ylms.subtract(GIA_Ylms)\n", "# smooth harmonics and convert to output units\n", - "Ylms.convolve(dfactor*wt)\n", + "Ylms.convolve(dfactor * wt)\n", "# create merged masked array\n", "triangle = Ylms.to_masked_array()" ] @@ -405,7 +427,7 @@ "source": [ "# display widgets for setting GRACE/GRACE-FO regression plot parameters\n", "cmap = gravtk.tools.colormap(vmin=-1, vmax=1)\n", - "ipywidgets.VBox([cmap.name,cmap.reverse])" + "ipywidgets.VBox([cmap.name, cmap.reverse])" ] }, { @@ -424,30 +446,59 @@ "source": [ "%matplotlib inline\n", "# plot spherical harmonics for each month\n", - "fig, ax1 = plt.subplots(num=1, figsize=(8,4))\n", + "fig, ax1 = plt.subplots(num=1, figsize=(8, 4))\n", "\n", "# levels and normalization for plot range\n", - "cmap.value.set_bad('lightgrey',1.)\n", + "cmap.value.set_bad('lightgrey', 1.0)\n", "# imshow = show image (interpolation nearest for blocks)\n", - "im = ax1.imshow(np.ma.zeros((LMAX+1,LMAX+1)), interpolation='nearest',\n", - " cmap=cmap.value, extent=(-LMAX,LMAX,LMAX,0), animated=True)\n", + "im = ax1.imshow(\n", + " np.ma.zeros((LMAX + 1, LMAX + 1)),\n", + " interpolation='nearest',\n", + " cmap=cmap.value,\n", + " extent=(-LMAX, LMAX, LMAX, 0),\n", + " animated=True,\n", + ")\n", "# Z color limit between -1 and 1\n", - "im.set_clim(-1.0,1.0)\n", + "im.set_clim(-1.0, 1.0)\n", "\n", "# add date label (year-calendar month e.g. 2002-01)\n", - "time_text = ax1.text(0.025, 0.025, '', transform=fig.transFigure,\n", - " color='k', size=24, weight='bold', ha='left', va='baseline')\n", + "time_text = ax1.text(\n", + " 0.025,\n", + " 0.025,\n", + " '',\n", + " transform=fig.transFigure,\n", + " color='k',\n", + " size=24,\n", + " weight='bold',\n", + " ha='left',\n", + " va='baseline',\n", + ")\n", "\n", "# add text to label Slm side and Clm side\n", - "t1 = ax1.text(0.39, 0.92, '$S_{lm}$', size=24, weight='bold', \n", - " transform=ax1.transAxes, ha=\"center\", va=\"center\")\n", - "t2 = ax1.text(0.61, 0.92, '$C_{lm}$', size=24, weight='bold', \n", - " transform=ax1.transAxes, ha=\"center\", va=\"center\")\n", + "t1 = ax1.text(\n", + " 0.39,\n", + " 0.92,\n", + " '$S_{lm}$',\n", + " size=24,\n", + " weight='bold',\n", + " transform=ax1.transAxes,\n", + " ha='center',\n", + " va='center',\n", + ")\n", + "t2 = ax1.text(\n", + " 0.61,\n", + " 0.92,\n", + " '$C_{lm}$',\n", + " size=24,\n", + " weight='bold',\n", + " transform=ax1.transAxes,\n", + " ha='center',\n", + " va='center',\n", + ")\n", "# add x and y labels\n", "ax1.set_ylabel('Degree [l]', fontsize=13)\n", "ax1.set_xlabel('Order [m]', fontsize=13)\n", - "ax1.tick_params(axis='both', which='both',\n", - " labelsize=13, direction='in')\n", + "ax1.tick_params(axis='both', which='both', labelsize=13, direction='in')\n", "\n", "# Add horizontal colorbar and adjust size\n", "# extend = add extension triangles to upper and lower bounds\n", @@ -455,32 +506,43 @@ "# pad = distance from main plot axis\n", "# shrink = percent size of colorbar\n", "# aspect = lengthXwidth aspect of colorbar\n", - "cbar = plt.colorbar(im, ax=ax1, extend='both', extendfrac=0.0375,\n", - " orientation='vertical', pad=0.025, shrink=0.85,\n", - " aspect=15, drawedges=False)\n", + "cbar = plt.colorbar(\n", + " im,\n", + " ax=ax1,\n", + " extend='both',\n", + " extendfrac=0.0375,\n", + " orientation='vertical',\n", + " pad=0.025,\n", + " shrink=0.85,\n", + " aspect=15,\n", + " drawedges=False,\n", + ")\n", "# rasterized colorbar to remove lines\n", "cbar.solids.set_rasterized(True)\n", "# Add label to the colorbar\n", - "cbar.ax.set_ylabel(unit_name[UNITS-1], labelpad=5, fontsize=13)\n", - "cbar.ax.set_xlabel(unit_label[UNITS-1], fontsize=13, rotation=0)\n", - "cbar.ax.xaxis.set_label_coords(0.5,1.065)\n", + "cbar.ax.set_ylabel(unit_name[UNITS - 1], labelpad=5, fontsize=13)\n", + "cbar.ax.set_xlabel(unit_label[UNITS - 1], fontsize=13, rotation=0)\n", + "cbar.ax.xaxis.set_label_coords(0.5, 1.065)\n", "# ticks lines all the way across\n", - "cbar.ax.tick_params(which='both', width=1, length=15, labelsize=13,\n", - " direction='in')\n", - " \n", + "cbar.ax.tick_params(\n", + " which='both', width=1, length=15, labelsize=13, direction='in'\n", + ")\n", + "\n", "# stronger linewidth on frame\n", "[i.set_linewidth(2.0) for i in ax1.spines.values()]\n", "# adjust subplot within figure\n", "fig.patch.set_facecolor('white')\n", - "fig.subplots_adjust(left=0.075,right=0.99,bottom=0.07,top=0.99)\n", + "fig.subplots_adjust(left=0.075, right=0.99, bottom=0.07, top=0.99)\n", + "\n", "\n", "# animate frames\n", "def animate_frames(i):\n", " # set image\n", - " im.set_data(triangle[:,:,i])\n", + " im.set_data(triangle[:, :, i])\n", " # add date label (year-calendar month e.g. 2002-01)\n", - " year,month = gravtk.time.grace_to_calendar(Ylms.month[i])\n", - " time_text.set_text(u'{0:4d}\\u2013{1:02d}'.format(year,month))\n", + " year, month = gravtk.time.grace_to_calendar(Ylms.month[i])\n", + " time_text.set_text('{0:4d}\\u2013{1:02d}'.format(year, month))\n", + "\n", "\n", "# set animation\n", "anim = animation.FuncAnimation(fig, animate_frames, frames=nt)\n", diff --git a/doc/source/notebooks/GRACE-Spatial-Error.ipynb b/doc/source/notebooks/GRACE-Spatial-Error.ipynb index 1b765a04..b66aafe7 100644 --- a/doc/source/notebooks/GRACE-Spatial-Error.ipynb +++ b/doc/source/notebooks/GRACE-Spatial-Error.ipynb @@ -26,6 +26,7 @@ "source": [ "import numpy as np\n", "import matplotlib\n", + "\n", "matplotlib.rcParams['mathtext.default'] = 'regular'\n", "import matplotlib.pyplot as plt\n", "import cartopy.crs as ccrs\n", @@ -54,11 +55,7 @@ "# set the directory with GRACE/GRACE-FO data\n", "# update local data with PO.DAAC https servers\n", "widgets = gravtk.tools.widgets()\n", - "ipywidgets.VBox([\n", - " widgets.directory,\n", - " widgets.update,\n", - " widgets.endpoint\n", - "])" + "ipywidgets.VBox([widgets.directory, widgets.update, widgets.endpoint])" ] }, { @@ -119,12 +116,9 @@ "# update widgets\n", "widgets.select_product()\n", "# display widgets for setting GRACE/GRACE-FO parameters\n", - "ipywidgets.VBox([\n", - " widgets.center,\n", - " widgets.release,\n", - " widgets.product,\n", - " widgets.months\n", - "])" + "ipywidgets.VBox(\n", + " [widgets.center, widgets.release, widgets.product, widgets.months]\n", + ")" ] }, { @@ -154,19 +148,21 @@ "# update widgets\n", "widgets.select_options()\n", "# display widgets for setting GRACE/GRACE-FO read parameters\n", - "ipywidgets.VBox([\n", - " widgets.lmax,\n", - " widgets.mmax,\n", - " widgets.geocenter,\n", - " widgets.C20,\n", - " widgets.CS21,\n", - " widgets.CS22,\n", - " widgets.C30,\n", - " widgets.C40,\n", - " widgets.C50,\n", - " widgets.pole_tide,\n", - " widgets.atm,\n", - "])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.lmax,\n", + " widgets.mmax,\n", + " widgets.geocenter,\n", + " widgets.C20,\n", + " widgets.CS21,\n", + " widgets.CS22,\n", + " widgets.C30,\n", + " widgets.C40,\n", + " widgets.C50,\n", + " widgets.pole_tide,\n", + " widgets.atm,\n", + " ]\n", + ")" ] }, { @@ -204,11 +200,27 @@ "# read GRACE/GRACE-FO data for parameters\n", "start_mon = np.min(months)\n", "end_mon = np.max(months)\n", - "missing = sorted(set(np.arange(start_mon,end_mon+1)) - set(months))\n", - "Ylms = gravtk.grace_input_months(widgets.base_directory, PROC, DREL, DSET,\n", - " LMAX, start_mon, end_mon, missing, SLR_C20, DEG1, MMAX=MMAX,\n", - " SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40,\n", - " SLR_C50=SLR_C50, POLE_TIDE=POLE_TIDE, ATM=ATM)\n", + "missing = sorted(set(np.arange(start_mon, end_mon + 1)) - set(months))\n", + "Ylms = gravtk.grace_input_months(\n", + " widgets.base_directory,\n", + " PROC,\n", + " DREL,\n", + " DSET,\n", + " LMAX,\n", + " start_mon,\n", + " end_mon,\n", + " missing,\n", + " SLR_C20,\n", + " DEG1,\n", + " MMAX=MMAX,\n", + " SLR_21=SLR_21,\n", + " SLR_22=SLR_22,\n", + " SLR_C30=SLR_C30,\n", + " SLR_C40=SLR_C40,\n", + " SLR_C50=SLR_C50,\n", + " POLE_TIDE=POLE_TIDE,\n", + " ATM=ATM,\n", + ")\n", "# create harmonics object and remove mean\n", "GRACE_Ylms = gravtk.harmonics().from_dict(Ylms)\n", "GRACE_Ylms.mean(apply=True)\n", @@ -241,11 +253,9 @@ "# update widgets\n", "widgets.select_corrections()\n", "# display widgets for setting GRACE/GRACE-FO corrections parameters\n", - "ipywidgets.VBox([\n", - " widgets.gaussian,\n", - " widgets.destripe,\n", - " widgets.spacing,\n", - " widgets.interval])" + "ipywidgets.VBox(\n", + " [widgets.gaussian, widgets.destripe, widgets.spacing, widgets.interval]\n", + ")" ] }, { @@ -274,51 +284,57 @@ "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 = (90.0 - grid.lat)*np.pi/180.0\n", + "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 = grid.lon[np.newaxis,:]*np.pi/180.0\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 2b1b908f..a4361aee 100644 --- a/doc/source/notebooks/GRACE-Spatial-Maps.ipynb +++ b/doc/source/notebooks/GRACE-Spatial-Maps.ipynb @@ -37,9 +37,10 @@ "source": [ "import numpy as np\n", "import matplotlib\n", + "\n", "matplotlib.rcParams['mathtext.default'] = 'regular'\n", - "matplotlib.rcParams[\"animation.html\"] = \"jshtml\"\n", - "matplotlib.rcParams[\"animation.embed_limit\"] = 50\n", + "matplotlib.rcParams['animation.html'] = 'jshtml'\n", + "matplotlib.rcParams['animation.embed_limit'] = 50\n", "import matplotlib.pyplot as plt\n", "import matplotlib.animation as animation\n", "import cartopy.crs as ccrs\n", @@ -69,11 +70,7 @@ "# set the directory with GRACE/GRACE-FO data\n", "# update local data with PO.DAAC https servers\n", "widgets = gravtk.tools.widgets()\n", - "ipywidgets.VBox([\n", - " widgets.directory,\n", - " widgets.update,\n", - " widgets.endpoint\n", - "])" + "ipywidgets.VBox([widgets.directory, widgets.update, widgets.endpoint])" ] }, { @@ -134,12 +131,9 @@ "# update widgets\n", "widgets.select_product()\n", "# display widgets for setting GRACE/GRACE-FO parameters\n", - "ipywidgets.VBox([\n", - " widgets.center,\n", - " widgets.release,\n", - " widgets.product,\n", - " widgets.months\n", - "])" + "ipywidgets.VBox(\n", + " [widgets.center, widgets.release, widgets.product, widgets.months]\n", + ")" ] }, { @@ -193,19 +187,21 @@ "# update widgets\n", "widgets.select_options()\n", "# display widgets for setting GRACE/GRACE-FO read parameters\n", - "ipywidgets.VBox([\n", - " widgets.lmax,\n", - " widgets.mmax,\n", - " widgets.geocenter,\n", - " widgets.C20,\n", - " widgets.CS21,\n", - " widgets.CS22,\n", - " widgets.C30,\n", - " widgets.C40,\n", - " widgets.C50,\n", - " widgets.pole_tide,\n", - " widgets.atm\n", - "])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.lmax,\n", + " widgets.mmax,\n", + " widgets.geocenter,\n", + " widgets.C20,\n", + " widgets.CS21,\n", + " widgets.CS22,\n", + " widgets.C30,\n", + " widgets.C40,\n", + " widgets.C50,\n", + " widgets.pole_tide,\n", + " widgets.atm,\n", + " ]\n", + ")" ] }, { @@ -243,11 +239,27 @@ "# read GRACE/GRACE-FO data for parameters\n", "start_mon = np.min(months)\n", "end_mon = np.max(months)\n", - "missing = sorted(set(np.arange(start_mon,end_mon+1)) - set(months))\n", - "Ylms = gravtk.grace_input_months(widgets.base_directory, PROC, DREL, DSET,\n", - " LMAX, start_mon, end_mon, missing, SLR_C20, DEG1, MMAX=MMAX,\n", - " SLR_21=SLR_21, SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40,\n", - " SLR_C50=SLR_C50, POLE_TIDE=POLE_TIDE, ATM=ATM)\n", + "missing = sorted(set(np.arange(start_mon, end_mon + 1)) - set(months))\n", + "Ylms = gravtk.grace_input_months(\n", + " widgets.base_directory,\n", + " PROC,\n", + " DREL,\n", + " DSET,\n", + " LMAX,\n", + " start_mon,\n", + " end_mon,\n", + " missing,\n", + " SLR_C20,\n", + " DEG1,\n", + " MMAX=MMAX,\n", + " SLR_21=SLR_21,\n", + " SLR_22=SLR_22,\n", + " SLR_C30=SLR_C30,\n", + " SLR_C40=SLR_C40,\n", + " SLR_C50=SLR_C50,\n", + " POLE_TIDE=POLE_TIDE,\n", + " ATM=ATM,\n", + ")\n", "# create harmonics object and remove mean\n", "GRACE_Ylms = gravtk.harmonics().from_dict(Ylms)\n", "GRACE_Ylms.mean(apply=True)\n", @@ -343,19 +355,22 @@ "widgets.select_corrections()\n", "widgets.select_output()\n", "# display widgets for setting GRACE/GRACE-FO corrections parameters\n", - "ipywidgets.VBox([\n", - " widgets.GIA_file,\n", - " widgets.GIA,\n", - " widgets.remove_file,\n", - " widgets.remove_format,\n", - " widgets.redistribute_removed,\n", - " widgets.mask,\n", - " widgets.gaussian,\n", - " widgets.destripe,\n", - " widgets.spacing,\n", - " widgets.interval,\n", - " widgets.units,\n", - " widgets.output_format])" + "ipywidgets.VBox(\n", + " [\n", + " widgets.GIA_file,\n", + " widgets.GIA,\n", + " widgets.remove_file,\n", + " widgets.remove_format,\n", + " widgets.redistribute_removed,\n", + " widgets.mask,\n", + " widgets.gaussian,\n", + " widgets.destripe,\n", + " widgets.spacing,\n", + " widgets.interval,\n", + " widgets.units,\n", + " widgets.output_format,\n", + " ]\n", + ")" ] }, { @@ -391,41 +406,48 @@ "dlat = widgets.spacing.value\n", "# Output Degree Interval\n", "INTERVAL = widgets.interval.index + 1\n", - "if (INTERVAL == 1):\n", + "if INTERVAL == 1:\n", " # (-180:180,90:-90)\n", - " nlon = np.int64((360.0/dlon)+1.0)\n", - " nlat = np.int64((180.0/dlat)+1.0)\n", - " grid.lon = -180 + dlon*np.arange(0,nlon)\n", - " grid.lat = 90.0 - dlat*np.arange(0,nlat)\n", - "elif (INTERVAL == 2):\n", + " nlon = np.int64((360.0 / dlon) + 1.0)\n", + " nlat = np.int64((180.0 / dlat) + 1.0)\n", + " grid.lon = -180 + dlon * np.arange(0, nlon)\n", + " grid.lat = 90.0 - dlat * np.arange(0, nlat)\n", + "elif INTERVAL == 2:\n", " # (Degree spacing)/2\n", - " grid.lon = np.arange(-180+dlon/2.0,180+dlon/2.0,dlon)\n", - " grid.lat = np.arange(90.0-dlat/2.0,-90.0-dlat/2.0,-dlat)\n", + " grid.lon = np.arange(-180 + dlon / 2.0, 180 + dlon / 2.0, dlon)\n", + " grid.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat)\n", " nlon = len(grid.lon)\n", " nlat = len(grid.lat)\n", "\n", "# Computing plms for converting to spatial domain\n", - "theta = (90.0-grid.lat)*np.pi/180.0\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/project/Bibliography.rst b/doc/source/project/Bibliography.rst index 73b0782c..52774b51 100644 --- a/doc/source/project/Bibliography.rst +++ b/doc/source/project/Bibliography.rst @@ -1,3 +1,5 @@ +.. _bibliography: + ============ Bibliography ============ diff --git a/doc/source/project/Citations.rst b/doc/source/project/Citations.rst index 6fe33a86..22c21973 100644 --- a/doc/source/project/Citations.rst +++ b/doc/source/project/Citations.rst @@ -38,18 +38,33 @@ Dependencies This software is also dependent on other commonly used Python packages: -- `cartopy: Python package designed for geospatial data processing `_ +- `boto3: Amazon Web Services (AWS) SDK for Python `_ - `future: Compatibility layer between Python 2 and Python 3 `_ -- `h5py: Python interface for Hierarchal Data Format 5 (HDF5) `_ -- `ipywidgets: interactive HTML widgets for Jupyter notebooks and IPython `_ - `lxml: processing XML and HTML in Python `_ - `matplotlib: Python 2D plotting library `_ - `netCDF4: Python interface to the netCDF C library `_ - `numpy: Scientific Computing Tools For Python `_ +- `platformdirs: Python module for determining platform-specific directories `_ - `python-dateutil: powerful extensions to datetime `_ - `PyYAML: YAML parser and emitter for Python `_ - `scipy: Scientific Tools for Python `_ + +Optional Dependencies +--------------------- + +- `cartopy: Python package designed for geospatial data processing `_ +- `dask: Parallel computing with task scheduling `_ +- `geoid-toolkit: Python utilities for calculating geoid heights from static gravity field coefficients `_ +- `gdal: Pythonic interface to the Geospatial Data Abstraction Library (GDAL) `_ +- `h5py: Python interface for Hierarchal Data Format 5 (HDF5) `_ +- `ipywidgets: interactive HTML widgets for Jupyter notebooks and IPython `_ +- `obstore: Simple, high-throughput Python interface for object storage `_ +- `pyarrow: Apache Arrow Python bindings `_ +- `pyshp: Python read/write support for ESRI Shapefile format `_ +- `s3fs: Pythonic file interface to S3 built on top of botocore `_ +- `shapely: PostGIS-ish operations outside a database context for Python `_ - `tkinter: Python interface to the Tcl/Tk GUI toolkit `_ +- `zarr: Chunked, compressed, N-dimensional arrays in Python `_ Disclaimer ########## diff --git a/doc/source/release_notes/Release-Notes.rst b/doc/source/release_notes/Release-Notes.rst new file mode 100644 index 00000000..ae4d55e3 --- /dev/null +++ b/doc/source/release_notes/Release-Notes.rst @@ -0,0 +1,10 @@ +============= +Release Notes +============= + +.. toctree:: + :maxdepth: 1 + :glob: + :reversed: + + * diff --git a/doc/source/release_notes/release-v1.0.2.0.rst b/doc/source/release_notes/release-v1.0.2.0.rst new file mode 100644 index 00000000..3a53fe36 --- /dev/null +++ b/doc/source/release_notes/release-v1.0.2.0.rst @@ -0,0 +1,221 @@ +.. _release-v1.0.2.0: + +===================== +`Release v1.0.2.0`__ +===================== + +* ``docs``: Update documentation to use sphinx and readthedocs +* ``docs``: update ``readthedocs.yml`` to use conda +* ``fix``: update ``environment.yml`` to include pip +* ``docs``: different environment for docs +* ``feat``: add AOD1B oblateness and isomorphic parameters +* ``feat``: add netCDF4 and HDF5 options to read_GIA_model.py +* ``feat``: add GIA step to getting started +* ``feat``: added ``harmonics`` class for correcting GRACE/GRACE-FO data +* ``feat``: add ``mean`` and ``from_dict`` to ``harmonics`` class +* ``feat``: use ``harmonics`` class to ``index``, ``add``, ``subtract``, ``convolve`` and ``destripe`` +* ``refactor``: separate ``units`` into its own class +* ``feat``: ``units`` factors for ``harmonics`` and ``spatial`` +* ``docs``: add header notes to notebook describing GRACE/GRACE-FO measurements +* ``fix``: podaac program default release to RL06 +* ``fix``: enumeration in ``GRACE-Data-File-Formats.md`` +* ``feat``: add gravity model read from GFZ ICGEM +* ``feat``: add more functionality and mathematical functions to ``harmonics`` class +* ``feat``: check dimensions of ``harmonics`` objects if using mathematics functions +* ``feat``: add viscoelastic crustal uplift to ``units`` class for converting GIA rates +* ``docs``: include source code links to documents +* ``fix``: include degree and order in read GIA program +* ``docs``: include level-2 handbooks and processing standard documents +* ``docs``: include notes about pole tide and atmospheric corrections +* ``feat``: add ocean redistribution to ipynb, add spherical harmonic calculations +* ``feat``: add ocean redistribution to ipynb, add spherical harmonic calculations +* ``feat``: add date option to ncdf/hdf5 write programs +* ``fix``: in ``harmonics`` ``from_list`` separate date and sort +* ``feat``: get ``harmonics`` third dimension from shape +* ``feat``: add options to ``flatten`` and ``expand`` ``harmonics`` matrices or arrays +* ``docs``: updated ``README.md`` to have data repositories +* ``docs``: add more notes about spatial units and conversion from harmonics +* ``feat``: include file of load love numbers `(Han and Wahr, 1995) `_ +* ``docs``: update html link to `Martin Mohlenkamp's uguide `_ +* ``docs``: add link to love numbers to getting started doc +* ``feat``: add more legendre and harmonics programs +* ``docs``: updated readme and documentation +* ``docs``: update html links to https +* ``feat``: add webdav program for retrieving PO.DAAC credentials +* ``feat``: add ``netrc`` option to PO.DAAC sync program +* ``feat``: ``netrc`` file can be appended from webdav program +* ``docs``: updated README, getting started and program documentation +* ``feat``: add additional date and conversion programs +* ``feat``: output list of filenames if using ``from_list()`` in ``harmonics`` +* ``fix``: subset and index can output the harmonics filename if set +* ``fix``: increase timeout to 2 minutes in ``podaac_grace_sync.py`` +* ``docs``: Add install with pip from git to readme +* ``docs``: update readme to note CC4 license for non-code content +* ``feat``: Add ``spatial`` class for reading, writing and processing grids +* ``feat``: additional capablities within ``spatial`` class +* ``docs``: update ``spatial`` class documentation +* ``refactor``: reorganize code structure +* ``docs``: update documentation for new code structure +* ``fix``: use dependencies from ``requirements.txt`` in ``setup.py`` +* ``fix``: include files within scripts directory in ``setup.py`` +* ``feat``: add CLI spatial and regression programs +* ``feat``: add ``zeros_like`` to ``harmonics`` class +* ``docs``: update documentation for added modules +* ``docs``: update readme for added modules +* ``feat``: add GRACE spatial error program +* ``docs``: update documentation and readme for changes +* ``feat``: update setup to mark new version +* ``docs``: add note about John to citations +* ``docs``: add function docstrings +* ``feat``: add back level-1b dealiasing sync programs +* ``docs``: update documentation and readme +* ``feat``: add case insensitive file search +* ``feat``: update mask if no ``fill_value`` in ``spatial`` +* ``feat``: update jupyter notebook to use ``spatial`` class in plot +* ``ci``: add github actions for continuous integration +* ``ci``: ``flake8`` linter updates for CI +* ``fix``: add ``scipy`` to ``environment.yml`` and ``requirements.txt`` +* ``feat``: add github dependency for ``geocenter`` to requirements +* ``fix``: remove dependency links in lieu of requirements +* ``ci``: add github actions for continuous integration +* ``test``: add spherical harmonic conversion test +* ``fix``: update regular expressions for ``flake8`` compat +* ``test``: add spherical harmonic conversion test +* ``test``: add test for downloading and reading GRACE data +* ``refactor``: move build opener to ``utilities`` routines +* ``docs``: update readme and documentation for utilities +* ``test``: add more tests for downloading and reading GRACE data +* ``feat``: use ``podaac_list()`` within ``podaac_grace_sync`` program +* ``feat``: ``read_GRACE_harmonics()`` can read ``bytesIO`` objects +* ``feat``: added GFZ ftp download and read test +* ``feat``: added compression options to ``harmonic`` and ``spatial`` file input +* ``fix``: ``flake8`` updates for ``python3`` +* ``fix``: update legendre polynomial programs for divide by zero in differentials +* ``fix``: add ``KeyError`` to ``from_dict`` +* ``fix``: ``flake8`` updates for python3 +* ``feat``: use ``utilities`` to define path to load love numbers file +* ``feat``: include data in package +* ``feat``: Update ``MANIFEST.in`` for included data +* ``ci``: add ``macos-latest`` to testing strategy +* ``ci``: will use homebrew package manager to install dependencies +* ``feat``: add podaac sync within jupyter notebook with magics +* ``refactor``: reorganize base directory: .binder and notebooks +* ``test``: calculate test coverage +* ``test``: upload coverage file in github actions +* ``feat``: include GSFC GRACE mascons in dates +* ``fix``: update python language support +* ``fix``: use ``urllib`` from ``gravity_toolkit`` ``utilities`` +* ``feat``: generalize build opener for different earthdata instances +* ``refactor``: switching to main branch as primary +* ``chore``: update links to main branch in readme and docs +* ``feat``: use ``argparse`` to set parameters +* ``feat``: ``abspath`` and ``expanduser`` in ``argparse`` paths +* ``feat``: add ``spatial`` ascii header option +* ``fix``: update ``spatial`` ``mean`` to catch more exceptions +* ``feat``: add more routines to ``spatial`` class `(#17) `_ +* ``refactor``: update podaac programs to simplify args `(#17) `_ +* ``feat``: add updated CNES sync program `(#18) `_ +* ``feat``: add GFZ ICGEM list for static models `(#18) `_ +* ``docs``: update documentation `(#18) `_ +* ``feat``: added more love number options and from gfc for mean files `(#19) `_ +* ``feat``: add `Sutterley and Velicogna geocenter `_ download `(#19) `_ +* ``feat``: updated SLR geocenter for new solutions from Minkang Cheng `(#19) `_ +* ``feat``: added download for satellite laser ranging (SLR) files from UTCSR `(#19) `_ +* ``feat``: add first public versions of mascon programs `(#20) `_ +* ``docs``: add documentation outlining programs `(#20) `_ +* ``docs``: add documentation outlining grace/grace-fo processing `(#20) `_ +* ``docs``: add blurbs to add Yara's comments `(#20) `_ +* ``docs``: use restructuredtext for background `(#20) `_ +* ``refactor``: generalize utilities for downloading from JPL drive (PO.DAAC/ECCO) `(#21) `_ +* ``feat``: can calculate means (``spatial`` and ``harmonic``) for a subset `(#21) `_ +* ``feat``: add pressure harmonics routines for OBP/surface pressure `(#21) `_ +* ``fix``: update requirements `(#21) `_ +* ``feat``: added ``time`` module to be able to convert delta times `(#22) `_ +* ``refactor``: merged ``convert_calendar_decimal`` and ``convert_julian`` with ``time`` module `(#22) `_ +* ``feat``: update netCDF4 and HDF5 programs for attributes and references `(#22) `_ +* ``docs``: update documentation `(#22) `_ +* ``test``: add test module for time programs `(#22) `_ +* ``feat``: update netCDF and HDF5 programs to read from memory `(#23) `_ +* ``feat``: update ``ftp_list`` and read for protected ftp `(#23) `_ +* ``test``: add ftp connection check `(#23) `_ +* ``refactor``: update ftp programs to use ``utilities`` `(#24) `_ +* ``feat``: add even rounding utility `(#24) `_ +* ``refactor``: moved pressure harmonics function to ``model_harmonics`` `(#24) `_ +* ``feat``: use ``harmonics`` class as output from SH generators `(#25) `_ +* ``feat``: add piecewise regression routine for breakpoint analysis `(#26) `_ +* ``docs``: add harmonic triangle plot notebook +* ``docs``: add regression plots to spatial map notebook +* ``docs``: use ``sphinx_rtd_theme`` for documentation +* ``docs``: change some markdown docs to rst +* ``feat``: add date parser for cases when only a date and no units +* ``docs``: add badges to examples documentation +* ``feat``: add kfactor calculation program to ``spatial`` class +* ``feat``: add degree amplitude function to ``harmonics`` class +* ``fix``: prevent warnings with python3 compatible regex strings in nc/hdf5 read +* ``test``: add point mass test +* ``fix``: modify legendre case with underflow +* ``docs``: update references in point harmonics programs +* ``feat``: added ``replace_masked`` to replace masked values in ``spatial`` data +* ``fix``: in ``spatial`` broadcast mask over third dimension +* ``refactor``: changed remove index to files with specified formats `(#27) `_ +* ``feat``: added generic reader, generic writer and write to list functions `(#27) `_ +* ``feat``: added ``adjust_months`` function to fix "special" months cases `(#27) `_ +* ``feat``: include geocenter read program for coefficents from Sean `(#27) `_ +* ``fix``: replaced ``numpy`` bool to prevent deprecation warning `(#27) `_ +* ``refactor``: generalize kwargs to ascii, netCDF4 and HDF5 readers and writers +* ``refactor``: moved model mascon programs to ``model_harmonics`` +* ``refactor``: merged read ICGEM harmonics with ``geoid_toolkit`` reader +* ``docs``: add contribution guidelines `(#28) `_ +* ``docs``: more documentation standardization `(#28) `_ +* ``ci``: remove python 3.5 from tests `(#28) `_ +* ``docs``: documentation standardization +* ``docs``: update documentation `(#29) `_ +* ``feat``: set a default netrc file and check access `(#29) `_ +* ``feat``: default credentials from environmental variables `(#29) `_ +* ``docs``: add rst format citations to documentation `(#30) `_ +* ``fix``: update CSR SLR function (thanks @hulecom for pointing out the file format change) `(#30) `_ +* ``fix``: update setup file to check if ``readthedocs`` `(#30) `_ +* ``feat``: adding more SLR low-degree replacements `(#31) `_ +* ``docs``: update documentation for SLR harmonics `(#31) `_ +* ``feat``: add parser object for removing commented or empty lines `(#32) `_ +* ``feat``: add GFZ SLR solutions for C20/C21+S21/C30 `(#33) `_ +* ``feat``: add GFZ GravIS geocenter solutions `(#33) `_ +* ``docs``: update documentation for GFZ solutions `(#33) `_ +* ``fix``: update grace input months for GFZ SLR `(#33) `_ +* ``feat``: added option for connection timeout to sync programs `(#34) `_ +* ``fix``: define int/float precision to prevent deprecation warning `(#35) `_ +* ``feat``: use try/except for retrieving netrc credentials `(#35) `_ +* ``feat``: add figshare secure FTP uploader to utilities `(#35) `_ +* ``ci``: use cartopy no-binary in build `(#35) `_ +* ``fix``: use first value in requirements in setup `(#35) `_ +* ``ci``: brew install ``pkg-config`` `(#35) `_ +* ``ci``: use older proj7 in brew install for cartopy `(#35) `_ +* ``ci``: add LD and CPP flags for proj7 `(#35) `_ +* ``ci``: add cython to installations `(#35) `_ +* ``ci``: ``ACCEPT_USE_OF_DEPRECATED_PROJ_API_H`` `(#35) `_ +* ``ci``: set pkg-config path `(#35) `_ +* ``refactor``: switch from parameter files to argparse arguments `(#36) `_ +* ``fix``: degree spacing in spatial programs `(#36) `_ +* ``fix``: cycles in regression program `(#38) `_ +* ``fix``: documentation for spatial programs `(#38) `_ +* ``refactor``: simplified file exports using wrappers in harmonics `(#39) `_ +* ``fix``: gfc format in ``from_file`` wrapper in harmonics `(#39) `_ +* ``fix``: format for mean files `(#40) `_ +* ``docs``: clenshaw summation citations `(#40) `_ +* ``feat``: Add 3-hour AOD interval for RL06 `#37 `_ `(#41) `_ +* ``feat``: release monthly dealiasing (for CSR GAA etc) `(#41) `_ +* ``fix``: inputs to AOD-corrected SLR geocenter coefficients `(#41) `_ +* ``feat``: output index file for monthly dealiasing SHM files `(#42) `_ +* ``feat``: added check if needing to interpolate love numbers `(#42) `_ +* ``feat``: added path to default land-sea mask for mass redistribution `(#42) `_ +* ``feat``: added option to output mean harmonics in gfc format `(#43) `_ +* ``refactor``: rename monthly mean dealiasing program `(#43) `_ +* ``feat``: output uncalibrated spherical harmonic errors (eclm and eslm) `(#43) `_ +* ``fix``: remove choices for argparse processing centers `(#43) `_ +* ``fix``: remove defaults in monthly dealiasing `(#43) `_ +* ``refactor``: no default processing center `(#43) `_ +* ``fix``: require processing center argument `(#43) `_ +* ``feat``: add months option to gfz dealiasing sync `(#43) `_ +* ``feat``: add harmonic resolution calculator `(#44) `_ + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/v1.0.2.0 diff --git a/doc/source/release_notes/release-v1.0.2.4.rst b/doc/source/release_notes/release-v1.0.2.4.rst new file mode 100644 index 00000000..831cd9fc --- /dev/null +++ b/doc/source/release_notes/release-v1.0.2.4.rst @@ -0,0 +1,62 @@ +.. _release-v1.0.2.4: + +===================== +`Release v1.0.2.4`__ +===================== + +* ``fix``: uncertainties for SLR CS21 and CS22 +* ``feat``: add option for setting input format of the mascon files `(#46) `_ +* ``feat``: add averaging kernel program `(#46) `_ +* ``feat``: time-variable gravity data from COST-G, GRAZ, SWARM `#37 `_ `(#47) `_ +* ``feat``: time-variable gravity data from COST-G GRAZ, SWARM `#37 `_ `(#47) `_ +* ``feat``: need to add sync programs for all products `(#47) `_ +* ``refactor``: call read ICGEM from read gfc `(#47) `_ +* ``fix``: Swarm strings and titles `(#47) `_ +* ``docs``: update documentation to add additional information `(#47) `_ +* ``feat``: add sync programs for Swarm and GRACE COST-G `(#47) `_ +* ``feat``: add ITSG GRAZ GRACE sync `(#47) `_ +* ``fix``: accidental copy paste `(#47) `_ +* ``fix``: output index in separate loop for COST-G `(#47) `_ +* ``test``: add tests for COST-G, GRAZ and Swarm gfc files `(#47) `_ +* ``test``: use fixture to download geocenter files `(#47) `_ +* ``refactor``: merged integration and fourier harmonics programs ``fix``: use fill values for input ascii files in convert_harmonics ``fix``: update grid attributes after allocating for data in combine_harmonics ``ci``: install proj from source for cartopy dependency ``ci``: install devel cartopy from repo `(#48) `_ +* ``refactor``: adding new tools module to simplify notebooks `(#49) `_ +* ``feat``: adding new time functions for to/from grace months `(#49) `_ +* ``docs``: add documentation for tools `(#49) `_ +* ``fix``: adjust months if final in time series `(#49) `_ +* ``fix``: adjustable minimum in gaussian weights +* ``refactor``: slim requirements `(#50) `_ +* ``refactor``: using python logging for handling verbose output `(#51) `_ +* ``feat``: add version program and add package __version__ attribute `(#51) `_ +* ``feat``: add more remove file options in GRACE maps `(#52) `_ +* ``fix``: remove file choices in calc mascon `(#52) `_ +* ``fix``: logging ``CRITICAL`` +* ``fix``: logging with filenames +* ``feat``: grid conversion routines for publicly available mascon solutions `(#53) `_ +* ``test``: attempt to download cost-g from http `(#53) `_ +* ``refactor``: use logging to print multiprocessing exceptions +* ``refactor``: netCDF4 and HDF5 input programs `(#54) `_ +* ``ci``: update for geoid-toolkit dependencies `(#54) `_ +* ``docs``: pin docutils to 0.18 `(#54) `_ +* ``fix``: modify legendre normalization to prevent high degree overflows `(#55) `_ +* ``test``: add legendre test `(#55) `_ +* ``test``: add associated legendre polynomial test `(#56) `_ +* ``fix``: HDF5 and netCDF4 io for single time case `(#56) `_ +* ``fix``: format for index in notebooks `(#56) `_ +* ``feat``: use new GSFC weekly 5x5s for CS2 and C50 `(#57) `_ +* ``refactor``: create geocenter program for reading and operating `(#58) `_ +* ``test``: include new geocenter class `(#58) `_ +* ``feat``: add load love numbers for unit conversion `(#58) `_ +* ``feat``: add mean function to geocenter `(#58) `_ +* ``refactor``: rename SLF geocenter reader `(#58) `_ +* ``feat``: add more geocenter operations `(#58) `_ +* ``feat``: add geocenter figure programs from Sutterley and Velicogna (2019) +* ``feat``: update geocenter plot programs for AGU `(#60) `_ +* ``feat``: added netCDF4 reader for UCI iteration files `(#60) `_ +* ``feat``: added custom colormap function for some common scales `(#60) `_ +* ``feat``: added UNITS list option for converting from custom units `(#60) `_ +* ``feat``: option to specify a specific geocenter correction file `(#61) `_ +* ``feat``: can use variable loglevels for verbose output `(#61) `_ +* ``fix``: fix default file prefix to include center and release information `(#61) `_ + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/v1.0.2.4 diff --git a/doc/source/release_notes/release-v1.0.2.5.rst b/doc/source/release_notes/release-v1.0.2.5.rst new file mode 100644 index 00000000..c08a4496 --- /dev/null +++ b/doc/source/release_notes/release-v1.0.2.5.rst @@ -0,0 +1,38 @@ +.. _release-v1.0.2.5: + +===================== +`Release v1.0.2.5`__ +===================== + +* ``fix``: add try/except for read_GRACE_geocenter import +* ``feat``: S3 access using PO.DAAC cumulus to address `#59 `_ `(#63) `_ +* ``fix``: update readable granule for L1A/B grav +* ``fix``: for now use podaac drive provider +* ``docs``: updated docstrings to numpy documentation format `(#65) `_ +* ``docs``: use autodoc to build documentation `(#65) `_ +* ``feat``: new internal ncdf/hdf5 read/write within harmonics and spatial classes `(#65) `_ +* ``feat``: add citations and references to read_GIA_model `(#65) `_ +* ``docs``: update headers to remove deprecated ncdf/hdf5 read/write modules `(#65) `_ +* ``refactor``: moved Load love number wrapper function to within read `(#66) `_ +* ``feat``: change badge for pangeo to aws us-west-2 for podaac cloud access `(#66) `_ +* ``fix``: pin markupsafe to 2.0.1 to prevent soft_unicode error `(#66) `_ +* ``fix``: change function name back to load_love_numbers `(#67) `_ +* ``docs``: update history in headers `(#67) `_ +* ``test``: try docker build with windows to address `#64 `_ `(#68) `_ +* ``fix``: include utf-8 encoding in reads to be windows compliant `(#68) `_ +* ``fix``: only sync newsletters for mission of interest +* ``fix``: spatial field mapping for output +* ``fix``: mask in sea level equation +* ``feat``: add from_GIA to harmonics class `(#69) `_ +* ``feat``: prepare CMR queries for version 1 of RL06 `(#69) `_ +* ``fix``: expansion and squeezing of mask variable if None `(#69) `_ +* ``feat``: add option for L2 version in sync programs `(#69) `_ +* ``fix``: improved passing of filename attribute in harmonic objects `(#70) `_ +* ``fix``: include filename when copying spatial objects `(#70) `_ +* ``refactor``: always try syncing from both grace and grace-fo missions `(#71) `_ +* ``feat``: added AW13 models using IJ05-R2 ice history `(#71) `_ +* ``feat``: allow input ascii harmonic files to have additional columns `(#71) `_ +* ``docs``: update environment file `(#71) `_ +* ``fix``: index using granules `(#71) `_ + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/v1.0.2.5 diff --git a/doc/source/release_notes/release-v1.0.2.6.rst b/doc/source/release_notes/release-v1.0.2.6.rst new file mode 100644 index 00000000..3609aeee --- /dev/null +++ b/doc/source/release_notes/release-v1.0.2.6.rst @@ -0,0 +1,14 @@ +.. _release-v1.0.2.6: + +==================== +`Release v1.0.2.6`__ +==================== + +* ``docs``: use argparse descriptions within sphinx documentation `(#72) `_ +* ``feat``: initial version of public geocenter programs `(#73) `_ +* ``feat``: output full citation for each GIA model group `(#73) `_ +* ``docs``: update background and add geocenter section `(#73) `_ +* ``feat``: added notebook for visualizing harmonic errors `(#74) `_ +* ``feat``: add changes for uploading to pypi `(#75) `_ + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/1.0.2.6 diff --git a/doc/source/release_notes/release-v1.0.2.7.rst b/doc/source/release_notes/release-v1.0.2.7.rst new file mode 100644 index 00000000..aaa4509f --- /dev/null +++ b/doc/source/release_notes/release-v1.0.2.7.rst @@ -0,0 +1,27 @@ +.. _release-v1.0.2.7: + +==================== +`Release v1.0.2.7`__ +==================== + +* ``fix``: expand mask variable within if statement `(#76) `_ +* ``fix``: place ipython and tkinter imports within try/except `(#77) `_ +* ``fix``: remove cartopy for slimmer build `(#78) `_ +* ``fix``: create mask for output gridded variables `(#79) `_ +* ``refactor``: set plot tick formatter to not use offsets `(#79) `_ +* ``docs``: updated structure of documentation `(#80) `_ +* ``feat``: add program for overwriting the GRACE/GRACE-FO index `(#81) `_ +* ``feat``: made creating the spatial sensitivity kernel optional to improve compute time `(#81) `_ +* ``refactor``: moved regular expression function to utilities `(#81) `_ +* ``feat``: Dynamically select newest version of granules for index `(#82) `_ +* ``feat``: include geocenter land sea in data `(#83) `_ +* ``feat``: add GSFC SLR 5x5 download function `(#85) `_ +* ``feat``: add logging for debugging level verbose output `(#85) `_ +* ``fix``: place index filename within try/except statement `(#85) `_ +* ``feat``: add option to replace degree 4 zonal harmonics with SLR `(#86) `_ +* ``fix``: version check for mission in GFZ ftp sync `(#87) `_ +* ``feat``: add spatial gradient programs `(#87) `_ +* ``feat``: added more time parsing for longer periods `(#88) `_ +* ``refactor``: use UCI for geocenter solutions (SLR still valid) `(#88) `_ + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/1.0.2.7 diff --git a/doc/source/release_notes/release-v1.1.0.rst b/doc/source/release_notes/release-v1.1.0.rst new file mode 100644 index 00000000..4c936ef0 --- /dev/null +++ b/doc/source/release_notes/release-v1.1.0.rst @@ -0,0 +1,29 @@ +.. _release-v1.1.0: + +================== +`Release v1.1.0`__ +================== + +* ``feat``: adding CMR queries for TN files `(#89) `_ +* ``docs``: use sphinx-argparse v0.4.0 with exec search `(#89) `_ +* ``refactor``: use f-strings for formatting verbose or ascii output `(#89) `_ +* ``fix``: no gzip of TN files in cumulus program `(#91) `_ +* ``fix``: expose GSFC SLR 5x5 url as option `(#91) `_ +* ``refactor``: camel_case updates for `#90 `_ `(#91) `_ +* ``refactor``: comment format to try to match flake8 styling for `#90 `_ `(#92) `_ +* ``refactor``: implicit import as gravtk `(#93) `_ +* ``feat``: add git utilities for managing repositories `(#93) `_ +* ``test``: add test for comparing GIA reads `(#93) `_ +* ``refactor``: use attributes kwards for output to nc/hdf5 `(#93) `_ +* ``feat``: add netCDF4/HDF5 source attribute to harmonics and spatial classes `(#93) `_ +* ``docs``: slimmer build to prevent overutilization of resources `(#95) `_ +* ``docs``: remove .py from function titles `#96 `_ +* ``feat``: can iterate over harmonics, spatal and geocenter objects `(#97) `_ +* ``fix``: don't include ``get_git_revision_hash`` in output files `(#97) `_ +* ``fix``: set default GIA parameters, title, reference and url as None `(#97) `_ +* ``fix``: iteration over slm harmonics `(#97) `_ +* ``refactor``: SLR, plm and time series functions `(#98) `_ +* ``test``: single implicit import of gravity toolkit `(#98) `_ +* ``chore``: increase version number `(#98) `_ + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/1.1.0 diff --git a/doc/source/release_notes/release-v1.2.0.rst b/doc/source/release_notes/release-v1.2.0.rst new file mode 100644 index 00000000..0f81a432 --- /dev/null +++ b/doc/source/release_notes/release-v1.2.0.rst @@ -0,0 +1,30 @@ +.. _release-v1.2.0: + +================== +`Release v1.2.0`__ +================== + +* ``refactor``: new repo name following `#84 `_ `(#99) `_ +* ``docs``: update docstrings `(#100) `_ +* ``fix``: verify warnings have type and filtered after `(#101) `_ +* ``feat``: added function to retrieve named units `(#102) `_ +* ``fix``: set custom units as top option in if/else statements `(#102) `_ +* ``feat``: added wrapper function for smoothing and converting to output units `(#102) `_ +* ``test``: add test for getting units `(#102) `_ +* ``fix``: expand harmonics case where data is a single degree `(#103) `_ +* ``fix``: harmonics case where maximum spherical harmonic degree is 0 `(#103) `_ +* ``fix``: units case where maximum spherical harmonic degree is 0 `(#103) `_ +* ``fix``: load love number case with maximum spherical harmonic degree is 0 `(#103) `_ +* ``feat``: add PREM hard and soft sediment love numbers `(#104) `_ +* ``feat``: data class for load love numbers with attributes for model `(#105) `_ +* ``feat``: customizable file-level attributes to netCDF4 and HDF5 `(#106) `_ +* ``feat``: add root attributes to output netCDF4 and HDF5 files `(#107) `_ +* ``fix``: only attempt to squeeze from final dimension in harmonics objects `(#108) `_ +* ``feat``: add indexing of filenames to object iterators `(#109) `_ +* ``feat``: new ``scaling_factors`` inheritance of ``spatial`` class `(#110) `_ +* ``refactor``: simplified unit factors in spherical caps and disc loads `(#111) `_ +* ``fix``: case insensitive searching for HDF5 and netCDF4 GIA attributes `(#111) `_ +* ``docs``: slimmer builds by placing imports within try/except `(#111) `_ +* ``refactor``: remove deprecated functions `(#112) `_ + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/1.2.0 diff --git a/doc/source/release_notes/release-v1.2.1.rst b/doc/source/release_notes/release-v1.2.1.rst new file mode 100644 index 00000000..fb215354 --- /dev/null +++ b/doc/source/release_notes/release-v1.2.1.rst @@ -0,0 +1,60 @@ +.. _release-v1.2.1: + +================== +`Release v1.2.1`__ +================== + +* ``feat``: add global dealiasing uplift program `(#113) `_ +* ``refactor``: in monthly dealiasing, read data into flattened ``harmonics`` objects `(#113) `_ +* ``feat``: debug-level logging of member names and header lines `(#113) `_ +* ``feat``: add ``extend_matrix`` function `(#113) `_ +* ``feat``: add ``wrap_longitudes`` to ``tools`` `(#113) `_ +* ``refactor``: convert ``shape`` and ``ndim`` to ``harmonics`` and ``spatial`` class properties `(#113) `_ +* ``refactor``: updated inputs to ``spatial`` ``from_ascii`` function `(#113) `_ +* ``fix``: scale and dimensions `(#113) `_ +* ``fix``: ``spatial`` ``extend_matrix`` `(#113) `_ +* ``feat``: add mapping programs from Sutterley et al. 2019 and 2020 `(#114) `_ +* ``fix``: output file append in regress_grace_maps.py `(#114) `_ +* ``refactor``: merge regional plot programs into single program `(#114) `_ +* ``docs``: add documentation for graphing programs `(#114) `_ +* ``fix``: place cmap set in try/except `(#114) `_ +* ``refactor``: add descriptive file-level attributes to output netCDF4/HDF5 files `(#115) `_ +* ``feat``: include option to not compensate for elastic deformation for `#37 `_ `(#115) `_ +* ``feat``: added regex formatting for CNES GRGS harmonics `(#115) `_ +* ``feat``: added functions for getting unit attributes for known types `(#116) `_ +* ``docs``: improve typing for variables in docstrings `(#116) `_ +* ``feat``: add initial geostrophic current program `(#117) `_ +* ``feat``: add angular velocity of the Earth to ``units`` `(#117) `_ +* ``fix``: set ``case_insensitive_filename`` to ``None`` if filename is empty `(#117) `_ +* ``feat``: add solver option to geocenter and mascons `(#118) `_ +* ``feat``: allow option ``0`` in ``combine_harmonics.py`` for no unit conversion `(#118) `_ +* ``fix``: ``remove_label`` in ``tools.py`` `(#118) `_ +* ``fix``: frame in geostrophic currents notebook `(#118) `_ +* ``fix``: allow love numbers to be None for custom units case `(#119) `_ +* ``fix``: headers in plot script documentation `(#119) `_ +* ``feat``: add notes about shida numbers `(#119) `_ +* ``fix``: podaac now has different openers for s3 and data endpoints `(#120) `_ +* ``feat``: add piecewise program `(#120) `_ +* ``feat``: add additional fit term options to regression programs `(#120) `_ +* ``docs``: remove binder links :( `(#120) `_ +* ``fix``: add podaac cumulus test and skip drive `(#120) `_ +* ``fix``: bump GFZ GravIS files to Release-03 `(#120) `_ +* ``feat``: split S2 tidal aliasing terms into GRACE and GRACE-FO eras `(#121) `_ +* ``feat``: add reify decorator for evaluation of properties `(#121) `_ +* ``refactor``: use formatting for reading from date file `(#121) `_ +* ``refactor``: use ``pathlib`` to define and operate on paths `(#123) `_ +* ``feat``: split S2 tidal aliasing terms into GRACE and GRACE-FO eras `(#123) `_ +* ``fix``: remove deprecated podaac drive sync programs `(#123) `_ +* ``docs``: remove references to deprecated sync programs `(#123) `_ +* ``refactor``: use fit module for getting tidal aliasing terms `(#125) `_ +* ``feat``: add functions to retrieve and revoke Earthdata tokens `(#125) `_ +* ``feat``: more operatations on spatial error if in possible data keys `(#125) `_ +* ``docs``: scrub PO.DAAC Drive and WebDAV `(#126) `_ +* ``fix``: append amplitude and phase titles when creating flags `(#127) `_ +* ``refactor``: modified custom units in spherical caps and disc loads `(#129) `_ +* ``fix``: GRACE/GRACE-FO months in drift function for `#131 `_ `(#132) `_ +* ``feat``: can choose different tidal aliasing periods `(#132) `_ +* ``feat``: add timescale class for converting between time scales `(#133) `_ +* ``feat``: add functions for retrieving leap seconds from NIST or IERF servers `(#133) `_ + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/1.2.1 diff --git a/doc/source/release_notes/release-v1.2.2.rst b/doc/source/release_notes/release-v1.2.2.rst new file mode 100644 index 00000000..4df8255d --- /dev/null +++ b/doc/source/release_notes/release-v1.2.2.rst @@ -0,0 +1,47 @@ +.. _release-v1.2.2: + +================== +`Release v1.2.2`__ +================== + +* ``feat``: add land_stokes function to parallel ocean_stokes `(#138) `_ +* ``docs``: update font and style of flowchart `(#138) `_ +* ``feat``: add zorder to quick mascon plot `(#138) `_ +* ``fix``: environment for auto update `(#138) `_ +* ``feat``: add string representation of major classes `(#139) `_ +* ``feat``: add raster program for GRACE projected fields `(#139) `_ +* ``ci``: bump versions of imported actions `(#139) `_ +* ``ci``: use mamba for builds `(#140) `_ +* ``docs``: use micromamba for RTD builds `(#140) `_ +* ``fix``: check that collection metadata urls exist `(#141) `_ +* ``fix``: don't restrict version number to a set list of presently available `(#141) `_ +* ``feat``: output netCDF4 files for all geocenter runs `(#142) `_ +* ``fix``: plot S11 `(#142) `_ +* ``fix``: prevent double printing of filenames when using debug `(#143) `_ +* ``refactor``: simplify I-matrix and G-matrix calculations `(#143) `_ +* ``feat``: add widget for setting endpoint for accessing PODAAC data `(#145) `_ +* ``fix``: added check to verify access to s3 buckets `(#145) `_ +* ``feat``: add Greenland 5 map code `(#145) `_ +* ``fix``: autoupdate files `(#145) `_ +* ``fix``: add ssl context update for deprecation `(#145) `_ +* ``fix``: spelling mistakes `(#145) `_ +* ``fix``: place time and month variables in try/except block `(#145) `_ +* ``feat``: added argument for products in CMR shortname query `(#145) `_ +* ``fix``: increase mask buffer to twice the smoothing radius in raster `(#145) `_ +* ``refactor``: use wrapper to importlib for optional dependencies `(#146) `_ +* ``feat``: make classes subscriptable and allow item assignment `(#146) `_ +* ``feat``: add lomb scargle wrapper program `(#146) `_ +* ``refactor``: remove timescale class and leap second calculations `(#146) `_ +* ``fix``: update CMR search utility to replace deprecated scrolling `(#147) `_ +* ``fix``: endpoint for TN-13 and TN-14 is now documentation `(#148) `_ +* ``feat``: add function to scrape GSFC website for GRACE mascons `(#149) `_ +* ``fix``: deprecated widget object copies for `#150 `_ `(#151) `_ +* ``fix``: add polar argument for x == +/-1 to prevent drift `(#151) `_ +* ``feat``: allow reading love numbers where degree is infinite `(#151) `_ +* ``fix``: check if the GRACE/GRACE-FO files are gfc format for `#152 `_ `(#153) `_ +* ``docs``: use ``sphinxcontrib-bibtex`` for citations `(#154) `_ +* ``fix``: deprecated tick label resizing `(#155) `_ +* ``docs``: merge multiple bibtex citations together `(#156) `_ +* ``fix``: deprecated tick label resizing `(#156) `_ + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/1.2.2 diff --git a/doc/source/release_notes/release-v1.2.3.rst b/doc/source/release_notes/release-v1.2.3.rst new file mode 100644 index 00000000..152fb85e --- /dev/null +++ b/doc/source/release_notes/release-v1.2.3.rst @@ -0,0 +1,10 @@ +.. _release-v1.2.3: + +================== +`Release v1.2.3`__ +================== + +* ``chore``: bump version to ``1.2.3`` +* ``fix``: update ``MANIFEST.in`` to include version and requirements + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/1.2.3 diff --git a/doc/source/release_notes/release-v1.2.4.rst b/doc/source/release_notes/release-v1.2.4.rst new file mode 100644 index 00000000..e2bcc939 --- /dev/null +++ b/doc/source/release_notes/release-v1.2.4.rst @@ -0,0 +1,13 @@ +.. _release-v1.2.4: + +================== +`Release v1.2.4`__ +================== + +* ``test``: add check for reading lsmasks `(#157) `_ +* ``ci``: verify that ``git-lfs`` is used `(#157) `_ +* ``docs``: add badge for conda-forge `(#157) `_ +* ``chore``: bump version to ``1.2.4`` +* ``docs``: add ``repository-artifact`` to ``CITATION.cff`` + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/1.2.4 diff --git a/doc/source/release_notes/release-v1.2.5.rst b/doc/source/release_notes/release-v1.2.5.rst new file mode 100644 index 00000000..41d83d97 --- /dev/null +++ b/doc/source/release_notes/release-v1.2.5.rst @@ -0,0 +1,34 @@ +.. _release-v1.2.5: + +================== +`Release v1.2.5`__ +================== + +* ``feat``: added options to run SLF from input spatial fields `(#158) `_ +* ``fix``: add netCDF4 to default requirements `(#158) `_ +* ``fix``: copy latitude and longitude as float64 for numpy 2.0 stability `(#158) `_ +* ``feat``: added option to set the density of water in g/cm^3 `(#158) `_ +* ``fix``: earthdata link for `#160 `_ `(#162) `_ +* ``fix``: change default endpoint in cumulus to ``data`` for `#161 `_ `(#162) `_ +* ``ci``: add paths to check if needing to run tests `(#162) `_ +* ``docs``: notebooks in documentation `(#163) `_ +* ``fix``: docstring for UCI geocenter files +* ``refactor``: organize scripts into subdirectories `(#165) `_ +* ``docs``: add logo and color scheme `(#165) `_ +* ``docs``: add annotations `(#165) `_ +* ``docs``: add code of conduct `(#165) `_ +* ``docs``: add project pages `(#165) `_ +* ``refactor``: use ``pyproject.toml`` for builds `(#165) `_ +* ``docs``: adjust ``style.css`` +* ``docs``: add spherical harmonic plots `(#166) `_ +* ``docs``: add ``cartopy`` to ``environment.yml`` `(#166) `_ +* ``ci``: use pixi for builds `(#167) `_ +* ``docs``: use pixi for builds `(#167) `_ +* ``refactor``: GFZ ISDC ftp server is being retired `(#168) `_ +* ``fix``: Update pixi.lock `(#168) `_ +* ``docs``: fix testing description +* ``docs``: fix contributors (now markdown) +* ``fix``: switch ``from_gfz`` to https as ftp server is retired `(#169) `_ +* ``chore``: bump version to ``1.2.5`` + +.. __: https://github.com/tsutterley/gravity-toolkit/releases/tag/1.2.5 diff --git a/doc/source/user_guide/NASA-Earthdata.ipynb b/doc/source/user_guide/NASA-Earthdata.ipynb new file mode 100644 index 00000000..43224b13 --- /dev/null +++ b/doc/source/user_guide/NASA-Earthdata.ipynb @@ -0,0 +1,164 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4c0ed22c", + "metadata": {}, + "source": [ + "# NASA Earthdata\n", + "\n", + "## NASA Data Distribution Centers\n", + "\n", + "The NASA Earth Science Data Information Systems Project funds and operates\n", + "[12 Distributed Active Archive Centers (DAACs)](https://earthdata.nasa.gov/about/daacs) throughout the United States.\n", + "These centers have recently transitioned from ftp to https servers.\n", + "The https updates are designed to increase performance and improve security during data retrieval.\n", + "NASA Earthdata uses [OAuth2](https://wiki.earthdata.nasa.gov/pages/viewpage.action?pageId=71700485), an approach to authentication that protects your personal information.\n", + "\n", + "- \n", + "- \n", + "\n", + "## PO.DAAC\n", + "\n", + "The [Physical Oceanography Distributed Active Archive Center (PO.DAAC)](https://podaac.jpl.nasa.gov/)\n", + "provides data and related information pertaining to the physical processes and conditions of the global oceans,\n", + "including measurements of ocean winds, temperature, topography, salinity, circulation and currents, and sea ice.\n", + "\n", + "PO.DAAC has [migrated its data archive to the Earthdata Cloud](https://podaac.jpl.nasa.gov/cloud-datasets/migration),\n", + "which is hosted in Amazon Web Services (AWS).\n", + "\n", + "```{tip}\n", + "If any problems contact JPL PO.DAAC support at [podaac@podaac.jpl.nasa.gov](mailto:podaac@podaac.jpl.nasa.gov)\n", + "or the NASA EOSDIS support team [support@earthdata.nasa.gov](mailto:support@earthdata.nasa.gov).\n", + "```\n", + "\n", + "### Steps to Sync from PO.DAAC\n", + "\n", + "1. [Register with NASA Earthdata Login system](https://urs.earthdata.nasa.gov/users/new)\n", + "2. [Sync time-variable gravity data using your Earthdata credentials](https://github.com/tsutterley/gravity-toolkit/blob/main/scripts/podaac_cumulus.py)\n", + "\n", + "Can also create a `.netrc` file for permanently storing NASA Earthdata credentials:\n", + "\n", + "```bash\n", + "echo \"machine urs.earthdata.nasa.gov login password \" >> ~/.netrc\n", + "chmod 0600 ~/.netrc\n", + "```\n", + "\n", + "Or set environmental variables for your NASA Earthdata credentials:\n", + "\n", + "```bash\n", + "export EARTHDATA_USERNAME=\n", + "export EARTHDATA_PASSWORD=\n", + "```\n", + "\n", + "## NASA Common Metadata Repository\n", + "\n", + "The NASA Common Metadata Repository (CMR) is a catalog of all data and service metadata records contained as part of NASA's Earth Observing System Data and Information System (EOSDIS).\n", + "Querying the CMR system is a way of quickly performing a search through the NASA Earthdata archive.\n", + "Basic queries for the granule names, PO.DAAC URLs and modification times of GRACE/GRACE-FO data are available through the {py:func}`cmr` routine in the {py:mod}`utilities` module.\n", + "For AWS instances in `us-west-2`, CMR queries can access URLs for S3 endpoints." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cfe94ede", + "metadata": { + "tags": [ + "remove-input" + ] + }, + "outputs": [], + "source": [ + "from IPython import get_ipython\n", + "\n", + "\n", + "def list_formatter(var, pp, *args, **kwargs):\n", + " pp.text('\\n'.join(var))\n", + "\n", + "\n", + "plain = get_ipython().display_formatter.formatters['text/plain']\n", + "plain.for_type(list, list_formatter);" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6a79f49e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/GRACE_GSM_L2_GRAV_JPL_RL06/GSM-2_2002094-2002120_GRAC_JPLEM_BA01_0600\n", + "https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/GRACE_GSM_L2_GRAV_JPL_RL06/GSM-2_2002122-2002138_GRAC_JPLEM_BA01_0600\n", + "https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/GRACE_GSM_L2_GRAV_JPL_RL06/GSM-2_2002213-2002243_GRAC_JPLEM_BA01_0600\n", + "https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/GRACE_GSM_L2_GRAV_JPL_RL06/GSM-2_2002244-2002273_GRAC_JPLEM_BA01_0600\n", + "https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/GRACE_GSM_L2_GRAV_JPL_RL06/GSM-2_2002274-2002304_GRAC_JPLEM_BA01_0600\n", + "https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/GRACE_GSM_L2_GRAV_JPL_RL06/GSM-2_2002305-2002334_GRAC_JPLEM_BA01_0600\n", + "https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/GRACE_GSM_L2_GRAV_JPL_RL06/GSM-2_2002335-2002365_GRAC_JPLEM_BA01_0600\n", + "https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/GRACE_GSM_L2_GRAV_JPL_RL06/GSM-2_2003001-2003031_GRAC_JPLEM_BA01_0600\n", + "https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/GRACE_GSM_L2_GRAV_JPL_RL06/GSM-2_2003032-2003059_GRAC_JPLEM_BA01_0600\n", + "https://archive.podaac.earthdata.nasa.gov/podaac-ops-cumulus-protected/GRACE_GSM_L2_GRAV_JPL_RL06/GSM-2_2003060-2003090_GRAC_JPLEM_BA01_0600" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import gravity_toolkit as gravtk\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])" + ] + }, + { + "cell_type": "markdown", + "id": "83ad8927", + "metadata": { + "vscode": { + "languageId": "plaintext" + } + }, + "source": [ + "## Other Data Access Examples\n", + "\n", + "- [Curl and Wget](https://wiki.earthdata.nasa.gov/display/EL/How+To+Access+Data+With+cURL+And+Wget)\n", + "- [Python](https://wiki.earthdata.nasa.gov/display/EL/How+To+Access+Data+With+Python)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "py13", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/geocenter/calc_degree_one.py b/geocenter/calc_degree_one.py index bf0f5386..271af509 100755 --- a/geocenter/calc_degree_one.py +++ b/geocenter/calc_degree_one.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" calc_degree_one.py -Written by Tyler Sutterley (01/2025) +Written by Tyler Sutterley (07/2026) Calculates degree 1 variations using GRACE coefficients of degree 2 and greater, and ocean bottom pressure variations from ECCO and OMCT/MPIOM @@ -100,6 +100,15 @@ --ocean-file X: Index file for ocean model harmonics --mean-file X: GRACE/GRACE-FO mean file to remove from the harmonic data --mean-format X: Input data format for GRACE/GRACE-FO mean file + --remove-file X: Monthly files to be removed from the GRACE/GRACE-FO data + --remove-format X: Input data format for files to be removed + ascii + netCDF4 + HDF5 + index-ascii + index-netCDF4 + index-HDF5 + --redistribute-removed: redistribute removed mass fields over the ocean --iterative: Iterate degree one solutions -s X, --solver X: Least squares solver for degree one solutions inv: matrix inversion @@ -167,6 +176,9 @@ https://doi.org/10.1029/2007JB005338 UPDATE HISTORY: + Updated 07/2026: use np.einsum for spherical harmonic summations + can remove sets of harmonic files from the GRACE/GRACE-FO data + use np.radians to convert from degrees to radians Updated 01/2025: fixed deprecated tick label resizing Updated 06/2024: use wrapper to importlib for optional dependencies Updated 10/2023: generalize mission variable to be GRACE/GRACE-FO @@ -248,6 +260,7 @@ Forked 06/2013 from calc_deg_one.pro Written 09/2012 """ + from __future__ import print_function import sys @@ -272,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) @@ -281,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 @@ -294,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 @@ -310,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 @@ -340,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 + APx*np.pi/180.0) + \ - SAAx*np.sin(4.0*np.pi*grace_date + SAPx*np.pi/180.0) - Y = AAy*np.sin(2.0*np.pi*grace_date + APy*np.pi/180.0) + \ - SAAy*np.sin(4.0*np.pi*grace_date + SAPy*np.pi/180.0) - Z = AAz*np.sin(2.0*np.pi*grace_date + APz*np.pi/180.0) + \ - SAAz*np.sin(4.0*np.pi*grace_date + SAPz*np.pi/180.0) - 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, @@ -371,6 +439,9 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, DATAFORM=None, MEAN_FILE=None, MEANFORM=None, + REMOVE_FILES=None, + REMOVE_FORMAT=None, + REDISTRIBUTE_REMOVED=False, MODEL_INDEX=None, ITERATIVE=False, SOLVER=None, @@ -379,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') @@ -418,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 @@ -465,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 @@ -477,24 +548,25 @@ 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 = dlon*np.pi/180.0 - dth = dlat*np.pi/180.0 + dphi = np.radians(dlon) + dth = np.radians(dlat) # longitude and colatitude in radians - phi = landsea.lon[np.newaxis,:]*np.pi/180.0 - th = (90.0 - np.squeeze(landsea.lat))*np.pi/180.0 + phi = np.radians(np.squeeze(landsea.lon)) + 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 @@ -505,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 @@ -529,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) @@ -575,17 +669,22 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, GAD = gravtk.geocenter() GAD.time = np.copy(GAD_Ylms.time) GAD.month = np.copy(GAD_Ylms.month) - GAD.C10 = np.zeros((n_files)) - GAD.C11 = np.zeros((n_files)) - GAD.S11 = np.zeros((n_files)) - for t in range(0,n_files): - # converting GAD degree 1 harmonics to mass - # 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[t] = rho_e*rad_e*np.squeeze(GAD_Ylms.clm[1,0,t])*(2.0 + 1.0)/3.0 - GAD.C11[t] = rho_e*rad_e*np.squeeze(GAD_Ylms.clm[1,1,t])*(2.0 + 1.0)/3.0 - GAD.S11[t] = rho_e*rad_e*np.squeeze(GAD_Ylms.slm[1,1,t])*(2.0 + 1.0)/3.0 + GAD.C10 = np.empty((n_files)) + GAD.C11 = np.empty((n_files)) + GAD.S11 = np.empty((n_files)) + # converting GAD degree 1 harmonics to mass + # 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 + ) # removing the mean of the GAD OBP coefficients GAD.mean(apply=True) @@ -594,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 @@ -606,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() @@ -622,49 +721,120 @@ def calc_degree_one(base_dir, PROC, DREL, MODEL, LMAX, RAD, # converting ecco degree 1 harmonics to coefficients of mass OBP = gravtk.geocenter.from_harmonics(OBP_Ylms).scale(dfactor[1]) + # input spherical harmonic datafiles to be removed from the GRACE data + # Remove sets of Ylms from the GRACE data before returning + remove_Ylms = GSM_Ylms.zeros_like() + remove_Ylms.time[:] = np.copy(GSM_Ylms.time) + remove_Ylms.month[:] = np.copy(GSM_Ylms.month) + if REMOVE_FILES: + # extend list if a single format was entered for all files + if len(REMOVE_FORMAT) < len(REMOVE_FILES): + REMOVE_FORMAT = REMOVE_FORMAT * len(REMOVE_FILES) + # for each file to be removed + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): + # ascii (.txt) + # netCDF4 (.nc) + # HDF5 (.H5) + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) + attributes['lineage'].append(Ylms.filename) + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): + # read from index file + _, removeform = REMOVEFORM.split('-') + # index containing files in data format + 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) + # remove the temporal mean of the coefficients + Ylms.mean(apply=True) + # distribute removed Ylms uniformly over the ocean + if REDISTRIBUTE_REMOVED: + # calculate ratio between total removed mass and + # a uniformly distributed cm of water over the ocean + ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0] + # for each spherical harmonic + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX + # remove the ratio*ocean Ylms from Ylms + # note: x -= y is equivalent to x = x - y + Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m] + Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m] + # filter removed coefficients + if DESTRIPE: + Ylms = Ylms.destripe() + # add data for month t and INDEX_FILE to the total + # remove_clm and remove_slm matrices + # redistributing the mass over the ocean if specified + remove_Ylms.add(Ylms) + # save geocenter coefficients of the auxiliary corrections + remove = gravtk.geocenter().from_harmonics(remove_Ylms) + # Calculating cos/sin of phi arrays # output [m,phi] - m = GSM_Ylms.m[:, np.newaxis] + m = GSM_Ylms.m # Integration factors (solid angle) - int_fact = np.sin(th)*dphi*dth - # Calculating cos(m*phi) and sin(m*phi) - ccos = np.cos(np.dot(m,phi)) - ssin = np.sin(np.dot(m,phi)) + int_fact = np.sin(th) * dphi * dth + # 4-pi normalization + 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)) # 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)) - for l in range(1,LMAX+1): - m = np.arange(0,np.min([l,MMAX])+1) - # convert to smoothed coefficients of mass - # Convolving plms with degree dependent factor and smoothing - plmout[l,m,:] = PLM[l,m,:]*dfactor[l]*wt[l] + 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, :] + ) # Initializing 3x3 I-Parameter matrix - IMAT = np.zeros((3,3)) - # Calculating I-Parameter matrix by integrating over latitudes + # (see equations 12 and 13 of Swenson et al., 2008) + IMAT = np.zeros((3, 3)) # I-Parameter matrix accounts for the fact that the GRACE data only # includes spherical harmonic degrees greater than or equal to 2 - for i in range(0,nlat): - # C10, C11, S11 - PC10 = P10[i]*ccos[0,:] - PC11 = P11[i]*ccos[1,:] - PS11 = P11[i]*ssin[1,:] - # C10: C10, C11, S11 (see equations 12 and 13 of Swenson et al., 2008) - IMAT[0,0] += np.sum(int_fact[i]*PC10*ocean_function[:,i]*PC10)/(4.0*np.pi) - IMAT[1,0] += np.sum(int_fact[i]*PC10*ocean_function[:,i]*PC11)/(4.0*np.pi) - IMAT[2,0] += np.sum(int_fact[i]*PC10*ocean_function[:,i]*PS11)/(4.0*np.pi) - # C11: C10, C11, S11 (see equations 12 and 13 of Swenson et al., 2008) - IMAT[0,1] += np.sum(int_fact[i]*PC11*ocean_function[:,i]*PC10)/(4.0*np.pi) - IMAT[1,1] += np.sum(int_fact[i]*PC11*ocean_function[:,i]*PC11)/(4.0*np.pi) - IMAT[2,1] += np.sum(int_fact[i]*PC11*ocean_function[:,i]*PS11)/(4.0*np.pi) - # S11: C10, C11, S11 (see equations 12 and 13 of Swenson et al., 2008) - IMAT[0,2] += np.sum(int_fact[i]*PS11*ocean_function[:,i]*PC10)/(4.0*np.pi) - IMAT[1,2] += np.sum(int_fact[i]*PS11*ocean_function[:,i]*PC11)/(4.0*np.pi) - IMAT[2,2] += np.sum(int_fact[i]*PS11*ocean_function[:,i]*PS11)/(4.0*np.pi) + # 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) + # 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 + ) + # 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 + ) + # 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 + ) # get seasonal variations of an initial geocenter correction # for use in the land water mass calculation @@ -688,81 +858,80 @@ 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)) - iteration.C11 = np.zeros((n_files,max_iter)) - iteration.S11 = np.zeros((n_files,max_iter)) + iteration.C10 = np.zeros((n_files, max_iter)) + iteration.C11 = np.zeros((n_files, max_iter)) + iteration.S11 = np.zeros((n_files, max_iter)) # calculate non-iterated terms for each file (G-matrix parameters) for t in range(n_files): # calculate geocenter component of ocean mass with GRACE - # allocate for product of grace and legendre polynomials - pcos = np.zeros((MMAX+1, nlat))#-[m,lat] - psin = np.zeros((MMAX+1, nlat))#-[m,lat] # Summing product of plms and c/slms over all SH degrees >= 2 - # Removing monthly GIA signal and atmospheric correction + # Removing monthly GIA signal, atmospheric correction + # and the auxiliary coefficients Ylms = GSM_Ylms.index(t) Ylms.subtract(GIA_Ylms.index(t)) Ylms.subtract(ATM_Ylms.index(t)) - for i in range(0, nlat): - l = np.arange(2,LMAX+1) - pcos[:,i] = np.sum(plmout[l,:,i]*Ylms.clm[l,:], axis=0) - psin[:,i] = np.sum(plmout[l,:,i]*Ylms.slm[l,:], axis=0) + 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, :] + ) # 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.dot(np.transpose(ccos),pcos) + np.dot(np.transpose(ssin),psin) + rmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # calculate G matrix parameters through a summation of each latitude - for i in range(0,nlat): - # C10, C11, S11 - PC10 = P10[i]*ccos[0,:] - PC11 = P11[i]*ccos[1,:] - PS11 = P11[i]*ssin[1,:] - # summation of integration factors, Legendre polynomials, - # (convolution of order and harmonics) and the ocean mass at t - G.C10[t] += np.sum(int_fact[i]*PC10*ocean_function[:,i]*rmass[:,i])/(4.0*np.pi) - G.C11[t] += np.sum(int_fact[i]*PC11*ocean_function[:,i]*rmass[:,i])/(4.0*np.pi) - G.S11[t] += np.sum(int_fact[i]*PS11*ocean_function[:,i]*rmass[:,i])/(4.0*np.pi) + # 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 + ) # 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] - # allocate for product of grace and legendre polynomials - pcos = np.zeros((MMAX+1, nlat))#-[m,lat] - psin = np.zeros((MMAX+1, nlat))#-[m,lat] # Summing product of plms and c/slms over all SH degrees - # Removing monthly GIA signal and atmospheric correction + # Removing monthly GIA signal, atmospheric correction + # and the auxiliary coefficients Ylms = GSM_Ylms.index(t) Ylms.subtract(GIA_Ylms.index(t)) Ylms.subtract(ATM_Ylms.index(t)) - for i in range(0, nlat): - # for land water: use an initial seasonal geocenter estimate - # from Chen et al. (1999) then the iterative if specified - l = np.arange(1,LMAX+1) - pcos[:,i] = np.sum(plmout[l,:,i]*Ylms.clm[l,:], axis=0) - psin[:,i] = np.sum(plmout[l,:,i]*Ylms.slm[l,:], axis=0) + 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, :] + ) # Multiplying by c/s(phi#m) to get surface density in cm w.e. (lonxlat) - # this will be a spatial field similar to outputs from stokes_combine.py # ccos/ssin are mXphi, pcos/psin are mXtheta: resultant matrices are phiXtheta # The summation over spherical harmonic order is in this multiplication - lmass = np.dot(np.transpose(ccos),pcos) + np.dot(np.transpose(ssin),psin) + lmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # use sea level fingerprints or eustatic from GRACE land components if FINGERPRINT: @@ -772,50 +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, ASTYPE=np.float64, SCALE=1e-32, - 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]]) @@ -831,40 +1032,70 @@ 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) - # save geocenter for iteration and time t after restoring GIA+ATM - iteration.C10[t,n_iter] = DMAT[0,t]/dfactor[1]+gia.C10[t]+atm.C10[t] - iteration.C11[t,n_iter] = DMAT[1,t]/dfactor[1]+gia.C11[t]+atm.C11[t] - iteration.S11[t,n_iter] = DMAT[2,t]/dfactor[1]+gia.S11[t]+atm.S11[t] + 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] + ) + # 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 # Convert inverted solutions into fully normalized spherical harmonics # restore geocenter variation from glacial isostatic adjustment (GIA) # restore atmospheric jump corrections from Fagiolini (2015) if applicable + # restore auxiliary removed spherical harmonics if applicable # 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[:] - DEG1.C11 = DMAT[1,:]/dfactor[1] + gia.C11[:] + atm.C11[:] - DEG1.S11 = DMAT[2,:]/dfactor[1] + gia.S11[:] + atm.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 @@ -876,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 @@ -904,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 @@ -950,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) @@ -967,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) @@ -980,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) @@ -996,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) @@ -1009,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) @@ -1022,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 @@ -1037,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) @@ -1074,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 @@ -1089,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 @@ -1133,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 @@ -1200,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)) @@ -1290,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 @@ -1345,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 @@ -1368,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 @@ -1387,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( @@ -1394,62 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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' @@ -1465,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') @@ -1508,78 +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', + ) + 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', + ) # 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] @@ -1589,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, @@ -1617,6 +2186,9 @@ def main(): MODEL_INDEX=args.ocean_file, MEAN_FILE=args.mean_file, MEANFORM=args.mean_format, + REMOVE_FILES=args.remove_file, + REMOVE_FORMAT=args.remove_format, + REDISTRIBUTE_REMOVED=args.redistribute_removed, ITERATIVE=args.iterative, SOLVER=args.solver, FINGERPRINT=args.fingerprint, @@ -1624,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 992df24d..8a0961df 100644 --- a/geocenter/geocenter_compare_tellus.py +++ b/geocenter/geocenter_compare_tellus.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" geocenter_compare_tellus.py Written by Tyler Sutterley (01/2025) Plots the GRACE/GRACE-FO geocenter time series for different @@ -27,6 +27,7 @@ Updated 11/2021: use gravity_toolkit geocenter class for operations Written 05/2021 """ + from __future__ import print_function import pathlib @@ -34,175 +35,212 @@ import warnings import numpy as np import gravity_toolkit as gravtk + # attempt imports try: import matplotlib import matplotlib.font_manager import matplotlib.pyplot as plt import matplotlib.offsetbox + # rebuilt the matplotlib fonts and set parameters matplotlib.font_manager._load_fontmanager() matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) + # PURPOSE: plots the GRACE/GRACE-FO geocenter time series -def geocenter_compare_tellus(grace_dir,DREL,START_MON,END_MON,MISSING): +def geocenter_compare_tellus(grace_dir, DREL, START_MON, END_MON, MISSING): # GRACE months - GAP = [187,188,189,190,191,192,193,194,195,196,197] - months = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] + months = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) # labels for each scenario - input_flags = ['','iter','SLF_iter','SLF_iter_wSLR21'] - input_labels = ['Static','Iterated','Iterated SLF'] + input_flags = ['', 'iter', 'SLF_iter', 'SLF_iter_wSLR21'] + input_labels = ['Static', 'Iterated', 'Iterated SLF'] # labels for Release-6 - PROC = ['CSR','GFZ','JPL'] - model_str = 'OMCT' if DREL in ('RL04','RL05') else 'MPIOM' + PROC = ['CSR', 'GFZ', 'JPL'] + model_str = 'OMCT' if DREL in ('RL04', 'RL05') else 'MPIOM' # degree one coefficient labels - fig_labels = ['C11','S11','C10'] - axes_labels = dict(C10='c)',C11='a)',S11='b)') - ylabels = dict(C10='z',C11='x',S11='y') + fig_labels = ['C11', 'S11', 'C10'] + axes_labels = dict(C10='c)', C11='a)', S11='b)') + ylabels = dict(C10='z', C11='x', S11='y') # plot colors for each dataset - plot_colors = {'Iterated SLF':'darkorchid','GFZ GravIS':'darkorange', - 'JPL Tellus':'mediumseagreen'} + plot_colors = { + 'Iterated SLF': 'darkorchid', + 'GFZ GravIS': 'darkorange', + 'JPL Tellus': 'mediumseagreen', + } # plot geocenter estimates for each processing center - for k,pr in enumerate(PROC): + for k, pr in enumerate(PROC): # 3 row plot (C10, C11 and S11) ax = {} - fig,(ax[0],ax[1],ax[2])=plt.subplots(num=1,ncols=3, - sharey=True,figsize=(9,4)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, ncols=3, sharey=True, figsize=(9, 4) + ) # additionally plot GFZ with SLR replaced pole tide - if (pr == 'GFZwPT'): - fargs = ('GFZ',DREL,model_str,input_flags[3]) + if pr == 'GFZwPT': + fargs = ('GFZ', DREL, model_str, input_flags[3]) else: - fargs = (pr,DREL,model_str,input_flags[2]) + fargs = (pr, DREL, model_str, input_flags[2]) # read geocenter file for processing center and model grace_file = '{0}_{1}_{2}_{3}.txt'.format(*fargs) DEG1 = gravtk.geocenter().from_UCI(grace_dir.joinpath(grace_file)) # indices for mean months - kk, = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) + (kk,) = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) DEG1.mean(apply=True, indices=kk) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # plot model outputs # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) val = getattr(DEG1, ylabels[key].upper()) - for i,m in enumerate(months): + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] data[i] = val[mm] # plot all dates - ax[j].plot(tdec, data, color=plot_colors['Iterated SLF'], - label='Iterated SLF') + ax[j].plot( + tdec, + data, + color=plot_colors['Iterated SLF'], + label='Iterated SLF', + ) - if (pr == 'GFZwPT'): + if pr == 'GFZwPT': grace_file = 'GRAVIS-2B_GFZOP_GEOCENTER_0002.dat' - DEG1 = gravtk.geocenter().from_gravis(grace_dir.joinpath(grace_file)) + DEG1 = gravtk.geocenter().from_gravis( + grace_dir.joinpath(grace_file) + ) # indices for mean months - kk, = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) + (kk,) = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) DEG1.mean(apply=True, indices=kk) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # plot model outputs val = getattr(DEG1, ylabels[key].upper()) val -= val[kk].mean() # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) - for i,m in enumerate(months): + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] data[i] = val[mm] # plot all dates - ax[j].plot(tdec, data, color=plot_colors['GFZ GravIS'], - label='GFZ GravIS') + ax[j].plot( + tdec, + data, + color=plot_colors['GFZ GravIS'], + label='GFZ GravIS', + ) # Running function read_tellus_geocenter.py grace_file = f'TN-13_GEOC_{pr}_{DREL}.txt' - DEG1 = gravtk.geocenter().from_tellus(grace_dir.joinpath(grace_file), - JPL=True) + DEG1 = gravtk.geocenter().from_tellus( + grace_dir.joinpath(grace_file), JPL=True + ) # indices for mean months - kk, = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) + (kk,) = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) DEG1.mean(apply=True, indices=kk) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # plot model outputs # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) val = getattr(DEG1, ylabels[key].upper()) - for i,m in enumerate(months): + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] data[i] = val[mm] # plot all dates - ax[j].plot(tdec, data, color=plot_colors['JPL Tellus'], - label='JPL Tellus') + ax[j].plot( + tdec, data, color=plot_colors['JPL Tellus'], label='JPL Tellus' + ) # add axis labels and adjust font sizes for axis ticks - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9,day=3,hour=12,minute=12) - ax[j].axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax[j].axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(8, 4)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(DEG1.month == 186) - kk, = np.flatnonzero(DEG1.month == 198) - vs = ax[j].axvspan(DEG1.time[jj],DEG1.time[kk], - color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (4,2) + (jj,) = np.flatnonzero(DEG1.month == 186) + (kk,) = np.flatnonzero(DEG1.month == 198) + vs = ax[j].axvspan( + DEG1.time[jj], + DEG1.time[kk], + color='0.5', + ls='dashed', + alpha=0.15, + ) + vs._dashes = (4, 2) # axis label ax[j].set_title(ylabels[key], style='italic', fontsize=14) - artist = matplotlib.offsetbox.AnchoredText(axes_labels[key], pad=0., - prop=dict(size=16, weight='bold'), frameon=False, loc=2) + artist = matplotlib.offsetbox.AnchoredText( + axes_labels[key], + pad=0.0, + prop=dict(size=16, weight='bold'), + frameon=False, + loc=2, + ) ax[j].add_artist(artist) ax[j].set_xlabel('Time [Yr]', fontsize=14) # set ticks - xmin = 2002 + (START_MON + 1.0)//12.0 - xmax = 2002 + (END_MON + 1.0)/12.0 + xmin = 2002 + (START_MON + 1.0) // 12.0 + xmax = 2002 + (END_MON + 1.0) / 12.0 major_ticks = np.arange(2005, xmax, 5) ax[j].xaxis.set_ticks(major_ticks) - minor_ticks = sorted(set(np.arange(xmin, xmax, 1)) - set(major_ticks)) + minor_ticks = sorted( + set(np.arange(xmin, xmax, 1)) - set(major_ticks) + ) ax[j].xaxis.set_ticks(minor_ticks, minor=True) ax[j].set_xlim(xmin, xmax) - ax[j].set_ylim(-9.5,8.5) + ax[j].set_ylim(-9.5, 8.5) # axes tick adjustments - ax[j].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[j].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # add legend - lgd = ax[0].legend(loc=3,frameon=False) + lgd = ax[0].legend(loc=3, frameon=False) lgd.get_frame().set_alpha(1.0) for line in lgd.get_lines(): line.set_linewidth(6) - for i,text in enumerate(lgd.get_texts()): + for i, text in enumerate(lgd.get_texts()): text.set_weight('bold') text.set_color(plot_colors[text.get_text()]) # labels and set limits ax[0].set_ylabel('Geocenter Variation [mm]', fontsize=14) # adjust locations of subplots - fig.subplots_adjust(left=0.06,right=0.98,bottom=0.12,top=0.94,wspace=0.05) + fig.subplots_adjust( + left=0.06, right=0.98, bottom=0.12, top=0.94, wspace=0.05 + ) # save figure to file OUTPUT_FIGURE = f'TN13_SV19_{pr}_{DREL}.pdf' plt.savefig(grace_dir.joinpath(OUTPUT_FIGURE), format='pdf', dpi=300) plt.clf() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -212,38 +250,86 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 925627a1..c5721c0e 100644 --- a/geocenter/geocenter_monte_carlo.py +++ b/geocenter/geocenter_monte_carlo.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" geocenter_monte_carlo.py Written by Tyler Sutterley (01/2025) @@ -24,6 +24,7 @@ Updated 12/2021: adjust minimum x limit based on starting GRACE month Written 11/2021 """ + from __future__ import print_function import pathlib @@ -39,22 +40,24 @@ import matplotlib.pyplot as plt import matplotlib.cm as cm import matplotlib.offsetbox + # rebuilt the matplotlib fonts and set parameters matplotlib.font_manager._load_fontmanager() matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) + # PURPOSE: plots the GRACE/GRACE-FO geocenter time series -def geocenter_monte_carlo(grace_dir,PROC,DREL,START_MON,END_MON,MISSING): +def geocenter_monte_carlo(grace_dir, PROC, DREL, START_MON, END_MON, MISSING): # GRACE months - GAP = [187,188,189,190,191,192,193,194,195,196,197] - months = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] + months = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) nmon = len(months) # labels for Release-6 - model_str = 'OMCT' if DREL in ('RL04','RL05') else 'MPIOM' + model_str = 'OMCT' if DREL in ('RL04', 'RL05') else 'MPIOM' # GIA and processing labels input_flag = 'SLF' gia_str = '_AW13_ice6g_GA' @@ -62,38 +65,40 @@ def geocenter_monte_carlo(grace_dir,PROC,DREL,START_MON,END_MON,MISSING): ds_str = '_FL' # degree one coefficient labels - fig_labels = ['C11','S11','C10'] - axes_labels = dict(C10='c)',C11='a)',S11='b)') - ylabels = dict(C10='z',C11='x',S11='y') + fig_labels = ['C11', 'S11', 'C10'] + axes_labels = dict(C10='c)', C11='a)', S11='b)') + ylabels = dict(C10='z', C11='x', S11='y') # 3 row plot (C10, C11 and S11) ax = {} - fig,(ax[0],ax[1],ax[2])=plt.subplots(num=1,ncols=3,sharey=True,figsize=(9,4)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, ncols=3, sharey=True, figsize=(9, 4) + ) # read geocenter file for processing center and model - fargs = (PROC,DREL,model_str,input_flag,gia_str,delta_str,ds_str) + fargs = (PROC, DREL, model_str, input_flag, gia_str, delta_str, ds_str) grace_file = '{0}_{1}_{2}_{3}{4}{5}{6}.nc'.format(*fargs) DEG1 = gravtk.geocenter().from_netCDF4(grace_dir.joinpath(grace_file)) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # number of monte carlo runs - _,nruns = np.shape(DEG1.C10) + _, nruns = np.shape(DEG1.C10) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # create a time series with nans for missing months - tdec = np.full((nmon),np.nan,dtype=np.float64) - data = np.full((nmon,nruns),np.nan,dtype=np.float64) + tdec = np.full((nmon), np.nan, dtype=np.float64) + data = np.full((nmon, nruns), np.nan, dtype=np.float64) val = getattr(DEG1, ylabels[key].upper()) - for i,m in enumerate(months): + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] - data[i,:] = val[mm,:] + data[i, :] = val[mm, :] # show solutions for each iteration - plot_colors = iter(cm.rainbow(np.linspace(0,1,nruns))) + plot_colors = iter(cm.rainbow(np.linspace(0, 1, nruns))) # mean of all monte carlo solutions MEAN = np.mean(data, axis=1) nvalid = np.count_nonzero(np.isfinite(MEAN)) @@ -103,58 +108,71 @@ def geocenter_monte_carlo(grace_dir,PROC,DREL,START_MON,END_MON,MISSING): for k in range(nruns): color_k = next(plot_colors) # plot all dates - ax[j].plot(tdec, data[:,k], color=color_k) + ax[j].plot(tdec, data[:, k], color=color_k) # variance off of the mean - variance[k] = np.nansum((data[:,k] - MEAN)**2)/nvalid - if (np.nanmax(np.abs(data[:,k] - MEAN)) > max_var): - max_var = np.nanmax(np.abs(data[:,k] - MEAN)) + variance[k] = np.nansum((data[:, k] - MEAN) ** 2) / nvalid + if np.nanmax(np.abs(data[:, k] - MEAN)) > max_var: + max_var = np.nanmax(np.abs(data[:, k] - MEAN)) # add mean solution ax[j].plot(tdec, MEAN, color='k', lw=1) # calculate total RMS - RMS = np.nansum(np.sqrt(variance))/nruns + RMS = np.nansum(np.sqrt(variance)) / nruns # add axis labels and adjust font sizes for axis ticks # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9,day=3,hour=12,minute=12) - ax[j].axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax[j].axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(8, 4)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(DEG1.month == 186) - kk, = np.flatnonzero(DEG1.month == 198) - vs = ax[j].axvspan(DEG1.time[jj],DEG1.time[kk], - color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (4,2) + (jj,) = np.flatnonzero(DEG1.month == 186) + (kk,) = np.flatnonzero(DEG1.month == 198) + vs = ax[j].axvspan( + DEG1.time[jj], DEG1.time[kk], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (4, 2) # axis label ax[j].set_title(ylabels[key], style='italic', fontsize=14) - artist = matplotlib.offsetbox.AnchoredText(axes_labels[key], pad=0., - prop=dict(size=16,weight='bold'), frameon=False, loc=2) + artist = matplotlib.offsetbox.AnchoredText( + axes_labels[key], + pad=0.0, + prop=dict(size=16, weight='bold'), + frameon=False, + loc=2, + ) ax[j].add_artist(artist) - lbl = f'$\sigma$ = {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) + lbl = r'$\sigma$' + f' = {RMS:0.2f} mm\nmax = {max_var:0.2f} mm' + 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,42 +181,100 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 cbbeb7b6..c4bcc71d 100644 --- a/geocenter/geocenter_ocean_models.py +++ b/geocenter/geocenter_ocean_models.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" geocenter_ocean_models.py Written by Tyler Sutterley (01/2025) Plots the GRACE/GRACE-FO geocenter time series comparing results @@ -34,6 +34,7 @@ Updated 11/2019: adjust axes and set directory to full path Updated 09/2019: for public release of time series to references page """ + from __future__ import print_function import pathlib @@ -48,107 +49,133 @@ import matplotlib.font_manager import matplotlib.pyplot as plt import matplotlib.offsetbox + # rebuilt the matplotlib fonts and set parameters matplotlib.font_manager._load_fontmanager() matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) + # PURPOSE: plots the GRACE/GRACE-FO geocenter time series # comparing results using different ocean bottom pressure estimates -def geocenter_ocean_models(grace_dir,PROC,DREL,MODEL,START_MON,END_MON,MISSING): +def geocenter_ocean_models( + grace_dir, PROC, DREL, MODEL, START_MON, END_MON, MISSING +): # GRACE months - GAP = [187,188,189,190,191,192,193,194,195,196,197] - months = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] + months = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) # labels for each scenario - input_flags = ['','iter','SLF_iter'] - input_labels = ['Static','Iterated','Iterated SLF'] + input_flags = ['', 'iter', 'SLF_iter'] + input_labels = ['Static', 'Iterated', 'Iterated SLF'] # degree one coefficient labels - fig_labels = ['C11','S11','C10'] - axes_labels = dict(C10='c)',C11='a)',S11='b)') - ylabels = dict(C10='z',C11='x',S11='y') + fig_labels = ['C11', 'S11', 'C10'] + axes_labels = dict(C10='c)', C11='a)', S11='b)') + ylabels = dict(C10='z', C11='x', S11='y') # list of plot colors - plot_colors = ['darkorange','darkorchid','mediumseagreen','dodgerblue','0.4'] + plot_colors = [ + 'darkorange', + 'darkorchid', + 'mediumseagreen', + 'dodgerblue', + '0.4', + ] # 3 row plot (C10, C11 and S11) ax = {} - fig,(ax[0],ax[1],ax[2])=plt.subplots(num=1,ncols=3,sharey=True,figsize=(9,4)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, ncols=3, sharey=True, figsize=(9, 4) + ) # plot geocenter estimates for each processing center - for k,mdl in enumerate(MODEL): + for k, mdl in enumerate(MODEL): # read geocenter file for processing center and model - grace_file = '{0}_{1}_{2}_{3}.txt'.format(PROC,DREL,mdl,input_flags[2]) + grace_file = '{0}_{1}_{2}_{3}.txt'.format( + PROC, DREL, mdl, input_flags[2] + ) DEG1 = gravtk.geocenter().from_UCI(grace_dir.joinpath(grace_file)) # indices for mean months - kk, = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) + (kk,) = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) DEG1.mean(apply=True, indices=kk) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) val = getattr(DEG1, ylabels[key].upper()) - for i,m in enumerate(months): + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] data[i] = val[mm] # plot all dates - label = mdl.replace('_','-') + label = mdl.replace('_', '-') ax[j].plot(tdec, data, color=plot_colors[k], label=label) # read geocenter file for processing center and model - model_str = 'OMCT' if DREL in ('RL04','RL05') else 'MPIOM' - grace_file = '{0}_{1}_{2}_{3}.txt'.format(PROC,DREL,model_str,input_flags[2]) + model_str = 'OMCT' if DREL in ('RL04', 'RL05') else 'MPIOM' + grace_file = '{0}_{1}_{2}_{3}.txt'.format( + PROC, DREL, model_str, input_flags[2] + ) DEG1 = gravtk.geocenter().from_UCI(grace_dir.joinpath(grace_file)) # add axis labels and adjust font sizes for axis ticks - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(DEG1.month == 186) - kk, = np.flatnonzero(DEG1.month == 198) - ax[j].axvspan(DEG1.time[jj],DEG1.time[kk], - color='0.5',ls='dashed',alpha=0.15) + (jj,) = np.flatnonzero(DEG1.month == 186) + (kk,) = np.flatnonzero(DEG1.month == 198) + ax[j].axvspan( + DEG1.time[jj], DEG1.time[kk], color='0.5', ls='dashed', alpha=0.15 + ) # axis label ax[j].set_title(ylabels[key], style='italic', fontsize=14) - artist = matplotlib.offsetbox.AnchoredText(axes_labels[key], pad=0., - prop=dict(size=16,weight='bold'), frameon=False, loc=2) + artist = matplotlib.offsetbox.AnchoredText( + axes_labels[key], + pad=0.0, + prop=dict(size=16, weight='bold'), + frameon=False, + loc=2, + ) ax[j].add_artist(artist) ax[j].set_xlabel('Time [Yr]', fontsize=14) # set ticks - xmin = 2002 + (START_MON + 1.0)//12.0 - xmax = 2002 + (END_MON + 1.0)/12.0 + xmin = 2002 + (START_MON + 1.0) // 12.0 + xmax = 2002 + (END_MON + 1.0) / 12.0 major_ticks = np.arange(2005, xmax, 5) ax[j].xaxis.set_ticks(major_ticks) minor_ticks = sorted(set(np.arange(xmin, xmax, 1)) - set(major_ticks)) ax[j].xaxis.set_ticks(minor_ticks, minor=True) ax[j].set_xlim(xmin, xmax) - ax[j].set_ylim(-9.5,8.5) + ax[j].set_ylim(-9.5, 8.5) # axes tick adjustments - ax[j].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[j].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # add legend - lgd = ax[0].legend(loc=3,frameon=False) + lgd = ax[0].legend(loc=3, frameon=False) lgd.get_frame().set_alpha(1.0) for line in lgd.get_lines(): line.set_linewidth(6) - for i,text in enumerate(lgd.get_texts()): + for i, text in enumerate(lgd.get_texts()): text.set_weight('bold') text.set_color(plot_colors[i]) # labels and set limits ax[0].set_ylabel('Geocenter Variation [mm]', fontsize=14) # adjust locations of subplots - fig.subplots_adjust(left=0.06,right=0.98,bottom=0.12,top=0.94,wspace=0.05) + fig.subplots_adjust( + left=0.06, right=0.98, bottom=0.12, top=0.94, wspace=0.05 + ) # save figure to file OUTPUT_FIGURE = f'SV19_{PROC}_{DREL}_ocean_models.pdf' plt.savefig(grace_dir.joinpath(OUTPUT_FIGURE), format='pdf', dpi=300) plt.clf() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -157,47 +184,111 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 252c9d0d..950acdce 100644 --- a/geocenter/geocenter_processing_centers.py +++ b/geocenter/geocenter_processing_centers.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" geocenter_processing_centers.py Written by Tyler Sutterley (01/2025) Plots the GRACE/GRACE-FO geocenter time series for different @@ -35,6 +35,7 @@ Updated 11/2019: adjust axes and set directory to full path Updated 09/2019: for public release of time series to references page """ + from __future__ import print_function import pathlib @@ -49,117 +50,141 @@ import matplotlib.font_manager import matplotlib.pyplot as plt import matplotlib.offsetbox + # rebuilt the matplotlib fonts and set parameters matplotlib.font_manager._load_fontmanager() matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except (AttributeError, ImportError, ModuleNotFoundError) as exc: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) + # PURPOSE: plots the GRACE/GRACE-FO geocenter time series -def geocenter_processing_centers(grace_dir,PROC,DREL,START_MON,END_MON,MISSING): +def geocenter_processing_centers( + grace_dir, PROC, DREL, START_MON, END_MON, MISSING +): # GRACE months - GAP = [187,188,189,190,191,192,193,194,195,196,197] - months = sorted(set(np.arange(START_MON,END_MON+1)) - set(MISSING)) + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] + months = sorted(set(np.arange(START_MON, END_MON + 1)) - set(MISSING)) # labels for each scenario - input_flags = ['','iter','SLF_iter','SLF_iter_wSLR21','SLF_iter_wSLR21_wSLR22'] - input_labels = ['Static','Iterated','Iterated SLF'] + input_flags = [ + '', + 'iter', + 'SLF_iter', + 'SLF_iter_wSLR21', + 'SLF_iter_wSLR21_wSLR22', + ] + input_labels = ['Static', 'Iterated', 'Iterated SLF'] # labels for Release-6 - model_str = 'OMCT' if DREL in ('RL04','RL05') else 'MPIOM' + model_str = 'OMCT' if DREL in ('RL04', 'RL05') else 'MPIOM' # degree one coefficient labels - fig_labels = ['C11','S11','C10'] - axes_labels = dict(C10='c)',C11='a)',S11='b)') - ylabels = dict(C10='z',C11='x',S11='y') + fig_labels = ['C11', 'S11', 'C10'] + axes_labels = dict(C10='c)', C11='a)', S11='b)') + ylabels = dict(C10='z', C11='x', S11='y') # plot colors for each dataset - plot_colors = dict(CSR='darkorange',GFZ='darkorchid',JPL='mediumseagreen') + plot_colors = dict(CSR='darkorange', GFZ='darkorchid', JPL='mediumseagreen') plot_colors['GFZwPT'] = 'dodgerblue' plot_colors['GFZ+CS21'] = 'darkorchid' plot_colors['GFZ+CS21+CS22'] = 'darkorchid' # 3 row plot (C10, C11 and S11) ax = {} - fig,(ax[0],ax[1],ax[2])=plt.subplots(num=1,ncols=3,sharey=True,figsize=(9,4)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, ncols=3, sharey=True, figsize=(9, 4) + ) # plot geocenter estimates for each processing center - for k,pr in enumerate(PROC): + for k, pr in enumerate(PROC): # additionally plot GFZ with SLR replaced pole tide - if pr in ('GFZwPT','GFZ+CS21'): - fargs = ('GFZ',DREL,model_str,input_flags[3]) - elif (pr == 'GFZ+CS21+CS22'): - fargs = ('GFZ',DREL,model_str,input_flags[4]) + if pr in ('GFZwPT', 'GFZ+CS21'): + fargs = ('GFZ', DREL, model_str, input_flags[3]) + elif pr == 'GFZ+CS21+CS22': + fargs = ('GFZ', DREL, model_str, input_flags[4]) else: - fargs = (pr,DREL,model_str,input_flags[2]) + fargs = (pr, DREL, model_str, input_flags[2]) # read geocenter file for processing center and model grace_file = '{0}_{1}_{2}_{3}.txt'.format(*fargs) DEG1 = gravtk.geocenter().from_UCI(grace_dir.joinpath(grace_file)) # indices for mean months - kk, = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) + (kk,) = np.nonzero((DEG1.month >= START_MON) & (DEG1.month <= 176)) DEG1.mean(apply=True, indices=kk) # setting Load Love Number (kl) to 0.021 to match Swenson et al. (2008) DEG1.to_cartesian(kl=0.021) # plot each coefficient - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) val = getattr(DEG1, ylabels[key].upper()) - for i,m in enumerate(months): + for i, m in enumerate(months): valid = np.count_nonzero(DEG1.month == m) if valid: - mm, = np.nonzero(DEG1.month == m) + (mm,) = np.nonzero(DEG1.month == m) tdec[i] = DEG1.time[mm] data[i] = val[mm] # plot all dates ax[j].plot(tdec, data, color=plot_colors[pr], label=pr) # add axis labels and adjust font sizes for axis ticks - for j,key in enumerate(fig_labels): + for j, key in enumerate(fig_labels): # vertical line denoting the accelerometer shutoff - acc = gravtk.time.convert_calendar_decimal(2016,9,day=3,hour=12,minute=12) - ax[j].axvline(acc,color='0.5',ls='dashed',lw=0.5,dashes=(8,4)) + acc = gravtk.time.convert_calendar_decimal( + 2016, 9, day=3, hour=12, minute=12 + ) + ax[j].axvline(acc, color='0.5', ls='dashed', lw=0.5, dashes=(8, 4)) # vertical lines for end of the GRACE mission and start of GRACE-FO - jj, = np.flatnonzero(DEG1.month == 186) - kk, = np.flatnonzero(DEG1.month == 198) - vs = ax[j].axvspan(DEG1.time[jj],DEG1.time[kk], - color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (4,2) + (jj,) = np.flatnonzero(DEG1.month == 186) + (kk,) = np.flatnonzero(DEG1.month == 198) + vs = ax[j].axvspan( + DEG1.time[jj], DEG1.time[kk], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (4, 2) # axis label ax[j].set_title(ylabels[key], style='italic', fontsize=14) - artist = matplotlib.offsetbox.AnchoredText(axes_labels[key], pad=0., - prop=dict(size=16,weight='bold'), frameon=False, loc=2) + artist = matplotlib.offsetbox.AnchoredText( + axes_labels[key], + pad=0.0, + prop=dict(size=16, weight='bold'), + frameon=False, + loc=2, + ) ax[j].add_artist(artist) ax[j].set_xlabel('Time [Yr]', fontsize=14) # set ticks - xmin = 2002 + (START_MON + 1.0)//12.0 - xmax = 2002 + (END_MON + 1.0)/12.0 + xmin = 2002 + (START_MON + 1.0) // 12.0 + xmax = 2002 + (END_MON + 1.0) / 12.0 major_ticks = np.arange(2005, xmax, 5) ax[j].xaxis.set_ticks(major_ticks) minor_ticks = sorted(set(np.arange(xmin, xmax, 1)) - set(major_ticks)) ax[j].xaxis.set_ticks(minor_ticks, minor=True) ax[j].set_xlim(xmin, xmax) - ax[j].set_ylim(-9.5,8.5) + ax[j].set_ylim(-9.5, 8.5) # axes tick adjustments - ax[j].tick_params(axis='both', which='both', - labelsize=14, direction='in') + ax[j].tick_params( + axis='both', which='both', labelsize=14, direction='in' + ) # add legend - lgd = ax[0].legend(loc=3,frameon=False) + lgd = ax[0].legend(loc=3, frameon=False) lgd.get_frame().set_alpha(1.0) for line in lgd.get_lines(): line.set_linewidth(6) - for i,text in enumerate(lgd.get_texts()): + for i, text in enumerate(lgd.get_texts()): text.set_weight('bold') text.set_color(plot_colors[text.get_text()]) # labels and set limits ax[0].set_ylabel('Geocenter Variation [mm]', fontsize=14) # adjust locations of subplots - fig.subplots_adjust(left=0.06,right=0.98,bottom=0.12,top=0.94,wspace=0.05) + fig.subplots_adjust( + left=0.06, right=0.98, bottom=0.12, top=0.94, wspace=0.05 + ) # save figure to file OUTPUT_FIGURE = f'SV19_{DREL}_centers.pdf' plt.savefig(grace_dir.joinpath(OUTPUT_FIGURE), format='pdf', dpi=300) plt.clf() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -168,43 +193,102 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 7ecb2b6f..b87f2f22 100644 --- a/geocenter/monte_carlo_degree_one.py +++ b/geocenter/monte_carlo_degree_one.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" monte_carlo_degree_one.py -Written by Tyler Sutterley (01/2025) +Written by Tyler Sutterley (07/2026) Calculates degree 1 errors using GRACE coefficients of degree 2 and greater, and ocean bottom pressure variations from OMCT/MPIOM in a Monte Carlo scheme @@ -157,6 +157,9 @@ https://doi.org/10.1029/2005GL025305 UPDATE HISTORY: + Updated 07/2026: use np.einsum for spherical harmonic summations + can remove sets of harmonic files from the GRACE/GRACE-FO data + use np.radians to convert from degrees to radians Updated 01/2025: fixed deprecated tick label resizing Updated 06/2024: use wrapper to importlib for optional dependencies Updated 10/2023: generalize mission variable to be GRACE/GRACE-FO @@ -208,6 +211,7 @@ output all monte carlo iterations to a single netCDF4 file Written 11/2018 """ + from __future__ import print_function import sys @@ -230,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) @@ -239,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 @@ -263,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 + APx*np.pi/180.0) + \ - SAAx*np.sin(4.0*np.pi*grace_date + SAPx*np.pi/180.0) - Y = AAy*np.sin(2.0*np.pi*grace_date + APy*np.pi/180.0) + \ - SAAy*np.sin(4.0*np.pi*grace_date + SAPy*np.pi/180.0) - Z = AAz*np.sin(2.0*np.pi*grace_date + APz*np.pi/180.0) + \ - SAAz*np.sin(4.0*np.pi*grace_date + SAPz*np.pi/180.0) - 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, @@ -295,14 +310,17 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, DATAFORM=None, MEAN_FILE=None, MEANFORM=None, + REMOVE_FILES=None, + REMOVE_FORMAT=None, + REDISTRIBUTE_REMOVED=False, ERROR_FILES=[], SOLVER=None, FINGERPRINT=False, EXPANSION=None, LANDMASK=None, PLOT=False, - MODE=0o775): - + MODE=0o775, +): # GRACE/GRACE-FO dataset DSET = 'GSM' # do not import degree 1 coefficients @@ -318,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 @@ -330,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') @@ -364,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 @@ -384,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') @@ -397,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 = dlon*np.pi/180.0 - dth = dlat*np.pi/180.0 + dphi = np.radians(dlon) + dth = np.radians(dlat) # longitude and colatitude in radians - phi = landsea.lon[np.newaxis,:]*np.pi/180.0 - th = (90.0 - np.squeeze(landsea.lat))*np.pi/180.0 + 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 @@ -424,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 @@ -451,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) @@ -495,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 @@ -505,6 +549,59 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # save geocenter coefficients of the atmospheric jump corrections atm = gravtk.geocenter().from_harmonics(ATM_Ylms) + # input spherical harmonic datafiles to be removed from the GRACE data + # Remove sets of Ylms from the GRACE data before returning + remove_Ylms = GSM_Ylms.zeros_like() + remove_Ylms.time[:] = np.copy(GSM_Ylms.time) + remove_Ylms.month[:] = np.copy(GSM_Ylms.month) + if REMOVE_FILES: + # extend list if a single format was entered for all files + if len(REMOVE_FORMAT) < len(REMOVE_FILES): + REMOVE_FORMAT = REMOVE_FORMAT * len(REMOVE_FILES) + # for each file to be removed + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): + # ascii (.txt) + # netCDF4 (.nc) + # HDF5 (.H5) + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) + attributes['lineage'].append(Ylms.filename) + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): + # read from index file + _, removeform = REMOVEFORM.split('-') + # index containing files in data format + 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) + # remove the temporal mean of the coefficients + Ylms.mean(apply=True) + # distribute removed Ylms uniformly over the ocean + if REDISTRIBUTE_REMOVED: + # calculate ratio between total removed mass and + # a uniformly distributed cm of water over the ocean + ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0] + # for each spherical harmonic + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX + # remove the ratio*ocean Ylms from Ylms + # note: x -= y is equivalent to x = x - y + Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m] + Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m] + # filter removed coefficients + if DESTRIPE: + Ylms = Ylms.destripe() + # add data for month t and INDEX_FILE to the total + # remove_clm and remove_slm matrices + # redistributing the mass over the ocean if specified + remove_Ylms.add(Ylms) + # save geocenter coefficients of the auxiliary corrections + remove = gravtk.geocenter().from_harmonics(remove_Ylms) + # input spherical harmonic datafiles to be used in monte carlo error_Ylms = [] # for each file to be removed @@ -516,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 @@ -529,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 = {} @@ -571,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) @@ -580,47 +687,65 @@ def monte_carlo_degree_one(base_dir, PROC, DREL, LMAX, RAD, # Calculating cos/sin of phi arrays # output [m,phi] - m = GSM_Ylms.m[:, np.newaxis] + m = GSM_Ylms.m # Integration factors (solid angle) - int_fact = np.sin(th)*dphi*dth - # Calculating cos(m*phi) and sin(m*phi) - ccos = np.cos(np.dot(m,phi)) - ssin = np.sin(np.dot(m,phi)) + int_fact = np.sin(th) * dphi * dth + # 4-pi normalization + 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)) # Legendre polynomials for degree 1 - P10 = np.squeeze(PLM[1,0,:]) - P11 = np.squeeze(PLM[1,1,:]) - # PLM for spherical harmonic degrees 2+ + 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)) - for l in range(1,LMAX+1): - m = np.arange(0,np.min([l,MMAX])+1) - # convert to smoothed coefficients of mass - # Convolving plms with degree dependent factor and smoothing - plmout[l,m,:] = PLM[l,m,:]*dfactor[l]*wt[l] + 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, :] + ) # Initializing 3x3 I-Parameter matrix - IMAT = np.zeros((3,3)) - # Calculating I-Parameter matrix by integrating over latitudes + # (see equations 12 and 13 of Swenson et al., 2008) + IMAT = np.zeros((3, 3)) # I-Parameter matrix accounts for the fact that the GRACE data only # includes spherical harmonic degrees greater than or equal to 2 - for i in range(0,nlat): - # C10, C11, S11 - PC10 = P10[i]*ccos[0,:] - PC11 = P11[i]*ccos[1,:] - PS11 = P11[i]*ssin[1,:] - # C10: C10, C11, S11 (see equations 12 and 13 of Swenson et al., 2008) - IMAT[0,0] += np.sum(int_fact[i]*PC10*ocean_function[:,i]*PC10)/(4.0*np.pi) - IMAT[1,0] += np.sum(int_fact[i]*PC10*ocean_function[:,i]*PC11)/(4.0*np.pi) - IMAT[2,0] += np.sum(int_fact[i]*PC10*ocean_function[:,i]*PS11)/(4.0*np.pi) - # C11: C10, C11, S11 (see equations 12 and 13 of Swenson et al., 2008) - IMAT[0,1] += np.sum(int_fact[i]*PC11*ocean_function[:,i]*PC10)/(4.0*np.pi) - IMAT[1,1] += np.sum(int_fact[i]*PC11*ocean_function[:,i]*PC11)/(4.0*np.pi) - IMAT[2,1] += np.sum(int_fact[i]*PC11*ocean_function[:,i]*PS11)/(4.0*np.pi) - # S11: C10, C11, S11 (see equations 12 and 13 of Swenson et al., 2008) - IMAT[0,2] += np.sum(int_fact[i]*PS11*ocean_function[:,i]*PC10)/(4.0*np.pi) - IMAT[1,2] += np.sum(int_fact[i]*PS11*ocean_function[:,i]*PC11)/(4.0*np.pi) - IMAT[2,2] += np.sum(int_fact[i]*PS11*ocean_function[:,i]*PS11)/(4.0*np.pi) + # 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) + # 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 + ) + # 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 + ) + # 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 + ) # get seasonal variations of an initial geocenter correction # for use in the land water mass calculation @@ -628,83 +753,102 @@ 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 - - # Removing monthly GIA signal and atmospheric correction + 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 GRACE_Ylms = GSM_Ylms.index(t) GRACE_Ylms.subtract(GIA_Ylms.index(t)) GRACE_Ylms.subtract(ATM_Ylms.index(t)) + GRACE_Ylms.subtract(remove_Ylms.index(t)) + # combining GRACE/GRACE-FO with uncertainty for monte carlo run + GRACE_Ylms.add(Ylms) # G matrix calculates the GRACE ocean mass variations G = gravtk.geocenter() G.C10 = 0.0 G.C11 = 0.0 G.S11 = 0.0 - # calculate non-iterated terms (G-matrix parameters) - # calculate geocenter component of ocean mass with GRACE - # allocate for product of grace and legendre polynomials - pcos = np.zeros((MMAX+1, nlat))#-[m,lat] - psin = np.zeros((MMAX+1, nlat))#-[m,lat] - # Summing product of plms and c/slms over all SH degrees >= 2 - for i in range(0, nlat): - l = np.arange(2,LMAX+1) - pcos[:,i] = np.sum(plmout[l,:,i]*(GRACE_Ylms.clm[l,:]+Ylms.clm[l,:]), axis=0) - psin[:,i] = np.sum(plmout[l,:,i]*(GRACE_Ylms.slm[l,:]+Ylms.slm[l,:]), axis=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, :] + ) # 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.dot(np.transpose(ccos),pcos) + np.dot(np.transpose(ssin),psin) + rmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # calculate G matrix parameters through a summation of each latitude - for i in range(0,nlat): - # C10, C11, S11 - PC10 = P10[i]*ccos[0,:] - PC11 = P11[i]*ccos[1,:] - PS11 = P11[i]*ssin[1,:] - # summation of integration factors, Legendre polynomials, - # (convolution of order and harmonics) and the ocean mass at t - G.C10 += np.sum(int_fact[i]*PC10*ocean_function[:,i]*rmass[:,i])/(4.0*np.pi) - G.C11 += np.sum(int_fact[i]*PC11*ocean_function[:,i]*rmass[:,i])/(4.0*np.pi) - G.S11 += np.sum(int_fact[i]*PS11*ocean_function[:,i]*rmass[:,i])/(4.0*np.pi) + # 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, + ) # 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] - # Removing monthly GIA signal and atmospheric correction + 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) GRACE_Ylms.subtract(GIA_Ylms.index(t)) GRACE_Ylms.subtract(ATM_Ylms.index(t)) + GRACE_Ylms.subtract(remove_Ylms.index(t)) + # combining GRACE/GRACE-FO with uncertainty for monte carlo run + GRACE_Ylms.add(Ylms) - # allocate for product of grace and legendre polynomials - pcos = np.zeros((MMAX+1, nlat))#-[m,lat] - psin = np.zeros((MMAX+1, nlat))#-[m,lat] - # Summing product of plms and c/slms over all SH degrees - for i in range(0, nlat): - # for land water: use an initial seasonal geocenter estimate - # from Chen et al. (1999) - l = np.arange(1,LMAX+1) - pcos[:,i] = np.sum(plmout[l,:,i]*(GRACE_Ylms.clm[l,:]+Ylms.clm[l,:]), axis=0) - psin[:,i] = np.sum(plmout[l,:,i]*(GRACE_Ylms.slm[l,:]+Ylms.slm[l,:]), axis=0) + # 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, :] + ) # Multiplying by c/s(phi#m) to get surface density in cm w.e. (lonxlat) - # this will be a spatial field similar to outputs from stokes_combine.py # ccos/ssin are mXphi, pcos/psin are mXtheta: resultant matrices are phiXtheta # The summation over spherical harmonic order is in this multiplication - lmass = np.dot(np.transpose(ccos),pcos) + np.dot(np.transpose(ssin),psin) + lmass = np.einsum('mp...,mh...->ph...', m_phi, pconv).real # use sea level fingerprints or eustatic from GRACE land components if FINGERPRINT: @@ -714,67 +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) - # save geocenter for iteration and time t after restoring GIA+ATM - iteration.C10[t,n_iter] = DMAT[0]+gia.C10[t]+atm.C10[t] - iteration.C11[t,n_iter] = DMAT[1]+gia.C11[t]+atm.C11[t] - iteration.S11[t,n_iter] = DMAT[2]+gia.S11[t]+atm.S11[t] + 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] + ) # 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() @@ -782,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 @@ -820,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) @@ -852,21 +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 - fileID.date_created = time.strftime('%Y-%m-%d',time.localtime()) + for att_name, att_val in attributes.items(): + fileID.setncattr(att_name, att_val) + fileID.date_created = time.strftime('%Y-%m-%d', time.localtime()) # close the output file fileID.close() # set the permissions mode of the output file @@ -877,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') @@ -921,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 @@ -980,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)) @@ -1067,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 @@ -1143,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 @@ -1165,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)) @@ -1184,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( @@ -1191,65 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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' @@ -1265,101 +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', + ) + 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', + ) # 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] @@ -1396,24 +1892,29 @@ def main(): DATAFORM=args.format, MEAN_FILE=args.mean_file, MEANFORM=args.mean_format, + REMOVE_FILES=args.remove_file, + REMOVE_FORMAT=args.remove_format, + REDISTRIBUTE_REMOVED=args.redistribute_removed, ERROR_FILES=args.error_file, SOLVER=args.solver, FINGERPRINT=args.fingerprint, 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 992018f6..2f56ffed 100644 --- a/gravity_toolkit/SLR/C20.py +++ b/gravity_toolkit/SLR/C20.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" C20.py Written by Tyler Sutterley (05/2023) @@ -105,11 +105,13 @@ Will accommodate upcoming GRACE RL05, which will use different SLR files Written 12/2011 """ + import re import pathlib import numpy as np import gravity_toolkit.time + # PURPOSE: read oblateness data from Satellite Laser Ranging (SLR) def C20(SLR_file, AOD=True, HEADER=True): r""" @@ -145,7 +147,7 @@ def C20(SLR_file, AOD=True, HEADER=True): # output dictionary with data variables dinput = {} # determine if imported file is from PO.DAAC or CSR - if bool(re.search(r'C20_RL\d+', SLR_file.name ,re.I)): + if bool(re.search(r'C20_RL\d+', SLR_file.name, re.I)): # SLR C20 file from CSR # Just for checking new months when TN series isn't up to date as the # SLR estimates always use the full set of days in each calendar month. @@ -157,8 +159,16 @@ def C20(SLR_file, AOD=True, HEADER=True): # Column 5: Mean value of Atmosphere-Ocean De-aliasing model (1E-10) # Columns 6-7: Start and end dates of data used in solution dtype = {} - dtype['names'] = ('time','C20','delta','sigma','AOD','start','end') - dtype['formats'] = ('f','f8','f','f','f','f','f') + dtype['names'] = ( + 'time', + 'C20', + 'delta', + 'sigma', + 'AOD', + 'start', + 'end', + ) + dtype['formats'] = ('f', 'f8', 'f', 'f', 'f', 'f', 'f') # header text is commented and won't be read file_input = np.loadtxt(SLR_file, dtype=dtype) # date and GRACE/GRACE-FO month @@ -167,13 +177,13 @@ def C20(SLR_file, AOD=True, HEADER=True): # monthly spherical harmonic replacement solutions dinput['data'] = file_input['C20'].copy() # monthly spherical harmonic formal standard deviations - dinput['error'] = file_input['sigma']*1e-10 + dinput['error'] = file_input['sigma'] * 1e-10 # Background gravity model includes solid earth and ocean tides, solid # earth and ocean pole tides, and the Atmosphere-Ocean De-aliasing # product. The monthly mean of the AOD model has been restored. if AOD: # Removing AOD product that was restored in the solution - dinput['data'] -= file_input['AOD']*1e-10 + dinput['data'] -= file_input['AOD'] * 1e-10 elif bool(re.search(r'GFZ_(RL\d+)_C20_SLR', SLR_file.name, re.I)): # SLR C20 file from GFZ # Column 1: MJD of BEGINNING of solution span @@ -192,7 +202,7 @@ def C20(SLR_file, AOD=True, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line)) + HEADER = not bool(re.match(r'PRODUCT:+', line)) # add 1 to counter count += 1 @@ -200,7 +210,7 @@ def C20(SLR_file, AOD=True, HEADER=True): n_mon = file_lines - count # date and GRACE/GRACE-FO month dinput['time'] = np.zeros((n_mon)) - dinput['month'] = np.zeros((n_mon),dtype=np.int64) + dinput['month'] = np.zeros((n_mon), dtype=np.int64) # monthly spherical harmonic replacement solutions dinput['data'] = np.zeros((n_mon)) # monthly spherical harmonic formal standard deviations @@ -211,21 +221,22 @@ def C20(SLR_file, AOD=True, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) # check if line has G* or Gm flags - if bool(re.search(r'(G\*|Gm)',line)): + if bool(re.search(r'(G\*|Gm)', line)): # reading decimal year for start of span dinput['time'][t] = np.float64(line_contents[1]) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[2]) - dinput['error'][t] = np.float64(line_contents[4])*1e-10 + dinput['error'][t] = np.float64(line_contents[4]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] elif bool(re.search(r'GRAVIS-2B_GFZOP', SLR_file.name, re.I)): @@ -247,7 +258,7 @@ def C20(SLR_file, AOD=True, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line)) + HEADER = not bool(re.match(r'PRODUCT:+', line)) # add 1 to counter count += 1 @@ -266,22 +277,23 @@ def C20(SLR_file, AOD=True, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) count = len(line_contents) # check for empty lines - if (count > 0): + if count > 0: # reading decimal year for start of span dinput['time'][t] = np.float64(line_contents[1]) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[2]) - dinput['error'][t] = np.float64(line_contents[4])*1e-10 + dinput['error'][t] = np.float64(line_contents[4]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] elif bool(re.search(r'TN-(11|14)', SLR_file.name, re.I)): @@ -298,7 +310,7 @@ def C20(SLR_file, AOD=True, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line,re.IGNORECASE)) + HEADER = not bool(re.match(r'PRODUCT:+', line, re.IGNORECASE)) # add 1 to counter count += 1 @@ -306,7 +318,7 @@ def C20(SLR_file, AOD=True, HEADER=True): n_mon = file_lines - count # date and GRACE/GRACE-FO month dinput['time'] = np.zeros((n_mon)) - dinput['month'] = np.zeros((n_mon),dtype=np.int64) + dinput['month'] = np.zeros((n_mon), dtype=np.int64) # monthly spherical harmonic replacement solutions dinput['data'] = np.zeros((n_mon)) # monthly spherical harmonic formal standard deviations @@ -317,31 +329,36 @@ def C20(SLR_file, AOD=True, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) # check for empty lines as there are # slight differences in RL04 TN-05_C20_SLR.txt # with blanks between the PRODUCT: line and the data count = len(line_contents) # if count is greater than 0 - if (count > 0): + if count > 0: # modified julian date for line MJD = np.float64(line_contents[0]) # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - MJD+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + MJD + 2400000.5, format='tuple' + ) # converting from month, day, year into decimal year - dinput['time'][t] = gravity_toolkit.time.convert_calendar_decimal( - YY, MM, day=DD, hour=hh) + (dinput['time'][t],) = ( + gravity_toolkit.time.convert_calendar_decimal( + YY, MM, day=DD, hour=hh + ) + ) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[2]) - dinput['error'][t] = np.float64(line_contents[4])*1e-10 + dinput['error'][t] = np.float64(line_contents[4]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] else: # SLR C20 file from PO.DAAC @@ -357,7 +374,7 @@ def C20(SLR_file, AOD=True, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line)) + HEADER = not bool(re.match(r'PRODUCT:+', line)) # add 1 to counter count += 1 @@ -370,33 +387,35 @@ def C20(SLR_file, AOD=True, HEADER=True): # monthly spherical harmonic formal standard deviations eC20_input = np.zeros((n_mon)) # flag denoting if replacement solution - slr_flag = np.zeros((n_mon),dtype=bool) + slr_flag = np.zeros((n_mon), dtype=bool) # time count t = 0 # for every other line: for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) # check for empty lines as there are # slight differences in RL04 TN-05_C20_SLR.txt # with blanks between the PRODUCT: line and the data count = len(line_contents) # if count is greater than 0 - if (count > 0): + if count > 0: # modified julian date for line MJD = np.float64(line_contents[0]) # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - MJD+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + MJD + 2400000.5, format='tuple' + ) # converting from month, day, year into decimal year - date_conv[t] = gravity_toolkit.time.convert_calendar_decimal( - YY, MM, day=DD, hour=hh) + (date_conv[t],) = gravity_toolkit.time.convert_calendar_decimal( + YY, MM, day=DD, hour=hh + ) # Spherical Harmonic data for line C20_input[t] = np.float64(line_contents[2]) - eC20_input[t] = np.float64(line_contents[4])*1e-10 + eC20_input[t] = np.float64(line_contents[4]) * 1e-10 # line has * flag - if bool(re.search(r'\*',line)): + if bool(re.search(r'\*', line)): slr_flag[t] = True # add to t count t += 1 @@ -408,7 +427,7 @@ def C20(SLR_file, AOD=True, HEADER=True): slr_flag = slr_flag[:t] # GRACE/GRACE-FO month of SLR solutions - mon = gravity_toolkit.time.calendar_to_grace(date_conv,around=np.round) + mon = gravity_toolkit.time.calendar_to_grace(date_conv, around=np.round) # number of unique months dinput['month'] = np.unique(mon) n_uniq = len(dinput['month']) @@ -423,14 +442,14 @@ def C20(SLR_file, AOD=True, HEADER=True): for t in range(n_uniq): count = np.count_nonzero(mon == dinput['month'][t]) # there is only one solution for the month - if (count == 1): + if count == 1: i = np.nonzero(mon == dinput['month'][t]) dinput['time'][t] = date_conv[i] dinput['data'][t] = C20_input[i] dinput['error'][t] = eC20_input[i] # there is a special solution for the month # will the solution flagged with slr_flag - elif (count == 2): + elif count == 2: i = np.nonzero((mon == dinput['month'][t]) & slr_flag) dinput['time'][t] = date_conv[i] dinput['data'][t] = C20_input[i] @@ -446,4 +465,4 @@ def C20(SLR_file, AOD=True, HEADER=True): dinput['month'] = gravity_toolkit.time.adjust_months(dinput['month']) # return the SLR-derived oblateness solutions - return dinput \ No newline at end of file + return dinput diff --git a/gravity_toolkit/SLR/C30.py b/gravity_toolkit/SLR/C30.py index 84c2dda4..d55c50af 100644 --- a/gravity_toolkit/SLR/C30.py +++ b/gravity_toolkit/SLR/C30.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" C30.py Written by Yara Mohajerani and Tyler Sutterley (05/2023) @@ -77,12 +77,14 @@ read CSR monthly 5x5 file and extract C3,0 coefficients Written 05/2019 """ + import re import pathlib import numpy as np import gravity_toolkit.time import gravity_toolkit.read_SLR_harmonics + # PURPOSE: read Degree 3 zonal data from Satellite Laser Ranging (SLR) def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): r""" @@ -118,7 +120,6 @@ def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): dinput = {} # determine source of input file if bool(re.search(r'TN-(14)', SLR_file.name, re.I)): - # SLR C30 RL06 file from PO.DAAC produced by GSFC with SLR_file.open(mode='r', encoding='utf8') as f: file_contents = f.read().splitlines() @@ -132,7 +133,7 @@ def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'Product:+',line)) + HEADER = not bool(re.match(r'Product:+', line)) # add 1 to counter count += 1 @@ -151,41 +152,46 @@ def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) count = len(line_contents) # only read lines where C30 data exists (don't read NaN lines) - if (count > 7): + if count > 7: # modified julian date for line MJD = np.float64(line_contents[0]) # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - MJD+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + MJD + 2400000.5, format='tuple' + ) # converting from month, day, year into decimal year - dinput['time'][t] = gravity_toolkit.time.convert_calendar_decimal( - YY, MM, day=DD, hour=hh) + (dinput['time'][t],) = ( + gravity_toolkit.time.convert_calendar_decimal( + YY, MM, day=DD, hour=hh + ) + ) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[5]) - dinput['error'][t] = np.float64(line_contents[7])*1e-10 + dinput['error'][t] = np.float64(line_contents[7]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # verify that there imported C30 solutions # (TN-14 data format has changed in the past) - if (t == 0): + if t == 0: raise Exception('No GSFC C30 data imported') # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] elif bool(re.search(r'C30_LARES', SLR_file.name, re.I)): # read LARES filtered values - LARES_input = np.loadtxt(SLR_file,skiprows=1) - dinput['time'] = LARES_input[:,0].copy() + LARES_input = np.loadtxt(SLR_file, skiprows=1) + dinput['time'] = LARES_input[:, 0].copy() # convert C30 from anomalies to absolute - dinput['data'] = 1e-10*LARES_input[:,1] + C30_MEAN + dinput['data'] = 1e-10 * LARES_input[:, 1] + C30_MEAN # filtered data does not have errors - dinput['error'] = np.zeros_like(LARES_input[:,1]) + dinput['error'] = np.zeros_like(LARES_input[:, 1]) # calculate GRACE/GRACE-FO month dinput['month'] = gravity_toolkit.time.calendar_to_grace(dinput['time']) elif bool(re.search(r'GRAVIS-2B_GFZOP', SLR_file.name, re.I)): @@ -207,7 +213,7 @@ def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line)) + HEADER = not bool(re.match(r'PRODUCT:+', line)) # add 1 to counter count += 1 @@ -226,35 +232,37 @@ def C30(SLR_file, C30_MEAN=9.5717395773300e-07, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) count = len(line_contents) # check for empty lines - if (count > 0): + if count > 0: # reading decimal year for start of span dinput['time'][t] = np.float64(line_contents[1]) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[5]) - dinput['error'][t] = np.float64(line_contents[7])*1e-10 + dinput['error'][t] = np.float64(line_contents[7]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] else: # CSR 5x5 + 6,1 file from CSR and extract C3,0 coefficients Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # extract dates, C30 harmonics and errors dinput['time'] = Ylms['time'].copy() - dinput['data'] = Ylms['clm'][3,0,:].copy() - dinput['error'] = Ylms['error']['clm'][3,0,:].copy() + dinput['data'] = Ylms['clm'][3, 0, :].copy() + dinput['error'] = Ylms['error']['clm'][3, 0, :].copy() # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - Ylms['MJD']+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + Ylms['MJD'] + 2400000.5, format='tuple' + ) # calculate GRACE/GRACE-FO month - dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY,MM) + dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY, MM) # The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with # Accelerometer shutoffs make the relation between month number diff --git a/gravity_toolkit/SLR/C40.py b/gravity_toolkit/SLR/C40.py index b1dd7ab6..f75171a7 100644 --- a/gravity_toolkit/SLR/C40.py +++ b/gravity_toolkit/SLR/C40.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" C40.py Written by Tyler Sutterley (05/2023) @@ -47,12 +47,14 @@ Updated 01/2023: refactored satellite laser ranging read functions Written 09/2022 """ + import re import pathlib import numpy as np import gravity_toolkit.time import gravity_toolkit.read_SLR_harmonics + # PURPOSE: read Degree 4 zonal data from Satellite Laser Ranging (SLR) def C40(SLR_file, C40_MEAN=0.0, DATE=None, **kwargs): r""" @@ -91,18 +93,21 @@ def C40(SLR_file, C40_MEAN=0.0, DATE=None, **kwargs): # read 5x5 + 6,1 file from GSFC and extract coefficients Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # calculate 28-day moving-average solution from 7-day arcs - dinput.update(gravity_toolkit.convert_weekly(Ylms['time'], - Ylms['clm'][4,0,:], DATE=DATE, NEIGHBORS=28)) + dinput.update( + gravity_toolkit.convert_weekly( + Ylms['time'], Ylms['clm'][4, 0, :], DATE=DATE, NEIGHBORS=28 + ) + ) # no estimated spherical harmonic errors - dinput['error'] = np.zeros_like(DATE,dtype='f8') + dinput['error'] = np.zeros_like(DATE, dtype='f8') elif bool(re.search(r'C40_LARES', SLR_file.name, re.I)): # read LARES filtered values LARES_input = np.loadtxt(SLR_file, skiprows=1) - dinput['time'] = LARES_input[:,0].copy() + dinput['time'] = LARES_input[:, 0].copy() # convert C40 from anomalies to absolute - dinput['data'] = 1e-10*LARES_input[:,1] + C40_MEAN + dinput['data'] = 1e-10 * LARES_input[:, 1] + C40_MEAN # filtered data does not have errors - dinput['error'] = np.zeros_like(LARES_input[:,1]) + dinput['error'] = np.zeros_like(LARES_input[:, 1]) # calculate GRACE/GRACE-FO month dinput['month'] = gravity_toolkit.time.calendar_to_grace(dinput['time']) else: @@ -110,13 +115,14 @@ def C40(SLR_file, C40_MEAN=0.0, DATE=None, **kwargs): Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # extract dates, C40 harmonics and errors dinput['time'] = Ylms['time'].copy() - dinput['data'] = Ylms['clm'][4,0,:].copy() - dinput['error'] = Ylms['error']['clm'][4,0,:].copy() + dinput['data'] = Ylms['clm'][4, 0, :].copy() + dinput['error'] = Ylms['error']['clm'][4, 0, :].copy() # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - Ylms['MJD']+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + Ylms['MJD'] + 2400000.5, format='tuple' + ) # calculate GRACE/GRACE-FO month - dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY,MM) + dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY, MM) # The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with # Accelerometer shutoffs make the relation between month number diff --git a/gravity_toolkit/SLR/C50.py b/gravity_toolkit/SLR/C50.py index af93dada..2f9e75bd 100644 --- a/gravity_toolkit/SLR/C50.py +++ b/gravity_toolkit/SLR/C50.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" C50.py Written by Yara Mohajerani and Tyler Sutterley (05/2023) @@ -57,12 +57,14 @@ Updated 07/2020: added function docstrings Written 11/2019 """ + import re import pathlib import numpy as np import gravity_toolkit.time import gravity_toolkit.read_SLR_harmonics + # PURPOSE: read Degree 5 zonal data from Satellite Laser Ranging (SLR) def C50(SLR_file, C50_MEAN=0.0, DATE=None, HEADER=True): r""" @@ -100,7 +102,6 @@ def C50(SLR_file, C50_MEAN=0.0, DATE=None, HEADER=True): dinput = {} # determine source of input file if bool(re.search(r'GSFC_SLR_C(20)_C(30)_C(50)', SLR_file.name, re.I)): - # SLR C50 RL06 file from GSFC with SLR_file.open(mode='r', encoding='utf8') as f: file_contents = f.read().splitlines() @@ -114,7 +115,7 @@ def C50(SLR_file, C50_MEAN=0.0, DATE=None, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'Product:+',line)) + HEADER = not bool(re.match(r'Product:+', line)) # add 1 to counter count += 1 @@ -133,48 +134,56 @@ def C50(SLR_file, C50_MEAN=0.0, DATE=None, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) count = len(line_contents) # only read lines where C50 data exists (don't read NaN lines) - if (count > 7): + if count > 7: # modified julian date for line MJD = np.float64(line_contents[0]) # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - MJD+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + MJD + 2400000.5, format='tuple' + ) # converting from month, day, year into decimal year - dinput['time'][t] = gravity_toolkit.time.convert_calendar_decimal( - YY, MM, day=DD, hour=hh) + (dinput['time'][t],) = ( + gravity_toolkit.time.convert_calendar_decimal( + YY, MM, day=DD, hour=hh + ) + ) # Spherical Harmonic data for line dinput['data'][t] = np.float64(line_contents[10]) - dinput['error'][t] = np.float64(line_contents[12])*1e-10 + dinput['error'][t] = np.float64(line_contents[12]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # verify that there imported C50 solutions - if (t == 0): + if t == 0: raise Exception('No GSFC C50 data imported') # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] elif bool(re.search(r'gsfc_slr_5x5c61s61', SLR_file.name, re.I)): # read 5x5 + 6,1 file from GSFC and extract coefficients Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # calculate 28-day moving-average solution from 7-day arcs - dinput.update(gravity_toolkit.convert_weekly(Ylms['time'], - Ylms['clm'][5,0,:], DATE=DATE, NEIGHBORS=28)) + dinput.update( + gravity_toolkit.convert_weekly( + Ylms['time'], Ylms['clm'][5, 0, :], DATE=DATE, NEIGHBORS=28 + ) + ) # no estimated spherical harmonic errors - dinput['error'] = np.zeros_like(DATE,dtype='f8') + dinput['error'] = np.zeros_like(DATE, dtype='f8') elif bool(re.search(r'C50_LARES', SLR_file.name, re.I)): # read LARES filtered values LARES_input = np.loadtxt(SLR_file, skiprows=1) - dinput['time'] = LARES_input[:,0].copy() + dinput['time'] = LARES_input[:, 0].copy() # convert C50 from anomalies to absolute - dinput['data'] = 1e-10*LARES_input[:,1] + C50_MEAN + dinput['data'] = 1e-10 * LARES_input[:, 1] + C50_MEAN # filtered data does not have errors - dinput['error'] = np.zeros_like(LARES_input[:,1]) + dinput['error'] = np.zeros_like(LARES_input[:, 1]) # calculate GRACE/GRACE-FO month dinput['month'] = gravity_toolkit.time.calendar_to_grace(dinput['time']) else: @@ -182,13 +191,14 @@ def C50(SLR_file, C50_MEAN=0.0, DATE=None, HEADER=True): Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # extract dates, C50 harmonics and errors dinput['time'] = Ylms['time'].copy() - dinput['data'] = Ylms['clm'][5,0,:].copy() - dinput['error'] = Ylms['error']['clm'][5,0,:].copy() + dinput['data'] = Ylms['clm'][5, 0, :].copy() + dinput['error'] = Ylms['error']['clm'][5, 0, :].copy() # converting from MJD into month, day and year - YY,MM,DD,hh,mm,ss = gravity_toolkit.time.convert_julian( - Ylms['MJD']+2400000.5, format='tuple') + YY, MM, DD, hh, mm, ss = gravity_toolkit.time.convert_julian( + Ylms['MJD'] + 2400000.5, format='tuple' + ) # calculate GRACE/GRACE-FO month - dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY,MM) + dinput['month'] = gravity_toolkit.time.calendar_to_grace(YY, MM) # The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with # Accelerometer shutoffs make the relation between month number diff --git a/gravity_toolkit/SLR/CS2.py b/gravity_toolkit/SLR/CS2.py index 372fad2a..4c037813 100644 --- a/gravity_toolkit/SLR/CS2.py +++ b/gravity_toolkit/SLR/CS2.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" CS2.py Written by Hugo Lecomte and Tyler Sutterley (05/2023) @@ -77,12 +77,14 @@ Updated 04/2021: use adjust_months function to fix special months cases Written 11/2020 """ + import re import pathlib import numpy as np import gravity_toolkit.time import gravity_toolkit.read_SLR_harmonics + # PURPOSE: read Degree 2,m data from Satellite Laser Ranging (SLR) def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): r""" @@ -128,30 +130,30 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): # 7-day arc SLR file produced by GSFC # input variable names and types dtype = {} - dtype['names'] = ('time','C2','S2') - dtype['formats'] = ('f','f8','f8') + dtype['names'] = ('time', 'C2', 'S2') + dtype['formats'] = ('f', 'f8', 'f8') # read SLR 2,1 file from GSFC # Column 1: Approximate mid-point of 7-day solution (years) # Column 2: Solution from SLR (normalized) # Column 3: Solution from SLR (normalized) content = np.loadtxt(SLR_file, dtype=dtype) # duplicate time and harmonics - tdec = np.repeat(content['time'],7) - c2m = np.repeat(content['C2'],7) - s2m = np.repeat(content['S2'],7) + tdec = np.repeat(content['time'], 7) + c2m = np.repeat(content['C2'], 7) + s2m = np.repeat(content['S2'], 7) # calculate daily dates to use in centered moving average - tdec += (np.mod(np.arange(len(tdec)),7) - 3.5)/365.25 + tdec += (np.mod(np.arange(len(tdec)), 7) - 3.5) / 365.25 # number of dates to use in average n_neighbors = 28 # calculate 28-day moving-average solution from 7-day arcs dinput['time'] = np.zeros_like(DATE) - dinput['C2m'] = np.zeros_like(DATE,dtype='f8') - dinput['S2m'] = np.zeros_like(DATE,dtype='f8') + dinput['C2m'] = np.zeros_like(DATE, dtype='f8') + dinput['S2m'] = np.zeros_like(DATE, dtype='f8') # no estimated spherical harmonic errors - dinput['eC2m'] = np.zeros_like(DATE,dtype='f8') - dinput['eS2m'] = np.zeros_like(DATE,dtype='f8') - for i,D in enumerate(DATE): - isort = np.argsort((tdec - D)**2)[:n_neighbors] + dinput['eC2m'] = np.zeros_like(DATE, dtype='f8') + dinput['eS2m'] = np.zeros_like(DATE, dtype='f8') + for i, D in enumerate(DATE): + isort = np.argsort((tdec - D) ** 2)[:n_neighbors] dinput['time'][i] = np.mean(tdec[isort]) dinput['C2m'][i] = np.mean(c2m[isort]) dinput['S2m'][i] = np.mean(s2m[isort]) @@ -161,22 +163,22 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): # read 5x5 + 6,1 file from GSFC and extract coefficients Ylms = gravity_toolkit.read_SLR_harmonics(SLR_file, HEADER=True) # duplicate time and harmonics - tdec = np.repeat(Ylms['time'],7) - c2m = np.repeat(Ylms['clm'][2,ORDER],7) - s2m = np.repeat(Ylms['slm'][2,ORDER],7) + tdec = np.repeat(Ylms['time'], 7) + c2m = np.repeat(Ylms['clm'][2, ORDER], 7) + s2m = np.repeat(Ylms['slm'][2, ORDER], 7) # calculate daily dates to use in centered moving average - tdec += (np.mod(np.arange(len(tdec)),7) - 3.5)/365.25 + tdec += (np.mod(np.arange(len(tdec)), 7) - 3.5) / 365.25 # number of dates to use in average n_neighbors = 28 # calculate 28-day moving-average solution from 7-day arcs dinput['time'] = np.zeros_like(DATE) - dinput['C2m'] = np.zeros_like(DATE,dtype='f8') - dinput['S2m'] = np.zeros_like(DATE,dtype='f8') + dinput['C2m'] = np.zeros_like(DATE, dtype='f8') + dinput['S2m'] = np.zeros_like(DATE, dtype='f8') # no estimated spherical harmonic errors - dinput['eC2m'] = np.zeros_like(DATE,dtype='f8') - dinput['eS2m'] = np.zeros_like(DATE,dtype='f8') - for i,D in enumerate(DATE): - isort = np.argsort((tdec - D)**2)[:n_neighbors] + dinput['eC2m'] = np.zeros_like(DATE, dtype='f8') + dinput['eS2m'] = np.zeros_like(DATE, dtype='f8') + for i, D in enumerate(DATE): + isort = np.argsort((tdec - D) ** 2)[:n_neighbors] dinput['time'][i] = np.mean(tdec[isort]) dinput['C2m'][i] = np.mean(c2m[isort]) dinput['S2m'][i] = np.mean(s2m[isort]) @@ -186,9 +188,18 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): # SLR RL06 file produced by CSR # input variable names and types dtype = {} - dtype['names'] = ('time','C2','S2','eC2','eS2', - 'C2aod','S2aod','start','end') - dtype['formats'] = ('f','f8','f8','f','f','f','f','f','f') + dtype['names'] = ( + 'time', + 'C2', + 'S2', + 'eC2', + 'eS2', + 'C2aod', + 'S2aod', + 'start', + 'end', + ) + dtype['formats'] = ('f', 'f8', 'f8', 'f', 'f', 'f', 'f', 'f', 'f') # read SLR 2,1 or 2,2 RL06 file from CSR # header text is commented and won't be read # Column 1: Approximate mid-point of monthly solution (years) @@ -204,11 +215,11 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): dinput['time'] = content['time'].copy() dinput['month'] = gravity_toolkit.time.calendar_to_grace(dinput['time']) # remove the monthly mean of the AOD model - dinput['C2m'] = content['C2'] - content['C2aod']*10**-10 - dinput['S2m'] = content['S2'] - content['S2aod']*10**-10 + dinput['C2m'] = content['C2'] - content['C2aod'] * 10**-10 + dinput['S2m'] = content['S2'] - content['S2aod'] * 10**-10 # scale SLR solution sigmas - dinput['eC2m'] = content['eC2']*10**-10 - dinput['eS2m'] = content['eS2']*10**-10 + dinput['eC2m'] = content['eC2'] * 10**-10 + dinput['eS2m'] = content['eS2'] * 10**-10 elif bool(re.search(r'GRAVIS-2B_GFZOP', SLR_file.name, re.I)): # Combined GRACE/SLR solution file produced by GFZ # Column 1: MJD of BEGINNING of solution data span @@ -231,7 +242,7 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): # file line at count line = file_contents[count] # find PRODUCT: within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'PRODUCT:+',line)) + HEADER = not bool(re.match(r'PRODUCT:+', line)) # add 1 to counter count += 1 @@ -252,24 +263,25 @@ def CS2(SLR_file, ORDER=1, DATE=None, HEADER=True): for line in file_contents[count:]: # find numerical instances in line including exponents, # decimal points and negatives - line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?',line) + line_contents = re.findall(r'[-+]?\d*\.\d*(?:[eE][-+]?\d+)?', line) count = len(line_contents) # check for empty lines - if (count > 0): + if count > 0: # reading decimal year for start of span dinput['time'][t] = np.float64(line_contents[1]) # Spherical Harmonic data for line dinput['C2m'][t] = np.float64(line_contents[8]) - dinput['eC2m'][t] = np.float64(line_contents[10])*1e-10 + dinput['eC2m'][t] = np.float64(line_contents[10]) * 1e-10 dinput['S2m'][t] = np.float64(line_contents[11]) - dinput['eS2m'][t] = np.float64(line_contents[13])*1e-10 + dinput['eS2m'][t] = np.float64(line_contents[13]) * 1e-10 # GRACE/GRACE-FO month of SLR solutions dinput['month'][t] = gravity_toolkit.time.calendar_to_grace( - dinput['time'][t], around=np.round) + dinput['time'][t], around=np.round + ) # add to t count t += 1 # truncate variables if necessary - for key,val in dinput.items(): + for key, val in dinput.items(): dinput[key] = val[:t] # The 'Special Months' (Nov 2011, Dec 2011 and April 2012) with diff --git a/gravity_toolkit/__init__.py b/gravity_toolkit/__init__.py index a6eff2f4..99f1129e 100644 --- a/gravity_toolkit/__init__.py +++ b/gravity_toolkit/__init__.py @@ -15,6 +15,7 @@ Documentation is available at https://gravity-toolkit.readthedocs.io """ + import gravity_toolkit.geocenter import gravity_toolkit.mascons import gravity_toolkit.time @@ -27,15 +28,12 @@ associated_legendre, plm_colombo, plm_holmes, - plm_mohlenkamp + plm_mohlenkamp, ) from gravity_toolkit.clenshaw_summation import clenshaw_summation from gravity_toolkit.degree_amplitude import degree_amplitude from gravity_toolkit.destripe_harmonics import destripe_harmonics -from gravity_toolkit.fourier_legendre import ( - fourier_legendre, - legendre_gradient -) +from gravity_toolkit.fourier_legendre import fourier_legendre, legendre_gradient from gravity_toolkit.gauss_weights import gauss_weights from gravity_toolkit.gen_averaging_kernel import gen_averaging_kernel from gravity_toolkit.gen_disc_load import gen_disc_load @@ -48,42 +46,37 @@ from gravity_toolkit.grace_find_months import grace_find_months from gravity_toolkit.grace_input_months import ( grace_input_months, - read_ecmwf_corrections + read_ecmwf_corrections, ) from gravity_toolkit.grace_months_index import grace_months_index from gravity_toolkit.harmonics import harmonics from gravity_toolkit.harmonic_gradients import ( harmonic_gradients, - geostrophic_currents + geostrophic_currents, ) from gravity_toolkit.harmonic_summation import ( harmonic_summation, harmonic_transform, - stokes_summation + stokes_summation, ) from gravity_toolkit.legendre_polynomials import legendre_polynomials from gravity_toolkit.legendre import legendre from gravity_toolkit.ocean_stokes import ocean_stokes, land_stokes from gravity_toolkit.read_gfc_harmonics import read_gfc_harmonics -from gravity_toolkit.read_GIA_model import ( - read_GIA_model, - gia -) +from gravity_toolkit.read_GIA_model import read_GIA_model, gia from gravity_toolkit.read_GRACE_harmonics import read_GRACE_harmonics from gravity_toolkit.read_love_numbers import ( read_love_numbers, load_love_numbers, - love_numbers + love_numbers, ) from gravity_toolkit.read_SLR_harmonics import ( read_SLR_harmonics, - convert_weekly + convert_weekly, ) from gravity_toolkit.sea_level_equation import sea_level_equation -from gravity_toolkit.spatial import ( - spatial, - scaling_factors -) +from gravity_toolkit.spatial import spatial, scaling_factors from gravity_toolkit.units import units + # get version number __version__ = gravity_toolkit.version.version diff --git a/gravity_toolkit/associated_legendre.py b/gravity_toolkit/associated_legendre.py index cfb11abe..20b921c4 100644 --- a/gravity_toolkit/associated_legendre.py +++ b/gravity_toolkit/associated_legendre.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" associated_legendre.py Written by Tyler Sutterley (03/2023) @@ -19,14 +19,12 @@ Updated 09/2013: new format for file headers Written 03/2013 """ + from __future__ import division import numpy as np -def associated_legendre(LMAX, x, - method='holmes', - MMAX=None, - astype=np.float64 - ): + +def associated_legendre(LMAX, x, method='holmes', MMAX=None, astype=np.float64): """ Computes fully-normalized associated Legendre Polynomials and their first derivative @@ -57,18 +55,16 @@ def associated_legendre(LMAX, x, dplms: np.ndarray first derivative of Legendre polynomials """ - if (method.lower() == 'colombo'): + if method.lower() == 'colombo': return plm_colombo(LMAX, x, MMAX=MMAX, astype=astype) - elif (method.lower() == 'holmes'): + elif method.lower() == 'holmes': return plm_holmes(LMAX, x, MMAX=MMAX, astype=astype) - elif (method.lower() == 'mohlenkamp'): + elif method.lower() == 'mohlenkamp': return plm_mohlenkamp(LMAX, x, MMAX=MMAX, astype=astype) raise ValueError(f'Unknown method {method}') -def plm_colombo(LMAX, x, - MMAX=None, - astype=np.float64 - ): + +def plm_colombo(LMAX, x, MMAX=None, astype=np.float64): """ Computes fully-normalized associated Legendre Polynomials and their first derivative using a Standard forward column method :cite:p:`Colombo:1981vh` @@ -105,8 +101,8 @@ def plm_colombo(LMAX, x, MMAX = np.copy(LMAX) # allocating for the plm matrix and differentials - plm = np.zeros((LMAX+1,LMAX+1,jm)) - dplm = np.zeros((LMAX+1,LMAX+1,jm)) + plm = np.zeros((LMAX + 1, LMAX + 1, jm)) + dplm = np.zeros((LMAX + 1, LMAX + 1, jm)) # u is sine of colatitude (cosine of latitude) so that 0 <= s <= 1 # for x=cos(th): u=sin(th) @@ -115,39 +111,50 @@ def plm_colombo(LMAX, x, u[u == 0] = np.finfo(u.dtype).eps # Calculating the initial polynomials for the recursion - plm[0,0,:] = 1.0 - plm[1,0,:] = np.sqrt(3.0)*x - plm[1,1,:] = np.sqrt(3.0)*u + plm[0, 0, :] = 1.0 + plm[1, 0, :] = np.sqrt(3.0) * x + plm[1, 1, :] = np.sqrt(3.0) * u # calculating first derivatives for harmonics of degree 1 - dplm[1,0,:] = (1.0/u)*(x*plm[1,0,:] - np.sqrt(3)*plm[0,0,:]) - dplm[1,1,:] = (x/u)*plm[1,1,:] - for l in range(2, LMAX+1): - for m in range(0, l):# Zonal and Tesseral harmonics (non-sectorial) + dplm[1, 0, :] = (1.0 / u) * (x * plm[1, 0, :] - np.sqrt(3) * plm[0, 0, :]) + dplm[1, 1, :] = (x / u) * plm[1, 1, :] + for l in range(2, LMAX + 1): + for m in range(0, l): # Zonal and Tesseral harmonics (non-sectorial) # Computes the non-sectorial terms from previously computed # sectorial terms. - alm = np.sqrt(((2.0*l-1.0)*(2.0*l+1.0))/((l-m)*(l+m))) - blm = np.sqrt(((2.0*l+1.0)*(l+m-1.0)*(l-m-1.0))/((l-m)*(l+m)*(2.0*l-3.0))) + alm = np.sqrt( + ((2.0 * l - 1.0) * (2.0 * l + 1.0)) / ((l - m) * (l + m)) + ) + blm = np.sqrt( + ((2.0 * l + 1.0) * (l + m - 1.0) * (l - m - 1.0)) + / ((l - m) * (l + m) * (2.0 * l - 3.0)) + ) # if (m == l-1): plm[l-2,m,:] will be 0 - plm[l,m,:] = alm*x*plm[l-1,m,:] - blm*plm[l-2,m,:] + plm[l, m, :] = alm * x * plm[l - 1, m, :] - blm * plm[l - 2, m, :] # calculate first derivatives - flm = np.sqrt(((l**2.0 - m**2.0)*(2.0*l + 1.0))/(2.0*l - 1.0)) - dplm[l,m,:] = (1.0/u)*(l*x*plm[l,m,:] - flm*plm[l-1,m,:]) + flm = np.sqrt( + ((l**2.0 - m**2.0) * (2.0 * l + 1.0)) / (2.0 * l - 1.0) + ) + dplm[l, m, :] = (1.0 / u) * ( + l * x * plm[l, m, :] - flm * plm[l - 1, m, :] + ) # Sectorial harmonics # The sectorial harmonics serve as seed values for the recursion # starting with P00 and P11 (outside the loop) - plm[l,l,:] = u*np.sqrt((2.0*l+1.0)/(2.0*l))*np.squeeze(plm[l-1,l-1,:]) + plm[l, l, :] = ( + u + * np.sqrt((2.0 * l + 1.0) / (2.0 * l)) + * np.squeeze(plm[l - 1, l - 1, :]) + ) # calculate first derivatives for sectorial harmonics - dplm[l,l,:] = np.longdouble(l)*(x/u)*plm[l,l,:] + dplm[l, l, :] = np.longdouble(l) * (x / u) * plm[l, l, :] # return the legendre polynomials and their first derivative # truncating orders to MMAX - return plm[:,:MMAX+1,:], dplm[:,:MMAX+1,:] + return plm[:, : MMAX + 1, :], dplm[:, : MMAX + 1, :] + -def plm_holmes(LMAX, x, - MMAX=None, - astype=np.float64 - ): +def plm_holmes(LMAX, x, MMAX=None, astype=np.float64): """ Computes fully-normalized associated Legendre Polynomials and their first derivative using the recursion relation from :cite:p:`Holmes:2002ff` @@ -186,25 +193,39 @@ def plm_holmes(LMAX, x, scalef = 1.0e-280 # allocate for multiplicative factors, and plms - f1 = np.zeros(((LMAX+1)*(LMAX+2)//2), dtype=astype) - f2 = np.zeros(((LMAX+1)*(LMAX+2)//2), dtype=astype) - p = np.zeros(((LMAX+1)*(LMAX+2)//2,jm), dtype=astype) - plm = np.zeros((LMAX+1,LMAX+1,jm), dtype=astype) - dplm = np.zeros((LMAX+1,LMAX+1,jm), dtype=astype) + f1 = np.zeros(((LMAX + 1) * (LMAX + 2) // 2), dtype=astype) + f2 = np.zeros(((LMAX + 1) * (LMAX + 2) // 2), dtype=astype) + p = np.zeros(((LMAX + 1) * (LMAX + 2) // 2, jm), dtype=astype) + plm = np.zeros((LMAX + 1, LMAX + 1, jm), dtype=astype) + dplm = np.zeros((LMAX + 1, LMAX + 1, jm), dtype=astype) # Precompute multiplicative factors used in recursion relationships # Note that prefactors are not used for the case when m=l and m=l-1, # as a different recursion is used for these two values. - k = 2# k = l*(l+1)/2 + m - for l in range(2, LMAX+1): + k = 2 # k = l*(l+1)/2 + m + for l in range(2, LMAX + 1): k += 1 - f1[k] = np.sqrt(2.0*l-1.0)*np.sqrt(2.0*l+1.0)/np.longdouble(l) - f2[k] = np.longdouble(l-1.0)*np.sqrt(2.0*l+1.0)/(np.sqrt(2.0*l-3.0)*np.longdouble(l)) - for m in range(1, l-1): + f1[k] = ( + np.sqrt(2.0 * l - 1.0) * np.sqrt(2.0 * l + 1.0) / np.longdouble(l) + ) + f2[k] = ( + np.longdouble(l - 1.0) + * np.sqrt(2.0 * l + 1.0) + / (np.sqrt(2.0 * l - 3.0) * np.longdouble(l)) + ) + for m in range(1, l - 1): k += 1 - f1[k] = np.sqrt(2.0*l+1.0)*np.sqrt(2.0*l-1.0)/(np.sqrt(l+m)*np.sqrt(l-m)) - f2[k] = np.sqrt(2.0*l+1.0)*np.sqrt(l-m-1.0)*np.sqrt(l+m-1.0)/ \ - (np.sqrt(2.0*l-3.0)*np.sqrt(l+m)*np.sqrt(l-m)) + f1[k] = ( + np.sqrt(2.0 * l + 1.0) + * np.sqrt(2.0 * l - 1.0) + / (np.sqrt(l + m) * np.sqrt(l - m)) + ) + f2[k] = ( + np.sqrt(2.0 * l + 1.0) + * np.sqrt(l - m - 1.0) + * np.sqrt(l + m - 1.0) + / (np.sqrt(2.0 * l - 3.0) * np.sqrt(l + m) * np.sqrt(l - m)) + ) k += 2 # u is sine of colatitude (cosine of latitude) so that 0 <= s <= 1 @@ -214,60 +235,62 @@ def plm_holmes(LMAX, x, u[u == 0] = np.finfo(u.dtype).eps # Calculate P(l,0). These are not scaled. - p[0,:] = 1.0 - p[1,:] = np.sqrt(3.0)*x + p[0, :] = 1.0 + p[1, :] = np.sqrt(3.0) * x k = 1 - for l in range(2, LMAX+1): + for l in range(2, LMAX + 1): k += l - p[k,:] = f1[k]*x*p[k-l,:] - f2[k]*p[k-2*l+1,:] + p[k, :] = f1[k] * x * p[k - l, :] - f2[k] * p[k - 2 * l + 1, :] # Calculate P(m,m), P(m+1,m), and P(l,m) - pmm = np.sqrt(2.0)*scalef - rescalem = 1.0/scalef + pmm = np.sqrt(2.0) * scalef + rescalem = 1.0 / scalef kstart = 0 for m in range(1, LMAX): rescalem = rescalem * u # Calculate P(m,m) - kstart += m+1 - pmm = pmm * np.sqrt(2*m+1)/np.sqrt(2*m) - p[kstart,:] = pmm + kstart += m + 1 + pmm = pmm * np.sqrt(2 * m + 1) / np.sqrt(2 * m) + p[kstart, :] = pmm # Calculate P(m+1,m) - k = kstart+m+1 - p[k,:] = x*np.sqrt(2*m+3)*pmm + k = kstart + m + 1 + p[k, :] = x * np.sqrt(2 * m + 3) * pmm # Calculate P(l,m) - for l in range(m+2, LMAX+1): + for l in range(m + 2, LMAX + 1): k += l - p[k,:] = x*f1[k]*p[k-l,:] - f2[k]*p[k-2*l+1,:] - p[k-2*l+1,:] = p[k-2*l+1,:] * rescalem + p[k, :] = x * f1[k] * p[k - l, :] - f2[k] * p[k - 2 * l + 1, :] + p[k - 2 * l + 1, :] = p[k - 2 * l + 1, :] * rescalem # rescale - p[k,:] = p[k,:] * rescalem - p[k-LMAX,:] = p[k-LMAX,:] * rescalem + p[k, :] = p[k, :] * rescalem + p[k - LMAX, :] = p[k - LMAX, :] * rescalem # Calculate P(LMAX,LMAX) rescalem = rescalem * u - kstart += m+2 - p[kstart,:] = pmm * np.sqrt(2*LMAX+1) / np.sqrt(2*LMAX) * rescalem + kstart += m + 2 + p[kstart, :] = pmm * np.sqrt(2 * LMAX + 1) / np.sqrt(2 * LMAX) * rescalem # reshape Legendre polynomials to output dimensions - for m in range(LMAX+1): - for l in range(m,LMAX+1): - lm = (l*(l+1))//2 + m - plm[l,m,:] = p[lm,:] + for m in range(LMAX + 1): + for l in range(m, LMAX + 1): + lm = (l * (l + 1)) // 2 + m + plm[l, m, :] = p[lm, :] # calculate first derivatives - if (l == m): - dplm[l,m,:] = np.longdouble(m)*(x/u)*plm[l,m,:] + if l == m: + dplm[l, m, :] = np.longdouble(m) * (x / u) * plm[l, m, :] else: - flm = np.sqrt(((l**2.0 - m**2.0)*(2.0*l + 1.0))/(2.0*l - 1.0)) - dplm[l,m,:]= (1.0/u)*(l*x*plm[l,m,:] - flm*plm[l-1,m,:]) + flm = np.sqrt( + ((l**2.0 - m**2.0) * (2.0 * l + 1.0)) / (2.0 * l - 1.0) + ) + dplm[l, m, :] = (1.0 / u) * ( + l * x * plm[l, m, :] - flm * plm[l - 1, m, :] + ) # return the legendre polynomials and their first derivative # truncating orders to MMAX - return plm[:,:MMAX+1,:], dplm[:,:MMAX+1,:] + return plm[:, : MMAX + 1, :], dplm[:, : MMAX + 1, :] + -def plm_mohlenkamp(LMAX, x, - MMAX=None, - astype=np.float64 - ): +def plm_mohlenkamp(LMAX, x, MMAX=None, astype=np.float64): """ Computes fully-normalized associated Legendre Polynomials and their first derivative using the recursion relation from :cite:p:`Mohlenkamp:2016vv` @@ -307,54 +330,66 @@ def plm_mohlenkamp(LMAX, x, sx = len(x) # Initialize the output Legendre polynomials - plm = np.zeros((LMAX+1, MMAX+1, sx), dtype=astype) - dplm = np.zeros((LMAX+1, LMAX+1, sx), dtype=astype) + plm = np.zeros((LMAX + 1, MMAX + 1, sx), dtype=astype) + dplm = np.zeros((LMAX + 1, LMAX + 1, sx), dtype=astype) # Jacobi polynomial for the recurrence relation - jlmm = np.zeros((LMAX+1, MMAX+1, sx)) + jlmm = np.zeros((LMAX + 1, MMAX + 1, sx)) # for x=cos(th): u= sin(th) u = np.sqrt(1.0 - x**2) # update where u==0 to eps of data type to prevent invalid divisions u[u == 0] = np.finfo(u.dtype).eps # for all spherical harmonic orders of interest - for mm in range(0,MMAX+1):# equivalent to 0:MMAX + for mm in range(0, MMAX + 1): # equivalent to 0:MMAX # Initialize the recurrence relation # J-1,m,m Term == 0 # J0,m,m Term - if (mm > 0): + if mm > 0: # j ranges from 1 to mm for the product - j = np.arange(0,mm)+1.0 - jlmm[0,mm,:] = np.prod(np.sqrt(1.0 + 1.0/(2.0*j)))/np.sqrt(2.0) - else: # if mm == 0: jlmm = 1/sqrt(2) - jlmm[0,mm,:] = 1.0/np.sqrt(2.0) + j = np.arange(0, mm) + 1.0 + jlmm[0, mm, :] = np.prod(np.sqrt(1.0 + 1.0 / (2.0 * j))) / np.sqrt( + 2.0 + ) + else: # if mm == 0: jlmm = 1/sqrt(2) + jlmm[0, mm, :] = 1.0 / np.sqrt(2.0) # Jk,m,m Terms - for k in range(1, LMAX+1):# computation for SH degrees + for k in range(1, LMAX + 1): # computation for SH degrees # Initialization begins at -1 # this is to make the formula parallel the function written in # Martin Mohlenkamp's Guide to Spherical Harmonics # Jacobi General Terms - if (k == 1):# for degree 1 terms - jlmm[k,mm,:] = 2.0*x * jlmm[k-1,mm,:] * \ - np.sqrt(1.0 + (mm - 0.5)/k) * \ - np.sqrt(1.0 - (mm - 0.5)/(k + 2.0*mm)) - else:# for all other spherical harmonic degrees - jlmm[k,mm,:] = 2.0*x * jlmm[k-1,mm,:] * \ - np.sqrt(1.0 + (mm - 0.5)/k) * \ - np.sqrt(1.0 - (mm - 0.5)/(k + 2.0*mm)) - \ - jlmm[k-2,mm,:] * np.sqrt(1.0 + 4.0/(2.0*k + 2.0*mm - 3.0)) * \ - np.sqrt(1.0 - (1.0/k)) * np.sqrt(1.0 - 1.0/(k + 2.0*mm)) + if k == 1: # for degree 1 terms + jlmm[k, mm, :] = ( + 2.0 + * x + * jlmm[k - 1, mm, :] + * np.sqrt(1.0 + (mm - 0.5) / k) + * np.sqrt(1.0 - (mm - 0.5) / (k + 2.0 * mm)) + ) + else: # for all other spherical harmonic degrees + jlmm[k, mm, :] = 2.0 * x * jlmm[k - 1, mm, :] * np.sqrt( + 1.0 + (mm - 0.5) / k + ) * np.sqrt(1.0 - (mm - 0.5) / (k + 2.0 * mm)) - jlmm[ + k - 2, mm, : + ] * np.sqrt(1.0 + 4.0 / (2.0 * k + 2.0 * mm - 3.0)) * np.sqrt( + 1.0 - (1.0 / k) + ) * np.sqrt(1.0 - 1.0 / (k + 2.0 * mm)) # Normalization is geodesy convention - for l in range(mm,LMAX+1): # equivalent to mm:LMAX - if (mm == 0):# Geodesy normalization (m=0) == sqrt(2)*sin(th)^0 + for l in range(mm, LMAX + 1): # equivalent to mm:LMAX + if mm == 0: # Geodesy normalization (m=0) == sqrt(2)*sin(th)^0 # u^mm term is dropped as u^0 = 1 - plm[l,mm,:] = np.sqrt(2.0)*jlmm[l-mm,mm,:] - else:# Geodesy normalization all others == 2*sin(th)^mm - plm[l,mm,:] = 2.0*(u**mm)*jlmm[l-mm,mm,:] + plm[l, mm, :] = np.sqrt(2.0) * jlmm[l - mm, mm, :] + else: # Geodesy normalization all others == 2*sin(th)^mm + plm[l, mm, :] = 2.0 * (u**mm) * jlmm[l - mm, mm, :] # calculate first derivatives - if (l == mm): - dplm[l,mm,:] = np.longdouble(mm)*(x/u)*plm[l,mm,:] + if l == mm: + dplm[l, mm, :] = np.longdouble(mm) * (x / u) * plm[l, mm, :] else: - flm = np.sqrt(((l**2.0 - mm**2.0)*(2.0*l + 1.0))/(2.0*l - 1.0)) - dplm[l,mm,:]= (1.0/u)*(l*x*plm[l,mm,:] - flm*plm[l-1,mm,:]) + flm = np.sqrt( + ((l**2.0 - mm**2.0) * (2.0 * l + 1.0)) / (2.0 * l - 1.0) + ) + dplm[l, mm, :] = (1.0 / u) * ( + l * x * plm[l, mm, :] - flm * plm[l - 1, mm, :] + ) # return the legendre polynomials and their first derivative return plm, dplm diff --git a/gravity_toolkit/clenshaw_summation.py b/gravity_toolkit/clenshaw_summation.py index c7bbeaff..adecf99b 100644 --- a/gravity_toolkit/clenshaw_summation.py +++ b/gravity_toolkit/clenshaw_summation.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" clenshaw_summation.py -Written by Tyler Sutterley (04/2023) +Written by Tyler Sutterley (07/2026) Calculates the spatial field for a series of spherical harmonics for a sequence of ungridded points @@ -49,6 +49,8 @@ Bollettino di Geodesia e Scienze (1982) UPDATE HISTORY: + Updated 07/2026: use np.einsum for spherical harmonic summations + use np.radians to convert from degrees to radians Updated 04/2023: allow love numbers to be None for custom units case Updated 03/2023: improve typing for variables in docstrings Updated 02/2023: set custom units as top option in if/else statements @@ -65,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` @@ -119,12 +127,12 @@ 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 - th = (90.0 - lat)*np.pi/180.0 - phi = np.squeeze(lon*np.pi/180.0) + th = np.radians(90.0 - lat) + phi = np.squeeze(np.radians(lon)) # calculate cos and sin of colatitudes t = np.cos(th) u = np.sin(th) @@ -133,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): @@ -154,43 +162,34 @@ def clenshaw_summation(clm, slm, lon, lat, else: raise ValueError(f'Unknown units {UNITS}') - # calculate arrays for clenshaw summations over colatitudes - s_m_c = np.zeros((npts,LMAX*2+2)) - for m in range(LMAX, -1, -1): - # convolve harmonics with unit factors and smoothing - s_m_c[:,2*m:2*m+2] = clenshaw_s_m(t, dfactor*wl, m, clm, slm, - LMAX, ASTYPE=ASTYPE, SCALE=SCALE) + # complex spherical harmonics + ylm = clm - 1j * slm + # smooth degree dependent factors + 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)) - # calculate cos(phi) - cos_phi_2 = 2.0*np.cos(phi) - # matrix of cos/sin m*phi summation - cos_m_phi = np.zeros((npts,LMAX+2),dtype=ASTYPE) - sin_m_phi = np.zeros((npts,LMAX+2),dtype=ASTYPE) - # initialize matrix with values at lmax+1 and lmax - cos_m_phi[:,LMAX+1] = np.cos(ASTYPE(LMAX + 1)*phi) - sin_m_phi[:,LMAX+1] = np.sin(ASTYPE(LMAX + 1)*phi) - cos_m_phi[:,LMAX] = np.cos(ASTYPE(LMAX)*phi) - sin_m_phi[:,LMAX] = np.sin(ASTYPE(LMAX)*phi) - # calculate summation for order LMAX - s_m = s_m_c[:,2*LMAX]*cos_m_phi[:,LMAX] + s_m_c[:,2*LMAX+1]*sin_m_phi[:,LMAX] + # initiate summation + s_m = 0.0 # iterate to calculate complete summation - for m in range(LMAX-1, 0, -1): - cos_m_phi[:,m] = cos_phi_2*cos_m_phi[:,m+1] - cos_m_phi[:,m+2] - sin_m_phi[:,m] = cos_phi_2*sin_m_phi[:,m+1] - sin_m_phi[:,m+2] + for m in range(LMAX, 0, -1): # calculate summation for order m - a_m = np.sqrt((2.0*m+3.0)/(2.0*m+2.0)) - s_m = a_m*u*s_m + s_m_c[:,2*m]*cos_m_phi[:,m] + s_m_c[:,2*m+1]*sin_m_phi[:,m] - # calculate spatial field - spatial = np.sqrt(3.0)*u*s_m + s_m_c[:,0] + 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_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 conditioned arrays for Clenshaw summation from the -# fully-normalized associated Legendre's function for an order m -def clenshaw_s_m(t, f, m, clm1, slm1, lmax, - ASTYPE=np.longdouble, - SCALE=1e-280 - ): + +# 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): """ Compute conditioned arrays for Clenshaw summation from the fully-normalized associated Legendre's function for an order m @@ -203,67 +202,89 @@ def clenshaw_s_m(t, f, m, clm1, slm1, lmax, degree dependent factors m: int spherical harmonic order - clm1: np.ndarray - cosine spherical harmonics - slm1: np.ndarray - sine spherical harmonics + Ylm1: np.ndarray + complex form of spherical harmonics lmax: int maximum spherical harmonic degree - ASTYPE: np.dtype, default np.longdouble - floating point precision for calculating Clenshaw summation SCALE: float, default 1e-280 scaling factor to prevent underflow in Clenshaw summation 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,2),dtype=ASTYPE) + cs_m = np.zeros((N), dtype=np.clongdouble) # scaling to prevent overflow - clm = SCALE*clm1.astype(ASTYPE) - slm = SCALE*slm1.astype(ASTYPE) + ylm = SCALE * Ylm1.astype(np.clongdouble) # convert lmax and m to float - lm = ASTYPE(lmax) - mm = ASTYPE(m) - if (m == lmax): - s_m[:,0] = f[lmax]*clm[lmax,lmax] - s_m[:,1] = f[lmax]*slm[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[:,0] = a_lm*f[lmax]*clm[lmax,lmax-1] + f[lmax-1]*clm[lmax-1,lmax-1] - s_m[:,1] = a_lm*f[lmax]*slm[lmax,lmax-1] + f[lmax-1]*slm[lmax-1,lmax-1] - elif ((m <= (lmax-2)) and (m >= 1)): - s_mm_c_pre_2 = f[lmax]*clm[lmax,m] - s_mm_s_pre_2 = f[lmax]*slm[lmax,m] - a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/((lm-mm)*(lm+mm)))*t - s_mm_c_pre_1 = a_lm*s_mm_c_pre_2 + f[lmax-1]*clm[lmax-1,m] - s_mm_s_pre_1 = a_lm*s_mm_s_pre_2 + f[lmax-1]*slm[lmax-1,m] - for l in range(lmax-2, m-1, -1): - ll = ASTYPE(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_c = a_lm * s_mm_c_pre_1 - b_lm * s_mm_c_pre_2 + f[l]*clm[l,m] - s_mm_s = a_lm * s_mm_s_pre_1 - b_lm * s_mm_s_pre_2 + f[l]*slm[l,m] - s_mm_c_pre_2 = np.copy(s_mm_c_pre_1) - s_mm_s_pre_2 = np.copy(s_mm_s_pre_1) - s_mm_c_pre_1 = np.copy(s_mm_c) - s_mm_s_pre_1 = np.copy(s_mm_s) - s_m[:,0] = np.copy(s_mm_c) - s_m[:,1] = np.copy(s_mm_s) - elif (m == 0): - s_mm_c_pre_2 = f[lmax]*clm[lmax,0] - a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/(lm*lm))*t - s_mm_c_pre_1 = a_lm * s_mm_c_pre_2 + f[lmax-1]*clm[lmax-1,0] - for l in range(lmax-2, m-1, -1): - ll = ASTYPE(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_c = a_lm * s_mm_c_pre_1 - b_lm * s_mm_c_pre_2 + f[l]*clm[l,0] - s_mm_c_pre_2 = np.copy(s_mm_c_pre_1) - s_mm_c_pre_1 = np.copy(s_mm_c) - s_m[:,0] = np.copy(s_mm_c) - # return s_m rescaled with scalef - return s_m/SCALE + lm = np.float64(lmax) + mm = np.float64(m) + 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.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) + 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] + ) + s_mm_minus_2 = np.copy(s_mm_minus_1) + s_mm_minus_1 = np.copy(s_mm_l) + cs_m[:] = np.copy(s_mm_l) + # return rescaled cs_m + return cs_m / SCALE diff --git a/gravity_toolkit/degree_amplitude.py b/gravity_toolkit/degree_amplitude.py index 64a8f5da..85e2adc7 100755 --- a/gravity_toolkit/degree_amplitude.py +++ b/gravity_toolkit/degree_amplitude.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" degree_amplitude.py Written Tyler Sutterley (03/2023) @@ -28,14 +28,16 @@ Updated 05/2015: added parameter MMAX for MMAX != LMAX Written 07/2013 """ + import numpy as np + def degree_amplitude( - clm, - slm, - LMAX=None, - MMAX=None, - ): + clm, + slm, + LMAX=None, + MMAX=None, +): """ Calculates the amplitude of each spherical harmonic degree @@ -59,7 +61,7 @@ def degree_amplitude( clm = np.atleast_3d(clm) slm = np.atleast_3d(slm) # check shape - LMp1,MMp1,nt = np.shape(clm) + LMp1, MMp1, nt = np.shape(clm) # upper bound of spherical harmonic degrees if LMAX is None: @@ -69,11 +71,13 @@ def degree_amplitude( MMAX = MMp1 - 1 # allocating for output array - amp = np.zeros((LMAX+1,nt)) - for l in range(LMAX+1): - m = np.arange(0,MMAX+1) + amp = np.zeros((LMAX + 1, nt)) + for l in range(LMAX + 1): + m = np.arange(0, MMAX + 1) # degree amplitude of spherical harmonic degree - amp[l,:] = np.sqrt(np.sum(clm[l,m,:]**2 + slm[l,m,:]**2,axis=0)) + amp[l, :] = np.sqrt( + np.sum(clm[l, m, :] ** 2 + slm[l, m, :] ** 2, axis=0) + ) # return the degree amplitude with singleton dimensions removed return np.squeeze(amp) diff --git a/gravity_toolkit/destripe_harmonics.py b/gravity_toolkit/destripe_harmonics.py index 932c0e07..4e881633 100644 --- a/gravity_toolkit/destripe_harmonics.py +++ b/gravity_toolkit/destripe_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" destripe_harmonics.py Original Fortran program remove_errors.f written by Isabella Velicogna Adapted by Chia-Wei Hsu (05/2018) @@ -56,17 +56,19 @@ Updated 05/2015: added parameter MMAX for MMAX != LMAX Updated 02/2014: generalization for GRACE GUI and other routines """ + import numpy as np + def destripe_harmonics( - clm1, - slm1, - LMIN=2, - LMAX=60, - MMAX=None, - ROUND=True, - NARROW=False, - ): + clm1, + slm1, + LMIN=2, + LMAX=60, + MMAX=None, + ROUND=True, + NARROW=False, +): """ Filters spherical harmonic coefficients for correlated striping errors :cite:p:`Swenson:2006hu` @@ -110,14 +112,14 @@ def destripe_harmonics( # matrix size declarations clmeven = np.zeros((LMAX), dtype=np.float64) slmeven = np.zeros((LMAX), dtype=np.float64) - clmodd = np.zeros((LMAX+1), dtype=np.float64) - slmodd = np.zeros((LMAX+1), dtype=np.float64) - clmsm = np.zeros((LMAX+1, MMAX+1), dtype=np.float64) - slmsm = np.zeros((LMAX+1, MMAX+1), dtype=np.float64) + clmodd = np.zeros((LMAX + 1), dtype=np.float64) + slmodd = np.zeros((LMAX + 1), dtype=np.float64) + clmsm = np.zeros((LMAX + 1, MMAX + 1), dtype=np.float64) + slmsm = np.zeros((LMAX + 1, MMAX + 1), dtype=np.float64) # start of the smoothing over orders (m) - for m in range(int(MMAX+1)): - smooth = np.exp(-np.float64(m)/10.0)*15.0 + for m in range(int(MMAX + 1)): + smooth = np.exp(-np.float64(m) / 10.0) * 15.0 if ROUND: # round(smooth) to nearest even instead of int(smooth) nsmooth = np.around(smooth) @@ -125,28 +127,28 @@ def destripe_harmonics( # Sean's method for finding nsmooth (use floor of smooth) nsmooth = np.int64(smooth) - if (nsmooth < 2): + if nsmooth < 2: # Isabella's method of picking nsmooth sets minimum to 2 nsmooth = np.int64(2) - rmat = np.zeros((3,3), dtype=np.float64) - lll = np.arange(np.float64(nsmooth)*2.+1.)-np.float64(nsmooth) + rmat = np.zeros((3, 3), dtype=np.float64) + lll = np.arange(np.float64(nsmooth) * 2.0 + 1.0) - np.float64(nsmooth) # create design matrix to have the following form: # [ 1 ll ll^2 ] # [ ll ll^2 ll^3 ] # [ ll^2 ll^3 ll^4 ] - for i,ill in enumerate(lll): - rmat[0,0] += 1.0 - rmat[0,1] += ill - rmat[0,2] += ill**2 + for i, ill in enumerate(lll): + rmat[0, 0] += 1.0 + rmat[0, 1] += ill + rmat[0, 2] += ill**2 - rmat[1,0] += ill - rmat[1,1] += ill**2 - rmat[1,2] += ill**3 + rmat[1, 0] += ill + rmat[1, 1] += ill**2 + rmat[1, 2] += ill**3 - rmat[2,0] += ill**2 - rmat[2,1] += ill**3 - rmat[2,2] += ill**4 + rmat[2, 0] += ill**2 + rmat[2, 1] += ill**3 + rmat[2, 2] += ill**4 # put the even and odd l's into their own arrays ieven = -1 @@ -154,133 +156,157 @@ def destripe_harmonics( leven = np.zeros((LMAX), dtype=np.int64) lodd = np.zeros((LMAX), dtype=np.int64) - for l in range(int(m),int(LMAX+1)): + for l in range(int(m), int(LMAX + 1)): # check if degree is odd or even - if np.remainder(l,2).astype(bool): + if np.remainder(l, 2).astype(bool): iodd += 1 lodd[iodd] = l - clmodd[iodd] = clm1[l,m].copy() - slmodd[iodd] = slm1[l,m].copy() + clmodd[iodd] = clm1[l, m].copy() + slmodd[iodd] = slm1[l, m].copy() else: ieven += 1 leven[ieven] = l - clmeven[ieven] = clm1[l,m].copy() - slmeven[ieven] = slm1[l,m].copy() + clmeven[ieven] = clm1[l, m].copy() + slmeven[ieven] = slm1[l, m].copy() # smooth, by fitting a quadratic polynomial to 7 points at a time # deal with even stokes coefficients l1 = 0 l2 = ieven - if (l1 > (l2-2*nsmooth)): - for l in range(l1,l2+1): + if l1 > (l2 - 2 * nsmooth): + for l in range(l1, l2 + 1): if NARROW: # Sean's method # Clm=Slm=0 if number of points is less than window size - clmsm[leven[l],m] = 0.0 - slmsm[leven[l],m] = 0.0 + clmsm[leven[l], m] = 0.0 + slmsm[leven[l], m] = 0.0 else: # Isabella's method # Clm and Slm passed through unaltered - clmsm[leven[l],m] = clm1[leven[l],m].copy() - slmsm[leven[l],m] = slm1[leven[l],m].copy() + clmsm[leven[l], m] = clm1[leven[l], m].copy() + slmsm[leven[l], m] = slm1[leven[l], m].copy() else: - for l in range(int(l1+nsmooth),int(l2-nsmooth+1)): + for l in range(int(l1 + nsmooth), int(l2 - nsmooth + 1)): rhsc = np.zeros((3), dtype=np.float64) rhss = np.zeros((3), dtype=np.float64) - for ll in range(int(-nsmooth),int(nsmooth+1)): - rhsc[0] += clmeven[l+ll] - rhsc[1] += clmeven[l+ll]*np.float64(ll) - rhsc[2] += clmeven[l+ll]*np.float64(ll**2) - rhss[0] += slmeven[l+ll] - rhss[1] += slmeven[l+ll]*np.float64(ll) - rhss[2] += slmeven[l+ll]*np.float64(ll**2) + for ll in range(int(-nsmooth), int(nsmooth + 1)): + rhsc[0] += clmeven[l + ll] + rhsc[1] += clmeven[l + ll] * np.float64(ll) + rhsc[2] += clmeven[l + ll] * np.float64(ll**2) + rhss[0] += slmeven[l + ll] + rhss[1] += slmeven[l + ll] * np.float64(ll) + rhss[2] += slmeven[l + ll] * np.float64(ll**2) # fit design matrix to coefficients # to get beta parameters - bhsc = np.linalg.lstsq(rmat,rhsc.T,rcond=-1)[0] - bhss = np.linalg.lstsq(rmat,rhss.T,rcond=-1)[0] + bhsc = np.linalg.lstsq(rmat, rhsc.T, rcond=-1)[0] + bhss = np.linalg.lstsq(rmat, rhss.T, rcond=-1)[0] # all other l is assigned as bhsc - clmsm[leven[l],m] = bhsc[0].copy() + clmsm[leven[l], m] = bhsc[0].copy() # all other l is assigned as bhss - slmsm[leven[l],m] = bhss[0].copy() + slmsm[leven[l], m] = bhss[0].copy() - if (l == (l1+nsmooth)): + if l == (l1 + nsmooth): # deal with l=l1+nsmooth - for ll in range(int(-nsmooth),0): - clmsm[leven[l+ll],m] = bhsc[0]+bhsc[1]*np.float64(ll) + \ - bhsc[2]*np.float64(ll**2) - slmsm[leven[l+ll],m] = bhss[0]+bhss[1]*np.float64(ll) + \ - bhss[2]*np.float64(ll**2) - - if (l == (l2-nsmooth)): + for ll in range(int(-nsmooth), 0): + clmsm[leven[l + ll], m] = ( + bhsc[0] + + bhsc[1] * np.float64(ll) + + bhsc[2] * np.float64(ll**2) + ) + slmsm[leven[l + ll], m] = ( + bhss[0] + + bhss[1] * np.float64(ll) + + bhss[2] * np.float64(ll**2) + ) + + if l == (l2 - nsmooth): # deal with l=l2-nsmnooth - for ll in range(1,int(nsmooth+1)): - clmsm[leven[l+ll],m] = bhsc[0]+bhsc[1]*np.float64(ll) + \ - bhsc[2]*np.float64(ll**2) - slmsm[leven[l+ll],m] = bhss[0]+bhss[1]*np.float64(ll) + \ - bhss[2]*np.float64(ll**2) + for ll in range(1, int(nsmooth + 1)): + clmsm[leven[l + ll], m] = ( + bhsc[0] + + bhsc[1] * np.float64(ll) + + bhsc[2] * np.float64(ll**2) + ) + slmsm[leven[l + ll], m] = ( + bhss[0] + + bhss[1] * np.float64(ll) + + bhss[2] * np.float64(ll**2) + ) # deal with odd stokes coefficients l1 = 0 l2 = iodd - if (l1 > (l2-2*nsmooth)): - for l in range(l1,l2+1): + if l1 > (l2 - 2 * nsmooth): + for l in range(l1, l2 + 1): if NARROW: # Sean's method # Clm=Slm=0 if number of points is less than window size - clmsm[lodd[l],m] = 0.0 - slmsm[lodd[l],m] = 0.0 + clmsm[lodd[l], m] = 0.0 + slmsm[lodd[l], m] = 0.0 else: # Isabella's method # Clm and Slm passed through unaltered - clmsm[lodd[l],m] = clm1[lodd[l],m].copy() - slmsm[lodd[l],m] = slm1[lodd[l],m].copy() + clmsm[lodd[l], m] = clm1[lodd[l], m].copy() + slmsm[lodd[l], m] = slm1[lodd[l], m].copy() else: - for l in range(int(l1+nsmooth),int(l2-nsmooth+1)): + for l in range(int(l1 + nsmooth), int(l2 - nsmooth + 1)): rhsc = np.zeros((3), dtype=np.float64) rhss = np.zeros((3), dtype=np.float64) - for ll in range(int(-nsmooth),int(nsmooth+1)): - rhsc[0] += clmodd[l+ll] - rhsc[1] += clmodd[l+ll]*np.float64(ll) - rhsc[2] += clmodd[l+ll]*np.float64(ll**2) - rhss[0] += slmodd[l+ll] - rhss[1] += slmodd[l+ll]*np.float64(ll) - rhss[2] += slmodd[l+ll]*np.float64(ll**2) + for ll in range(int(-nsmooth), int(nsmooth + 1)): + rhsc[0] += clmodd[l + ll] + rhsc[1] += clmodd[l + ll] * np.float64(ll) + rhsc[2] += clmodd[l + ll] * np.float64(ll**2) + rhss[0] += slmodd[l + ll] + rhss[1] += slmodd[l + ll] * np.float64(ll) + rhss[2] += slmodd[l + ll] * np.float64(ll**2) # fit design matrix to coefficients # to get beta parameters - bhsc = np.linalg.lstsq(rmat,rhsc.T,rcond=-1)[0] - bhss = np.linalg.lstsq(rmat,rhss.T,rcond=-1)[0] + bhsc = np.linalg.lstsq(rmat, rhsc.T, rcond=-1)[0] + bhss = np.linalg.lstsq(rmat, rhss.T, rcond=-1)[0] # all other l is assigned as bhsc - clmsm[lodd[l],m] = bhsc[0].copy() + clmsm[lodd[l], m] = bhsc[0].copy() # all other l is assigned as bhss - slmsm[lodd[l],m] = bhss[0].copy() + slmsm[lodd[l], m] = bhss[0].copy() - if (l == (l1+nsmooth)): + if l == (l1 + nsmooth): # deal with l=l1+nsmooth - for ll in range(int(-nsmooth),0): - clmsm[lodd[l+ll],m] = bhsc[0]+bhsc[1]*np.float64(ll) + \ - bhsc[2]*np.float64(ll**2) - slmsm[lodd[l+ll],m] = bhss[0]+bhss[1]*np.float64(ll) + \ - bhss[2]*np.float64(ll**2) - - if (l == (l2-nsmooth)): + for ll in range(int(-nsmooth), 0): + clmsm[lodd[l + ll], m] = ( + bhsc[0] + + bhsc[1] * np.float64(ll) + + bhsc[2] * np.float64(ll**2) + ) + slmsm[lodd[l + ll], m] = ( + bhss[0] + + bhss[1] * np.float64(ll) + + bhss[2] * np.float64(ll**2) + ) + + if l == (l2 - nsmooth): # deal with l=l2-nsmnooth - for ll in range(1,int(nsmooth+1)): - clmsm[lodd[l+ll],m] = bhsc[0]+bhsc[1]*np.float64(ll) + \ - bhsc[2]*np.float64(ll**2) - slmsm[lodd[l+ll],m] = bhss[0]+bhss[1]*np.float64(ll) + \ - bhss[2]*np.float64(ll**2) + for ll in range(1, int(nsmooth + 1)): + clmsm[lodd[l + ll], m] = ( + bhsc[0] + + bhsc[1] * np.float64(ll) + + bhsc[2] * np.float64(ll**2) + ) + slmsm[lodd[l + ll], m] = ( + bhss[0] + + bhss[1] * np.float64(ll) + + bhss[2] * np.float64(ll**2) + ) # deal with m greater than or equal to 5 - for l in range(int(m),int(LMAX+1)): - if (m >= 5): + for l in range(int(m), int(LMAX + 1)): + if m >= 5: # remove smoothed clm/slm from original spherical harmonics - Wclm[l,m] -= clmsm[l,m] - Wslm[l,m] -= slmsm[l,m] + Wclm[l, m] -= clmsm[l, m] + Wslm[l, m] -= slmsm[l, m] - return {'clm':Wclm,'slm':Wslm} + return {'clm': Wclm, 'slm': Wslm} diff --git a/gravity_toolkit/fourier_legendre.py b/gravity_toolkit/fourier_legendre.py index 6cc8b5cd..521b2cf9 100755 --- a/gravity_toolkit/fourier_legendre.py +++ b/gravity_toolkit/fourier_legendre.py @@ -1,25 +1,26 @@ #!/usr/bin/env python -u""" +""" fourier_legendre.py Original IDL code gen_plms.pro written by Sean Swenson -Adapted by Tyler Sutterley (03/2023) +Adapted by Tyler Sutterley (07/2026) Computes Fourier coefficients of the associated Legendre functions CALLING SEQUENCE: - plm = fourier_legendre(lmax,mmax) + Almk = fourier_legendre(lmax,mmax) INPUTS: lmax: maximum spherical harmonic degree mmax: maximum spherical harmonic order OUTPUTS: - plm: Fourier coefficients + Almk: Fourier coefficients PYTHON DEPENDENCIES: numpy: Scientific Computing Tools For Python (https://numpy.org) UPDATE HISTORY: + Updated 07/2027: add citations to docstrings Updated 03/2023: improve typing for variables in docstrings Updated 10/2022: add polynomial function for calculating gradients Updated 04/2022: updated docstrings to numpy documentation format @@ -28,12 +29,15 @@ 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 + :cite:p:`Hofsommer:1960wg,Gruber:2016hn` Parameters ---------- @@ -44,172 +48,258 @@ def fourier_legendre(lmax, mmax): Returns ------- - plm: np.ndarray + Almk: np.ndarray Fourier coefficients """ # allocate for output fourier coefficients - plm = 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 - plm[l_even,0,0] = 1.0 - p1 = (l_even*(l_even+1.0))*plm[l_even,0,0] - plm[l_even,0,2] = p1 / (l_even*(l_even+1.0)-2.0) - for j in range(2,lmax,2):# equivalent to 2:lmax-2 - p1 = 2.0*(l_even*(l_even+1.0)-j**2.0)*plm[l_even,0,j] - p2 = ((j-2.0)*(j-1.0)-l_even*(l_even+1.0))*plm[l_even,0,j-2] - dfactor = (l_even*(l_even+1.0)-(j+2.0)*(j+1.0)) - plm[l_even,0,j+2] = (p1 + p2) / 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 - plm[l_even,0,0] = plm[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(plm[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] + plm[l_even,0,j] * \ - np.dot(ptemp, dtemp)/2.0 - # normalize plms - norm = np.sqrt(norm/2.0) - for l in range(0,lmax+2,2):# equivalent to 0:lmax - plm[l,0,:] = plm[l,0,:]/norm[l//2] - + 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] # Compute m = 0, l = odd terms - plm[l_odd,0,1] = 1.0 - p1 = (2.0-l_odd*(l_odd+1.0))*plm[l_odd,0,1] - plm[l_odd,0,3] = p1 / (6.0-l_odd*(l_odd+1.0)) - for j in range(3,lmax-1,2):# equivalent to 3:lmax-3 - p1 = 2.0*(l_odd*(l_odd+1.0)-j**2.0)*plm[l_odd,0,j] - p2 = ((j-2.0)*(j-1.0)-l_odd*(l_odd+1.0))*plm[l_odd,0,j-2] - dfactor = (l_odd*(l_odd+1.0)-(j+2.0)*(j+1.0)) - plm[l_odd,0,j+2] = (p1 + p2) / 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(plm[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] + plm[l_odd,0,j] * \ - np.dot(ptemp, dtemp)/2.0 - # normalize plms - norm = np.sqrt(norm/2.0) - for l in range(1,lmax+1,2):# equivalent to 1:lmax-1 - plm[l,0,:] = plm[l,0,:]/norm[(l-1)//2] - + 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] # Compute m = 1, l = even terms - plm[l_even,1,0] = 0.0 - plm[l_even,1,2] = 1.0 - for j in range(2,lmax,2):# equivalent to 2:lmax-2 - p1 = 2.0*(l_even*(l_even+1)-j**2.0-2.0)*plm[l_even,1,j] - p2 = ((j-2.0)*(j-1.0)-l_even*(l_even+1))*plm[l_even,1,j-2] - dfactor = (l_even*(l_even+1.0)-(j+2.0)*(j+1.0)) - plm[l_even,1,j+2] = (p1 + p2) / 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(plm[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] + plm[l_even,1,j] * \ - np.dot(ptemp, dtemp)/2.0 - # normalize plms - norm = np.sqrt(norm/4.0) - for l in range(0,lmax+2,2):# equivalent to 0:lmax - plm[l,1,:] = plm[l,1,:]/norm[l//2] + 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] # Compute m = 1, l = odd terms - plm[l_odd,1,1] = 1.0 - plm[l_odd,1,3] = 3.0*(l_odd*(l_odd+1)-2)*plm[l_odd,1,1]/(l_odd*(l_odd+1)-6) - for j in range(3,lmax-1,2):# equivalent to 3:lmax-3 - p1 = 2.0*(l_odd*(l_odd+1.0)-j**2.0-2.0)*plm[l_odd,1,j] - p2 = ((j-2.0)*(j-1.0)-l_odd*(l_odd+1.0))*plm[l_odd,1,j-2] - dfactor = (l_odd*(l_odd+1.0)-(j+2.0)*(j+1.0)) - plm[l_odd,1,j+2] = (p1 + p2) / 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(plm[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] + plm[l_odd,1,j] * \ - np.dot(ptemp, dtemp)/2.0 - # normalize plms - norm = np.sqrt(norm/4.0) - for l in range(1,lmax+1,2):# equivalent to 1:lmax-1 - plm[l,1,:] = plm[l,1,:]/norm[(l-1)//2] - + 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] # 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 - p1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*plm[l,m,m_even] - p2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*plm[l+2,m,m_even] - p3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0)/2.0)*plm[l,m+2,m_even] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)/2.0) - plm[l+2,m+2,m_even] = (p1 - p2 + p3) / 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 - p1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*plm[l,m,m_odd] - p2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*plm[l+2,m,m_odd] - p3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0)/2.0)*plm[l,m+2,m_odd] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)/2.0) - plm[l+2,m+2,m_odd] = (p1 - p2 + p3) / 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 - p1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*plm[l,m,m_even] - p2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*plm[l+2,m,m_even] - p3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0))*plm[l,m+2,m_even] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)) - plm[l+2,m+2,m_even] = (p1 - p2 + p3) / 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): - p1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*plm[l,m,m_odd] - p2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*plm[l+2,m,m_odd] - p3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0))*plm[l,m+2,m_odd] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)) - plm[l+2,m+2,m_odd] = (p1 - p2 + p3) / 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 - p1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*plm[l,m,m_even] - p2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*plm[l+2,m,m_even] - p3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0))*plm[l,m+2,m_even] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)) - plm[l+2,m+2,m_even] = (p1 - p2 + p3) / 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 - p1 = np.sqrt((l+m+2.0)*(l+m+1.0)/(2.0*l+1.0))*plm[l,m,m_odd] - p2 = np.sqrt((l-m+1.0)*(l-m+2.0)/(2.0*l+5.0))*plm[l+2,m,m_odd] - p3 = np.sqrt((l-m)*(l-m-1.0)/(2.0*l+1.0))*plm[l,m+2,m_odd] - dfactor = np.sqrt((l+m+4.0)*(l+m+3.0)/(2.0*l+5.0)) - plm[l+2,m+2,m_odd] = (p1 - p2 + p3) / 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 plm + return Almk + def legendre_gradient(lmax, mmax): """ @@ -225,67 +315,40 @@ def legendre_gradient(lmax, mmax): Returns ------- - vlm: np.ndarray + Vlmk: np.ndarray Fourier coefficients for meridional gradients - wlm: np.ndarray + Wlmk: np.ndarray Fourier coefficients for zonal gradients """ - - plm = fourier_legendre(lmax, mmax) - vlm = np.zeros((lmax+1,lmax+1,lmax+1)) - wlm = np.zeros((lmax+1,lmax+1,lmax+1)) - - # l=0 zero by definition - lind = np.arange(1,lmax+1) - # m=0 special case - # terms with m=0, m=1 have different coefficients - vlm[lind,0,:] = 2.0*np.dot(np.diag(np.sqrt((lind+1)*lind/2.0)), plm[lind,1,:]) - - # m+1 terms - for l in range(2,lmax+1):# from 2 to lmax - m = np.arange(1,l)# from 1 to l-1 - lplus = np.arange(l+2,2*l+1)# from l+2 to 2*l - lminus = np.arange(l-1,0,-1)# from l-1 to 1 - vlm[l,m,:] = np.dot(np.diag(np.sqrt(lplus*lminus/4.0)), plm[l,m+1,:]) - - # m-1 terms, m-1=0 has different coefficients - vlm[lind,1,:] -= np.dot(np.diag(np.sqrt((lind+1)*lind/2.0)), plm[lind,0,:]) - - for l in range(2,lmax+1): - m = np.arange(2,l+1)# from 2 to l - lplus = np.arange(l+2,2*l+1)# from l+2 to 2*l - lminus = np.arange(l-1,0,-1)# from l-1 to 1 - vlm[l,m,:] -= np.dot(np.diag(np.sqrt(lplus*lminus/4.0)), plm[l,m-1,:]) - # normalizations - for l in range(1,lmax+1): - vlm[l,:,:] /= np.sqrt((l+1)*l) - - # m+1 terms - for l in range(2, lmax+1): - m = np.arange(1,l)# from 1 to l-1 - dfactor = (2.0*l+1.0)/(2.0*l-1.0) - lminus2 = np.arange(l-2,-1,-1)# from l-2 to 0 - lminus1 = np.arange(l-1,0,-1)# from l-1 to 1 - wlm[l,m,:] = np.sqrt(dfactor) * \ - np.dot(np.diag(np.sqrt(lminus2*lminus1/4.0)), plm[l,m+1,:]) - - # m-1 terms, m-1=0 has different coefficients - # m=1 term - for l in range(1, lmax+1): - dfactor = (2.0*l+1.0)/(2.0*l-1.0) - wlm[l,1,:] += np.sqrt(dfactor)*np.sqrt(l*(l+1)/2.0)*plm[l-1,0,:] - - for l in range(2,lmax+1): - m = np.arange(2,l+1) - dfactor = (2.0*l+1.0)/(2.0*l-1.0) - lplus2 = np.arange(l+2,2*l+1)# from l+2 to 2*l - lplus1 = np.arange(l+1,2*l)# from l+1 to (2*l-1) - wlm[l,m,:] += np.sqrt(dfactor) * \ - np.dot(np.diag(np.sqrt(lplus2*lplus1)/4.0), plm[l-1,m-1,:]) - # normalizations - for l in range(1, lmax+1): - wlm[l,:,:] /= np.sqrt((l+1)*l) - # normalize vlm - vlm[:,0,:] /= 2.0 - - return (vlm, wlm) + # 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)) + # for each spherical harmonic degree + for l in range(1, lmax + 1): + # degree dependent factor + 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 + 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, :] + # 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 + 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, :] + # normalizations + Vlmk[l, :, :] /= np.sqrt(l * (l + 1.0)) + Wlmk[l, :, :] /= np.sqrt(l * (l + 1.0)) + # return the coefficients + return (Vlmk, Wlmk) diff --git a/gravity_toolkit/gauss_weights.py b/gravity_toolkit/gauss_weights.py index 29849fba..1649e24f 100755 --- a/gravity_toolkit/gauss_weights.py +++ b/gravity_toolkit/gauss_weights.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gauss_weights.py Original IDL code gauss_weights.pro written by Sean Swenson Adapted by Tyler Sutterley (03/2023) @@ -47,8 +47,10 @@ Updated 02/2014: changed variables from ints to floats to prevent truncation Written 05/2013 """ + import numpy as np + def gauss_weights(hw, LMAX, CUTOFF=1e-10): """ Computes the Gaussian weights as a function of degree using @@ -69,32 +71,34 @@ def gauss_weights(hw, LMAX, CUTOFF=1e-10): degree dependent weighting function """ # allocate for output weights - wl = np.zeros((LMAX+1)) + wl = np.zeros((LMAX + 1)) # radius of the Earth in km rad_e = 6371.0 - if (hw < CUTOFF): + if hw < CUTOFF: # distance is smaller than cutoff - wl[:] = 1.0/(2.0*np.pi) + wl[:] = 1.0 / (2.0 * np.pi) else: # calculate gaussian weights using recursion - b = np.log(2.0)/(1.0 - np.cos(hw/rad_e)) + b = np.log(2.0) / (1.0 - np.cos(hw / rad_e)) # weight for degree 0 - wl[0] = 1.0/(2.0*np.pi) + wl[0] = 1.0 / (2.0 * np.pi) # weight for degree 1 - wl[1] = wl[0]*((1.0+np.exp(-2.0*b))/(1.0-np.exp(-2.0*b))-1.0/b) + wl[1] = wl[0] * ( + (1.0 + np.exp(-2.0 * b)) / (1.0 - np.exp(-2.0 * b)) - 1.0 / b + ) # valid flag valid = True # spherical harmonic degree l = 2 # while valid (within cutoff) # and spherical harmonic degree is less than LMAX - while (valid and (l <= LMAX)): + while valid and (l <= LMAX): # calculate weight with recursion - wl[l] = (1.0-2.0*l)/b*wl[l-1]+wl[l-2] + wl[l] = (1.0 - 2.0 * l) / b * wl[l - 1] + wl[l - 2] # weight is less than cutoff - if (wl[l] < CUTOFF): + if wl[l] < CUTOFF: # set all weights to cutoff - wl[l:LMAX+1] = CUTOFF + wl[l : LMAX + 1] = CUTOFF # set valid flag valid = False # add 1 to l diff --git a/gravity_toolkit/gen_averaging_kernel.py b/gravity_toolkit/gen_averaging_kernel.py index b291906b..41e6aee8 100755 --- a/gravity_toolkit/gen_averaging_kernel.py +++ b/gravity_toolkit/gen_averaging_kernel.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" gen_averaging_kernel.py Original IDL code gen_wclms_me.pro written by Sean Swenson Adapted by Tyler Sutterley (06/2023) @@ -54,11 +54,24 @@ Updated 05/2015: added parameter MMAX for MMAX != LMAX Written 05/2013 """ + import numpy as np import gravity_toolkit.units -def gen_averaging_kernel(gclm, gslm, eclm, eslm, sigma, hw, - LMAX=60, MMAX=None, CUTOFF=1e-15, UNITS=0, LOVE=None): + +def gen_averaging_kernel( + gclm, + gslm, + eclm, + eslm, + sigma, + hw, + LMAX=60, + MMAX=None, + CUTOFF=1e-15, + UNITS=0, + LOVE=None, +): r""" Generates averaging kernel coefficients which minimize the total error following :cite:t:`Swenson:2002hs` @@ -108,65 +121,65 @@ def gen_averaging_kernel(gclm, gslm, eclm, eslm, sigma, hw, # Earth Parameters factors = gravity_toolkit.units(lmax=LMAX) # extract arrays of kl, hl, and ll Love Numbers - if (UNITS == 0): + if UNITS == 0: # Input coefficients are fully-normalized dfactor = factors.harmonic(*LOVE).cmwe - elif (UNITS == 1): + elif UNITS == 1: # Inputs coefficients are mass (cmwe) - dfactor = np.ones((LMAX+1)) + dfactor = np.ones((LMAX + 1)) # average radius of the earth (km) - rad_e = factors.rad_e/1e5 + rad_e = factors.rad_e / 1e5 # allocate for gaussian function - gl = np.zeros((LMAX+1)) + gl = np.zeros((LMAX + 1)) # calculate gaussian weights using recursion - b = np.log(2.0)/(1.0-np.cos(hw/rad_e)) + b = np.log(2.0) / (1.0 - np.cos(hw / rad_e)) # weight for degree 0 - gl[0] = (1.0-np.exp(-2.0*b))/b + gl[0] = (1.0 - np.exp(-2.0 * b)) / b # weight for degree 1 - gl[1] = (1.0+np.exp(-2.0*b))/b - (1.0-np.exp(-2.0*b))/b**2 + gl[1] = (1.0 + np.exp(-2.0 * b)) / b - (1.0 - np.exp(-2.0 * b)) / b**2 # valid flag valid = True # spherical harmonic degree l = 2 # generate Legendre coefficients of Gaussian correlation function - while (valid and (l <= LMAX)): - gl[l] = (1.0 - 2.0*l)/b*gl[l-1] + gl[l-2] + while valid and (l <= LMAX): + gl[l] = (1.0 - 2.0 * l) / b * gl[l - 1] + gl[l - 2] # check validity - if (gl[l] < CUTOFF): - gl[l:LMAX+1] = CUTOFF + if gl[l] < CUTOFF: + gl[l : LMAX + 1] = CUTOFF valid = False # add to counter for spherical harmonic degree l += 1 # Convert sigma to correlation function amplitude - area = np.copy(gclm[0,0]) - temp_0 = np.zeros((LMAX+1)) - for l in range(0,LMAX+1):# equivalent to 0:LMAX - mm = np.min([MMAX,l])# find min of MMAX and l - m = np.arange(0,mm+1)# create m array 0:l or 0:MMAX - temp_0[l] = (gl[l]/2.0)*np.sum(gclm[l,m]**2 + gslm[l,m]**2) + area = np.copy(gclm[0, 0]) + temp_0 = np.zeros((LMAX + 1)) + for l in range(0, LMAX + 1): # equivalent to 0:LMAX + mm = np.min([MMAX, l]) # find min of MMAX and l + m = np.arange(0, mm + 1) # create m array 0:l or 0:MMAX + temp_0[l] = (gl[l] / 2.0) * np.sum(gclm[l, m] ** 2 + gslm[l, m] ** 2) # divide by the square of the area under the kernel - temp = np.sum(temp_0)/area**2 + temp = np.sum(temp_0) / area**2 # signal variance - sigma_0 = sigma/np.sqrt(temp) + sigma_0 = sigma / np.sqrt(temp) # Compute averaging kernel coefficients Ylms = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX) - Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) + Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) # for each spherical harmonic degree - for l in range(0,LMAX+1):# equivalent to 0:lmax + for l in range(0, LMAX + 1): # equivalent to 0:lmax # inverse of smoothed signal variance in output units - ldivg = (dfactor[l]**2)/(gl[l]*sigma_0**2) + ldivg = (dfactor[l] ** 2) / (gl[l] * sigma_0**2) # for each valid spherical harmonic order - mm = np.min([MMAX,l]) - for m in range(0,mm+1): - temp = 1.0 + 2.0*ldivg*eclm[l,m]**2 - Ylms.clm[l,m] = gclm[l,m]/temp - temp = 1.0 + 2.0*ldivg*eslm[l,m]**2 - Ylms.slm[l,m] = gslm[l,m]/temp + mm = np.min([MMAX, l]) + for m in range(0, mm + 1): + temp = 1.0 + 2.0 * ldivg * eclm[l, m] ** 2 + Ylms.clm[l, m] = gclm[l, m] / temp + temp = 1.0 + 2.0 * ldivg * eslm[l, m] ** 2 + Ylms.slm[l, m] = gslm[l, m] / temp # return kernels divided by the area under the kernel - return Ylms.scale(1.0/area) + return Ylms.scale(1.0 / area) diff --git a/gravity_toolkit/gen_disc_load.py b/gravity_toolkit/gen_disc_load.py index 474b1b3f..92e6fc8f 100644 --- a/gravity_toolkit/gen_disc_load.py +++ b/gravity_toolkit/gen_disc_load.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" gen_disc_load.py -Written by Tyler Sutterley (06/2023) +Written by Tyler Sutterley (07/2026) Calculates gravitational spherical harmonic coefficients for a uniform disc load CALLING SEQUENCE: @@ -55,6 +55,8 @@ https://doi.org/10.1007/s00190-011-0522-7 UPDATE HISTORY: + Updated 07/2026: use np.einsum for spherical harmonic summations + use np.radians to convert from degrees to radians Updated 06/2023: modified custom units case to not convert to cmwe Updated 03/2023: simplified unit degree factors using units class improve typing for variables in docstrings @@ -74,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` @@ -129,104 +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 = lon*np.pi/180.0# Longitude in radians - th = (90.0 - lat)*np.pi/180.0# 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 - dcos = unit_conv*data*np.cos(m*phi) - dsin = unit_conv*data*np.sin(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)) - # Initializing preliminary spherical harmonic matrices - yclm = np.zeros((LMAX+1, MMAX+1)) - yslm = 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) - Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - Ylms.slm = np.zeros((LMAX+1, MMAX+1)) - for m in range(0,MMAX+1):# MMAX+1 to include MMAX - l = np.arange(m,LMAX+1)# LMAX+1 to include LMAX - # rotate disc load to be centered at lat/lon - plm[l,m] = plmout[l,m]*pl_alpha[l] - # multiplying clm by cos(m*phi) and slm by sin(m*phi) - # to get a field of spherical harmonics - yclm[l,m] = plm[l,m]*dcos[m] - yslm[l,m] = plm[l,m]*dsin[m] - # multiplying by factors to convert to geoid coefficients - Ylms.clm[l,m] = dfactor[l]*yclm[l,m] - Ylms.slm[l,m] = dfactor[l]*yslm[l,m] + # rotate disc load to be centered at lat/lon + 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) + # 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) # return the output spherical harmonics object return Ylms diff --git a/gravity_toolkit/gen_harmonics.py b/gravity_toolkit/gen_harmonics.py index 8044f695..07b14fb0 100644 --- a/gravity_toolkit/gen_harmonics.py +++ b/gravity_toolkit/gen_harmonics.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" gen_harmonics.py -Written by Tyler Sutterley (03/2023) +Written by Tyler Sutterley (07/2026) Converts data from the spatial domain to spherical harmonic coefficients Does not compute the solid Earth elastic response or convert units @@ -47,6 +47,8 @@ Associated Legendre Functions", Journal of Geodesy (2002) UPDATE HISTORY: + Updated 07/2026: use np.einsum for spherical harmonic summations + use np.radians to convert from degrees to radians Updated 03/2023: improve typing for variables in docstrings Updated 01/2023: refactored associated legendre polynomials Updated 04/2022: updated docstrings to numpy documentation format @@ -61,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 @@ -103,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']) @@ -117,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 @@ -154,75 +159,51 @@ def integration(data, lon, lat, LMAX=60, MMAX=None, PLM=0, **kwargs): m: int spherical harmonic order to MMAX """ - - # dimensions of the longitude and latitude arrays - nlon = np.int64(len(lon)) - nlat = np.int64(len(lat)) - # grid step - dlon = np.abs(lon[1]-lon[0]) - dlat = np.abs(lat[1]-lat[0]) - # longitude degree spacing in radians - dphi = dlon*np.pi/180.0 - # colatitude degree spacing in radians - dth = dlat*np.pi/180.0 - - # reformatting longitudes to range 0:360 (if previously -180:180) - if np.count_nonzero(lon < 0): - lon[lon < 0] += 360.0 # calculate longitude and colatitude arrays in radians - phi = np.reshape(lon,(1,nlon))*np.pi/180.0# reshape to 1xnlon - th = (90.0 - np.squeeze(lat))*np.pi/180.0# remove singleton dimensions + 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) + # grid step in radians + dphi = np.abs(phi[1] - phi[0]) + dth = np.abs(th[1] - th[0]) - # Calculating cos/sin of phi arrays (output [m,phi]) # LMAX+1 as there are LMAX+1 elements between 0 and LMAX - m = np.arange(MMAX+1)[:, np.newaxis] - ccos = np.cos(np.dot(m,phi)) - ssin = np.sin(np.dot(m,phi)) + 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)) # 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 - plm = np.zeros((LMAX+1, MMAX+1, nlat)) - if (np.ndim(PLM) == 0): - plmout,dplm = plm_holmes(LMAX, np.cos(th)) - else: - # use precomputed plms to improve computational speed - # or to use a different recursion relation for polynomials - plmout = PLM - + 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 - m = np.arange(MMAX+1) - for j in range(0,nlat): - plm[:,m,j] = plmout[:,m,j]*int_fact[j] - - # Initializing preliminary spherical harmonic matrices - yclm = np.zeros((LMAX+1, MMAX+1)) - yslm = np.zeros((LMAX+1, MMAX+1)) + 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 - dcos = np.dot(ccos,data) - dsin = np.dot(ssin,data) - for l in range(0,LMAX+1): - mm = np.min([MMAX,l])# truncate to MMAX if specified (if l > MMAX) - m = np.arange(0,mm+1)# mm+1 elements between 0 and mm - # Summing product of plms and data over all latitudes - yclm[l,m] = np.sum(plm[l,m,:]*dcos[m,:], axis=1) - yslm[l,m] = np.sum(plm[l,m,:]*dsin[m,:], axis=1) - # convert to output normalization (4-pi normalized harmonics) - Ylms.clm[l,m] = coeff*yclm[l,m] - Ylms.slm[l,m] = coeff*yslm[l,m] - + 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) + # 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] # 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 @@ -257,145 +238,157 @@ def fourier(data, lon, lat, LMAX=60, MMAX=None, PLM=0, **kwargs): # dimensions of the longitude and latitude arrays nlon = np.int64(len(lon)) nlat = np.int64(len(lat)) - # remove singleton dimensions and convert to radians - phi = (np.squeeze(lon)*np.pi/180.0) - # Colatitude in radians - theta = ((90.0 - np.squeeze(lat))*np.pi/180.0) + # calculate longitude and colatitude arrays in radians + 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) + # 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)[:, np.newaxis] + mm = np.arange(MMAX + 1) # Calculate cos and sin coefficients of signal - ccos = np.cos(np.dot(mm,phi[np.newaxis,:])) - ssin = np.sin(np.dot(mm,phi[np.newaxis,:])) - dcos = np.dot(ccos,data) - dsin = np.dot(ssin,data) - - # Normalize fourier coefficients - dcos[0,:] = dcos[0,:]/nlon - dcos[1:MMAX+1,:] = 2.0*dcos[1:MMAX+1,:]/nlon - dsin[0,:] = dsin[0,:]/nlon - dsin[1:MMAX+1,:] = 2.0*dsin[1:MMAX+1,:]/nlon + 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 # Calculate cos and sin coefficients of theta component # 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 - theta_cc = np.zeros((MMAX+1,MMAX+1)) - theta_sc = np.zeros((MMAX+1,MMAX+1)) - m_even = np.arange(0,MMAX+1,2) - m_odd = np.arange(1,MMAX,2) - n_even = len(m_even) - n_odd = len(m_odd) - - if np.isclose([theta[0],theta[nlat-1]],[0.0,np.pi]).all(): - # non-endpoints - nt = np.dot(mm,theta[1:nlat-1][np.newaxis,:]) - theta_cc[m_even,:] = 2.0*np.dot(dcos[m_even,1:nlat-1],np.cos(nt).T) - theta_sc[m_even,:] = 2.0*np.dot(dsin[m_even,1:nlat-1],np.cos(nt).T) - theta_cc[m_odd,:] = 2.0*np.dot(dcos[m_odd,1:nlat-1],np.sin(nt).T) - theta_sc[m_odd,:] = 2.0*np.dot(dsin[m_odd,1:nlat-1],np.sin(nt).T) + 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(): + # 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 + ) # endpoints - theta_cc[m_even,:] += np.dot((dcos[m_even,0]*np.cos(theta[0]) + - dcos[m_even,nlat-1]*np.cos(theta[nlat-1]))[:,np.newaxis], mm.T) - theta_sc[m_even,:] += np.dot((dsin[m_even,0]*np.cos(theta[0]) + - dsin[m_even,nlat-1]*np.cos(theta[nlat-1]))[:,np.newaxis], mm.T) - theta_cc[m_odd,:] += np.dot((dcos[m_odd,0]*np.sin(theta[0]) + - dcos[m_odd,nlat-1]*np.sin(theta[nlat-1]))[:,np.newaxis], mm.T) - theta_sc[m_odd,:] += np.dot((dsin[m_odd,0]*np.sin(theta[0]) + - dsin[m_odd,nlat-1]*np.sin(theta[nlat-1]))[:,np.newaxis], mm.T) - - elif not np.isclose([theta[0],theta[nlat-1]],[0.0,np.pi]).any(): - nt = np.dot(mm,theta[np.newaxis,:]) - theta_cc[m_even,:] = 2.0*np.dot(dcos[m_even,:],np.cos(nt).T) - theta_sc[m_even,:] = 2.0*np.dot(dsin[m_even,:],np.cos(nt).T) - theta_cc[m_odd,:] = 2.0*np.dot(dcos[m_odd,:],np.sin(nt).T) - theta_sc[m_odd,:] = 2.0*np.dot(dsin[m_odd,:],np.sin(nt).T) + 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 - theta_cc[:,0] = theta_cc[:,0]/(2.0*nlat) - theta_cc[:,1:MMAX+1] = theta_cc[:,1:MMAX+1]/nlat - theta_sc[:,0] = theta_sc[:,0]/(2.0*nlat) - theta_sc[:,1:MMAX+1] = theta_sc[:,1:MMAX+1]/nlat - + f[:, 0] *= 1.0 / (2.0 * nlat) + f[:, 1 : MMAX + 1] *= 1.0 / nlat # Correct normalization for the incomplete coverage of the sphere - delphi = np.abs(phi[1]-phi[0]) - deltheta = np.abs(theta[1]-theta[0]) - norm = nlon*delphi/(2.0*np.pi)*nlat*deltheta/np.pi - theta_cc = theta_cc*norm - theta_sc = theta_sc*norm + 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): - plm = fourier_legendre(LMAX,MMAX) + if np.ndim(PLM) == 0: + Almk = fourier_legendre(LMAX, MMAX) else: - # use precomputed plms to improve computational speed - plm = PLM + # use precomputed alms to improve computational speed + Almk = PLM # 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)) - - # Sum theta fourier coefficients - # temp is the integral of cos(n theta) cos(k theta) dcos(theta) - # over the interval 0 to pi - # n and k must have like parities - - # m = even terms - k_even = np.zeros((n_even,n_even)) - for n in range(0,MMAX+2,2): - k_even[:,n//2] = 0.5*(1.0/(1.0-m_even-n) + 1.0/(1.0+m_even-n) + - 1.0/(1.0-m_even+n) + 1.0/(1.0+m_even+n)) - - k_odd = np.zeros((n_odd,n_odd)) - for n in range(1,MMAX+1,2): - k_odd[:,(n-1)//2] = 0.5*(1.0/(1-m_odd-n) + 1.0/(1+m_odd-n) + - 1.0/(1-m_odd+n) + 1.0/(1+m_odd+n)) + Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) + Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) # calculate spherical harmonics for m == even terms - l_even = np.arange(0,LMAX+1,2) - l_odd = np.arange(1,LMAX,2) - for m in range(0,MMAX+2,2): - temp = np.dot(plm[l_even,m,m_even[:,np.newaxis]].T,k_even) - Ylms.clm[l_even,m] = np.dot(theta_cc[m,m_even[:,np.newaxis]].T,temp.T) - Ylms.slm[l_even,m] = np.dot(theta_sc[m,m_even[:,np.newaxis]].T,temp.T) - temp = np.dot(plm[l_odd,m,m_odd[:,np.newaxis]].T,k_odd) - Ylms.clm[l_odd,m] = np.dot(theta_cc[m,m_odd[:,np.newaxis]].T,temp.T) - Ylms.slm[l_odd,m] = np.dot(theta_sc[m,m_odd[:,np.newaxis]].T,temp.T) - - # m = odd terms - k_even = np.zeros((n_even,n_even)) - for n in range(0,MMAX+2,2): - k_even[:,n//2] = 0.5*(-1.0/(1-m_even-n) + 1.0/(1.0+m_even-n) + - 1.0/(1.0-m_even+n) - 1.0/(1.0+m_even+n)) - - k_odd = np.zeros((n_odd,n_odd)) - for n in range(1,MMAX+1,2): - k_odd[:,(n-1)//2] = 0.5*(-1.0/(1-m_odd-n) + 1.0/(1.0+m_odd-n) + - 1.0/(1.0-m_odd+n) - 1.0/(1.0+m_odd+n)) + # even l terms (l even, m even, k even) + 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) + ) + # 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 + + # 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) + ) + # 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 # calculate spherical harmonics for m == odd terms - l_even = np.arange(2,LMAX+1,2)# do not in include l=0 - l_odd = np.arange(1,LMAX,2) - for m in range(1,MMAX+1,2): - temp = np.dot(plm[l_even,m,m_even[:,np.newaxis]].T,k_even) - Ylms.clm[l_even,m] = np.dot(theta_cc[m,m_even[:,np.newaxis]].T,temp.T) - Ylms.slm[l_even,m] = np.dot(theta_sc[m,m_even[:,np.newaxis]].T,temp.T) - temp = np.dot(plm[l_odd,m,m_odd[:,np.newaxis]].T,k_odd) - Ylms.clm[l_odd,m] = np.dot(theta_cc[m,m_odd[:,np.newaxis]].T,temp.T) - Ylms.slm[l_odd,m] = np.dot(theta_sc[m,m_odd[:,np.newaxis]].T,temp.T) + # even l terms (l even, m odd, k even) + 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 + + # 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) + ) + # 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 # 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 78ff3de3..f3be4cd7 100644 --- a/gravity_toolkit/gen_point_load.py +++ b/gravity_toolkit/gen_point_load.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" gen_point_load.py -Written by Tyler Sutterley (04/2023) +Written by Tyler Sutterley (07/2026) Calculates gravitational spherical harmonic coefficients for point masses CALLING SEQUENCE: @@ -47,6 +47,8 @@ https://doi.org/10.1029/JB078i011p01760 UPDATE HISTORY: + Updated 07/2026: use np.einsum for spherical harmonic summations + use np.radians to convert from degrees to radians Updated 04/2023: allow love numbers to be None for custom units case Updated 03/2023: improve typing for variables in docstrings Updated 02/2023: set custom units as top option in if/else statements @@ -57,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 @@ -107,8 +111,8 @@ def gen_point_load(data, lon, lat, LMAX=60, MMAX=None, UNITS=1, LOVE=None): # number of input data points npts = len(data.flatten()) # convert output longitude and latitude into radians - phi = np.pi*lon.flatten()/180.0 - theta = np.pi*(90.0 - lat.flatten())/180.0 + phi = np.radians(lon.flatten()) + theta = np.radians(90.0 - lat.flatten()) # extract degree dependent factor for specific units factors = gravity_toolkit.units(lmax=LMAX) @@ -117,35 +121,36 @@ 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 - SPH = spherical_harmonic_matrix(l, D, phi, theta, dfactor[l]) + 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 spherical_harmonic_matrix(l, data, phi, theta, coeff): +def _complex_harmonics(l, data, phi, theta, coeff): """ Calculates the spherical harmonics for a particular degree evaluated from data at coordinates @@ -168,15 +173,15 @@ def spherical_harmonic_matrix(l, data, phi, theta, coeff): Ylms: np.ndarray spherical harmonic coefficients in Eulerian form """ - # calculate normalized legendre polynomials (points, order) - Pl = legendre(l, np.cos(theta), NORMALIZE=True).T + # 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) - # calculate Euler's of spherical harmonic order multiplied by azimuth phi - mphi = np.exp(1j*np.dot(np.squeeze(phi)[:,np.newaxis], m[np.newaxis,:])) - # reshape data to order - D = np.kron(np.ones((1, l+1)), data[:,np.newaxis]) - # calculate spherical harmonics and multiply by coefficients and data - Ylms = coeff*D*Pl*mphi - # calculate the sum over all points and return harmonics for degree l - return np.sum(Ylms, axis=0) + 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)) + # reshape data to (order, points) + 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) + # return harmonics for degree l multiplied by coefficients + return coeff * Yl diff --git a/gravity_toolkit/gen_spherical_cap.py b/gravity_toolkit/gen_spherical_cap.py index bea477b9..7cf6c818 100755 --- a/gravity_toolkit/gen_spherical_cap.py +++ b/gravity_toolkit/gen_spherical_cap.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" gen_spherical_cap.py -Written by Tyler Sutterley (06/2023) +Written by Tyler Sutterley (07/2026) Calculates gravitational spherical harmonic coefficients for a spherical cap Creating a spherical cap with generating angle alpha is a 2 step process: @@ -61,6 +61,8 @@ https://doi.org/10.1007/s00190-011-0522-7 UPDATE HISTORY: + Updated 07/2026: use np.einsum for spherical harmonic summations + use np.radians to convert from degrees to radians Updated 06/2023: modified custom units case to not convert to cmwe Updated 03/2023: simplified unit degree factors using units class improve typing for variables in docstrings @@ -92,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` @@ -147,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 = lon*np.pi/180.0# Longitude in radians - th = (90.0 - lat)*np.pi/180.0# 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) @@ -157,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 = RAD_CAP*np.pi/180.0 - elif (AREA != 0): + alpha = np.radians(RAD_CAP) + 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') @@ -179,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}') @@ -212,64 +233,50 @@ 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 # this would be the plm for the center of the spherical cap # used to rotate the spherical cap to point lat/lon if PLM is None: - plmout,_ = plm_holmes(LMAX, np.cos(th)) - # truncate precomputed plms to order - plmout = np.squeeze(plmout[:,:MMAX+1,:]) - else: - # truncate precomputed plms to degree and order - plmout = PLM[:LMAX+1,:MMAX+1] + PLM, _ = plm_holmes(LMAX, np.cos(th)) # 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 - dcos = unit_conv*data*np.cos(m*phi) - dsin = unit_conv*data*np.sin(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)) - # Initializing preliminary spherical harmonic matrices - yclm = np.zeros((LMAX+1, MMAX+1)) - yslm = 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) - Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - Ylms.slm = np.zeros((LMAX+1, MMAX+1)) - for m in range(0,MMAX+1):# MMAX+1 to include MMAX - l = np.arange(m,LMAX+1)# LMAX+1 to include LMAX - # rotate spherical cap to be centered at lat/lon - plm[l,m] = plmout[l,m]*pl_alpha[l] - # multiplying clm by cos(m*phi) and slm by sin(m*phi) - # to get a field of spherical harmonics - yclm[l,m] = plm[l,m]*dcos[m] - yslm[l,m] = plm[l,m]*dsin[m] - # multiplying by factors to convert to geoid coefficients - Ylms.clm[l,m] = dfactor[l]*yclm[l,m] - Ylms.slm[l,m] = dfactor[l]*yslm[l,m] + # rotate spherical cap to be centered at lat/lon + 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) + # 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) # return the output spherical harmonics object return Ylms diff --git a/gravity_toolkit/gen_stokes.py b/gravity_toolkit/gen_stokes.py index 6b43f3fa..54a139f5 100755 --- a/gravity_toolkit/gen_stokes.py +++ b/gravity_toolkit/gen_stokes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" gen_stokes.py -Written by Tyler Sutterley (04/2023) +Written by Tyler Sutterley (07/2026) Converts data from the spatial domain to spherical harmonic coefficients @@ -43,6 +43,8 @@ and filters the GRACE/GRACE-FO coefficients for striping errors UPDATE HISTORY: + Updated 07/2026: use np.einsum for spherical harmonic summations + use np.radians to convert from degrees to radians Updated 06/2025: copy latitude and longitude as float64 for numpy 2.0 stability Updated 04/2023: allow love numbers to be None for custom units case Updated 03/2023: improve typing for variables in docstrings @@ -72,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` @@ -129,25 +134,15 @@ def gen_stokes(data, lon, lat, LMIN=0, LMAX=60, MMAX=None, UNITS=1, # grid dimensions nlat = np.int64(len(lat)) - # grid step - dlon = np.abs(lon[1]-lon[0]) - dlat = np.abs(lat[1]-lat[0]) - # longitude degree spacing in radians - dphi = dlon*np.pi/180.0 - # colatitude degree spacing in radians - dth = dlat*np.pi/180.0 - - # convert latitude and longitude to float if integers - lon = lon.astype(np.float64) - lat = lat.astype(np.float64) - # reformatting longitudes to range 0:360 (if previously -180:180) - lon = np.squeeze(lon.copy()) - if np.any(lon < 0): - lon[lon < 0] += 360.0 # Longitude in radians - phi = lon[np.newaxis,:]*np.pi/180.0 - # Colatitude in radians - th = (90.0 - np.squeeze(lat.copy()))*np.pi/180.0 + 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) + # colatitude in radians + th = np.radians(90.0 - np.squeeze(lat.copy())) + # grid step in radians + dphi = np.abs(phi[1] - phi[0]) + dth = np.abs(th[1] - th[0]) # reforming data to lonXlat if input latXlon sz = np.shape(data) @@ -162,64 +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] - m = np.arange(MMAX+1) - ccos = np.cos(np.dot(m[:,np.newaxis],phi)) - ssin = np.sin(np.dot(m[:,np.newaxis],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)) - # Multiplying by integration factors [sin(theta)*dtheta*dphi] - # truncate legendre polynomials to spherical harmonic order MMAX - for j in range(0,nlat): - plm[:,m,j] = PLM[:,m,j]*int_fact[j] + # truncate legendre polynomials to degree and order + plm = np.einsum( + 'lmh...,h...->lmh...', PLM[: LMAX + 1, : MMAX + 1, :], int_fact + ) - # Initializing preliminary spherical harmonic matrices - yclm = np.zeros((LMAX+1, MMAX+1)) - yslm = np.zeros((LMAX+1, MMAX+1)) # Initializing output spherical harmonic matrices Ylms = gravity_toolkit.harmonics(lmax=LMAX, mmax=MMAX) - Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - Ylms.slm = np.zeros((LMAX+1, MMAX+1)) # Multiplying gridded data with sin/cos of m#phis # This will sum through all phis in the dot product # output [m,theta] - dcos = np.dot(ccos,data) - dsin = np.dot(ssin,data) - for l in range(LMIN,LMAX+1):# equivalent to LMIN:LMAX - mm = np.min([MMAX,l])# truncate to MMAX if specified (if l > MMAX) - m = np.arange(0,mm+1)# mm+1 elements between 0 and mm - # Summing product of plms and data over all latitudes - # axis=1 signifies the direction of the summation - yclm[l,m] = np.sum(plm[l,m,:]*dcos[m,:], axis=1) - yslm[l,m] = np.sum(plm[l,m,:]*dsin[m,:], axis=1) - # Multiplying by factors to convert to fully normalized coefficients - Ylms.clm[l,m] = dfactor[l]*yclm[l,m] - Ylms.slm[l,m] = dfactor[l]*yslm[l,m] + 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) + # 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) # 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 247b77ff..a6c932bf 100644 --- a/gravity_toolkit/geocenter.py +++ b/gravity_toolkit/geocenter.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" geocenter.py -Written by Tyler Sutterley (06/2024) +Written by Tyler Sutterley (07/2026) Data class for reading and processing geocenter data PYTHON DEPENDENCIES: @@ -15,6 +15,7 @@ https://github.com/yaml/pyyaml UPDATE HISTORY: + Updated 07/2026: add dunder (magic) methods for mathematical operations Updated 06/2024: use wrapper to importlib for optional dependencies Updated 05/2024: make subscriptable and allow item assignment Updated 09/2023: add group option to netCDF read function @@ -42,6 +43,7 @@ Updated 02/2014: minor update to if statement Updated 03/2013: converted to python """ + import re import io import copy @@ -57,6 +59,7 @@ # attempt imports netCDF4 = import_dependency('netCDF4') + class geocenter(object): """ Data class for reading and processing geocenter data @@ -79,32 +82,36 @@ class geocenter(object): time variable of the spherical harmonics month: np.ndarray GRACE/GRACE-FO months variable of the spherical harmonics - radius: float, default 6371000.790009159 + 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 @@ -128,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) @@ -164,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 @@ -202,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 @@ -229,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 @@ -254,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 @@ -265,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 @@ -297,8 +312,8 @@ def from_SLR(self, geocenter_file, **kwargs): Reads monthly geocenter files from `satellite laser ranging provided by CSR `_ - - `RL04`: GCN_RL04.txt - - `RL05`: GCN_RL05.txt + - ``RL04``: GCN_RL04.txt + - ``RL05``: GCN_RL05.txt `New CF-CM geocenter dataset `_ @@ -333,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']) @@ -344,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' @@ -374,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')]) @@ -406,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) @@ -458,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 @@ -475,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+)?' @@ -490,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: @@ -507,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 @@ -536,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: @@ -551,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 @@ -579,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: @@ -599,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 @@ -637,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: @@ -649,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 @@ -688,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]) @@ -702,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) @@ -756,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 @@ -789,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']: @@ -814,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: @@ -839,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: @@ -848,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 @@ -874,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): @@ -891,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: @@ -910,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): @@ -931,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 @@ -959,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 @@ -1004,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 @@ -1030,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 @@ -1088,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])) @@ -1166,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): @@ -1183,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): @@ -1210,28 +1287,89 @@ 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): + """Add values to a ``geocenter`` object""" + temp = self.copy() + return temp.add(other) + + def __div__(self, other): + """Divide values from a ``geocenter`` object""" + return self.__truediv__(other) + + def __iadd__(self, other): + """In-place add values to a ``geocenter`` object""" + return self.add(other) + + def __idiv__(self, other): + """In-place divide values from a ``geocenter`` object""" + return self.__itruediv__(other) + + def __imul__(self, other): + """In-place multiply values from a ``geocenter`` object""" + if isinstance(other, (int, float, np.ndarray)): + return self.scale(other) + else: + return self.multiply(other) + + def __ipow__(self, other): + """In-place raise values from a ``geocenter`` object to a power""" + return self.power(other) + + def __isub__(self, other): + """In-place subtract values from a ``geocenter`` object""" + return self.subtract(other) + + def __itruediv__(self, other): + """In-place divide values from a ``geocenter`` object""" + if isinstance(other, (int, float, np.ndarray)): + return self.scale(1.0 / other) + else: + return self.divide(other) + + def __mul__(self, other): + """Multiply values from a ``geocenter`` object""" + temp = self.copy() + if isinstance(other, (int, float, np.ndarray)): + 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() + return temp.subtract(other) + + def __truediv__(self, other): + """Divide values from a ``geocenter`` object""" + temp = self.copy() + if isinstance(other, (int, float, np.ndarray)): + return temp.scale(1.0 / other) + else: + 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 6201292f..991dea50 100644 --- a/gravity_toolkit/grace_date.py +++ b/gravity_toolkit/grace_date.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_date.py Written by Tyler Sutterley (05/2023) Contributions by Hugo Lecomte and Yara Mohajerani @@ -102,13 +102,15 @@ the dataset from an external main level program Updated 04/2012: changes for RL05 data """ + from __future__ import print_function import logging import pathlib import argparse import numpy as np -import gravity_toolkit.time +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 = gravity_toolkit.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 = gravity_toolkit.time.calendar_days(start_yr[t]) + 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 = gravity_toolkit.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]) @@ -209,44 +225,46 @@ def grace_date(base_dir, PROC='', DREL='', DSET='', OUTPUT=True, MODE=0o775): end_day[t] = np.float64(end_date[1]) # number of days in the starting year for leap and standard years - dpy = gravity_toolkit.time.calendar_days(start_yr[t]).sum() + 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 = gravity_toolkit.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 = gravity_toolkit.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 year = 2002 + iyr # add all days from prior years to count # number of days in year i (if leap year or standard year) - count += gravity_toolkit.time.calendar_days(year).sum() + 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] = gravity_toolkit.time.calendar_to_grace( - cal_date['year'],cal_date['month']) + mon[t] = gravtk.time.calendar_to_grace( + 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 @@ -254,15 +272,15 @@ def grace_date(base_dir, PROC='', DREL='', DSET='', OUTPUT=True, MODE=0o775): # For CSR and GFZ: Nov 2011 (119) is centered in Oct 2011 (118) # For JPL: Dec 2011 (120) is centered in Jan 2012 (121) # For all: May 2015 (161) is centered in Apr 2015 (160) - mon = gravity_toolkit.time.adjust_months(mon) + mon = gravtk.time.adjust_months(mon) # Output GRACE/GRACE-FO date ascii file if OUTPUT: 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,48 +319,83 @@ def arguments(): """ ) # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + help='Working data directory', + ) # Data processing center or satellite mission - parser.add_argument('--center','-c', - metavar='PROC', type=str, nargs='+', - default=['CSR','GFZ','JPL'], - help='GRACE/GRACE-FO Processing Center') + parser.add_argument( + '--center', + '-c', + metavar='PROC', + type=str, + nargs='+', + default=['CSR', 'GFZ', 'JPL'], + help='GRACE/GRACE-FO Processing Center', + ) # GRACE/GRACE-FO data release - parser.add_argument('--release','-r', - metavar='DREL', type=str, nargs='+', + parser.add_argument( + '--release', + '-r', + metavar='DREL', + type=str, + nargs='+', default=['RL06'], - help='GRACE/GRACE-FO Data Release') + help='GRACE/GRACE-FO Data Release', + ) # GRACE/GRACE-FO data product - parser.add_argument('--product','-p', - metavar='DSET', type=str.upper, nargs='+', - default=['GAC','GAD','GSM'], - choices=['GAA','GAB','GAC','GAD','GSM'], - help='GRACE/GRACE-FO Level-2 data product') + parser.add_argument( + '--product', + '-p', + metavar='DSET', + type=str.upper, + nargs='+', + default=['GAC', 'GAD', 'GSM'], + choices=['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + help='GRACE/GRACE-FO Level-2 data product', + ) # output GRACE/GRACE-FO ascii date file - parser.add_argument('--output','-O', - default=False, action='store_true', - help='Overwrite existing data') + parser.add_argument( + '--output', + '-O', + default=False, + action='store_true', + help='Overwrite existing data', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run GRACE/GRACE-FO date program for pr in args.center: for rl in args.release: for ds in args.product: - grace_date(args.directory, PROC=pr, DREL=rl, DSET=ds, - OUTPUT=args.output, MODE=args.mode) + grace_date( + args.directory, + PROC=pr, + DREL=rl, + DSET=ds, + OUTPUT=args.output, + MODE=args.mode, + ) + # run main program if __name__ == '__main__': diff --git a/gravity_toolkit/grace_find_months.py b/gravity_toolkit/grace_find_months.py index 2105fb09..0fb5e5b0 100644 --- a/gravity_toolkit/grace_find_months.py +++ b/gravity_toolkit/grace_find_months.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_find_months.py Written by Tyler Sutterley (05/2023) @@ -51,10 +51,12 @@ Updated 09/2013: missing periods for for CNES Written 05/2013 """ + import pathlib import numpy as np from gravity_toolkit.grace_date import grace_date + def grace_find_months(base_dir, PROC, DREL, DSET='GSM'): """ Parses date index file @@ -113,9 +115,9 @@ def grace_find_months(base_dir, PROC, DREL, DSET='GSM'): grace_date(base_dir, PROC=PROC, DREL=DREL, DSET=DSET, OUTPUT=True) # names and formats of GRACE/GRACE-FO date ascii file - names = ('t','mon','styr','stday','endyr','endday','total') - formats = ('f','i','i','i','i','i','i') - dtype = np.dtype({'names':names, 'formats':formats}) + names = ('t', 'mon', 'styr', 'stday', 'endyr', 'endday', 'total') + formats = ('f', 'i', 'i', 'i', 'i', 'i', 'i') + dtype = np.dtype({'names': names, 'formats': formats}) # read GRACE/GRACE-FO date ascii file # skip the header row and extract dates (decimal format) and months date_input = np.loadtxt(grace_date_file, skiprows=1, dtype=dtype) @@ -132,7 +134,7 @@ def grace_find_months(base_dir, PROC, DREL, DSET='GSM'): var_info['missing'] = sorted(set(all_months) - set(date_input['mon'])) # If CNES RL01/2: simply convert into numpy array # else: remove months 1-3 and convert into numpy array - if ((PROC == 'CNES') & (DREL in ('RL01','RL02'))): + if (PROC == 'CNES') & (DREL in ('RL01', 'RL02')): var_info['missing'] = np.array(var_info['missing'], dtype=np.int64) else: var_info['missing'] = np.array(var_info['missing'][3:], dtype=np.int64) diff --git a/gravity_toolkit/grace_input_months.py b/gravity_toolkit/grace_input_months.py index 12cf35b7..0bd49f54 100644 --- a/gravity_toolkit/grace_input_months.py +++ b/gravity_toolkit/grace_input_months.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_input_months.py Written by Tyler Sutterley (10/2023) Contributions by Hugo Lecomte and Yara Mohajerani @@ -169,6 +169,7 @@ Updated 07/2013: can use different geocenter solutions Written 05/2013 """ + from __future__ import print_function, division import re @@ -184,8 +185,20 @@ from gravity_toolkit.read_GRACE_harmonics import read_GRACE_harmonics from gravity_toolkit.read_gfc_harmonics import read_gfc_harmonics -def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, - missing, SLR_C20, DEG1, **kwargs): + +def grace_input_months( + base_dir, + PROC, + DREL, + DSET, + LMAX, + start_mon, + end_mon, + missing, + SLR_C20, + DEG1, + **kwargs, +): """ Reads GRACE/GRACE-FO files for a spherical harmonic degree and order and a date range @@ -248,7 +261,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, - ``'SLR'``: Satellite laser ranging coefficients from CSR :cite:p:`Cheng:2013tz` - ``'UCI'``: GRACE/GRACE-FO coefficients from :cite:p:`Sutterley:2019bx` - ``'Swenson'``: GRACE-derived coefficients from :cite:p:`Swenson:2008cr` - - ``'GFZ'``: GFZ GravIS coefficients + - ``'GFZ'``: GFZ GravIS coefficients MMAX: int or NoneType, default None Upper bound of Spherical Harmonic Orders SLR_21: str or NoneType, default '' @@ -319,16 +332,16 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, Attributes of input files and corrections """ # set default keyword arguments - kwargs.setdefault('MMAX',LMAX) - kwargs.setdefault('SLR_21','') - kwargs.setdefault('SLR_22','') - kwargs.setdefault('SLR_C30','') - kwargs.setdefault('SLR_C40','') - kwargs.setdefault('SLR_C50','') - kwargs.setdefault('DEG1_FILE',None) - kwargs.setdefault('MODEL_DEG1',False) - kwargs.setdefault('ATM',False) - kwargs.setdefault('POLE_TIDE',False) + kwargs.setdefault('MMAX', LMAX) + kwargs.setdefault('SLR_21', '') + kwargs.setdefault('SLR_22', '') + kwargs.setdefault('SLR_C30', '') + kwargs.setdefault('SLR_C40', '') + kwargs.setdefault('SLR_C50', '') + kwargs.setdefault('DEG1_FILE', None) + kwargs.setdefault('MODEL_DEG1', False) + kwargs.setdefault('ATM', False) + kwargs.setdefault('POLE_TIDE', False) # directory of exact GRACE/GRACE-FO product base_dir = pathlib.Path(base_dir).expanduser().absolute() @@ -342,55 +355,53 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, # Range of months from start_mon to end_mon (end_mon+1 to include end_mon) # Removing the missing months and months not to consider - months = sorted(set(np.arange(start_mon, end_mon+1)) - set(missing)) + months = sorted(set(np.arange(start_mon, end_mon + 1)) - set(missing)) # number of months to consider in analysis n_cons = len(months) # Initializing input data matrices grace_Ylms = {} - grace_Ylms['clm'] = np.zeros((LMAX+1, MMAX+1, n_cons)) - grace_Ylms['slm'] = np.zeros((LMAX+1, MMAX+1, n_cons)) - grace_Ylms['eclm'] = np.zeros((LMAX+1, MMAX+1, n_cons)) - grace_Ylms['eslm'] = np.zeros((LMAX+1, MMAX+1, n_cons)) + grace_Ylms['clm'] = np.zeros((LMAX + 1, MMAX + 1, n_cons)) + grace_Ylms['slm'] = np.zeros((LMAX + 1, MMAX + 1, n_cons)) + grace_Ylms['eclm'] = np.zeros((LMAX + 1, MMAX + 1, n_cons)) + grace_Ylms['eslm'] = np.zeros((LMAX + 1, MMAX + 1, n_cons)) grace_Ylms['time'] = np.zeros((n_cons)) grace_Ylms['month'] = np.zeros((n_cons), dtype=np.int64) # output dimensions - grace_Ylms['l'] = np.arange(LMAX+1) - grace_Ylms['m'] = np.arange(MMAX+1) + grace_Ylms['l'] = np.arange(LMAX + 1) + grace_Ylms['m'] = np.arange(MMAX + 1) # attributes for processing run attributes = collections.OrderedDict() # input GRACE/GRACE-FO and correction files - attributes['lineage'] = [None]*n_cons + attributes['lineage'] = [None] * n_cons # associate GRACE/GRACE-FO files with each GRACE/GRACE-FO month - grace_files = grace_date(base_dir, - PROC=PROC, - DREL=DREL, - DSET=DSET, - OUTPUT=False + grace_files = grace_date( + base_dir, PROC=PROC, DREL=DREL, DSET=DSET, OUTPUT=False ) # importing data from GRACE/GRACE-FO files - for i,grace_month in enumerate(months): + for i, grace_month in enumerate(months): # read spherical harmonic data products infile = grace_files[grace_month] # log input file if debugging logging.debug(f'Reading file {i:d}: {str(infile)}') # read GRACE/GRACE-FO/Swarm file - if PROC in ('GRAZ','Swarm'): + if PROC in ('GRAZ', 'Swarm'): # Degree 2 zonals will be converted to a tide free state Ylms = read_gfc_harmonics(infile, TIDE='tide_free') else: # Effects of Pole tide drift will be compensated if specified - Ylms = read_GRACE_harmonics(infile, LMAX, MMAX=MMAX, - POLE_TIDE=kwargs['POLE_TIDE']) + Ylms = read_GRACE_harmonics( + infile, LMAX, MMAX=MMAX, POLE_TIDE=kwargs['POLE_TIDE'] + ) # truncate harmonics to degree and order - grace_Ylms['clm'][:,:,i] = Ylms['clm'][0:LMAX+1, 0:MMAX+1] - grace_Ylms['slm'][:,:,i] = Ylms['slm'][0:LMAX+1, 0:MMAX+1] + grace_Ylms['clm'][:, :, i] = Ylms['clm'][0 : LMAX + 1, 0 : MMAX + 1] + grace_Ylms['slm'][:, :, i] = Ylms['slm'][0 : LMAX + 1, 0 : MMAX + 1] # truncate harmonic errors to degree and order - grace_Ylms['eclm'][:,:,i] = Ylms['eclm'][0:LMAX+1, 0:MMAX+1] - grace_Ylms['eslm'][:,:,i] = Ylms['eslm'][0:LMAX+1, 0:MMAX+1] + grace_Ylms['eclm'][:, :, i] = Ylms['eclm'][0 : LMAX + 1, 0 : MMAX + 1] + grace_Ylms['eslm'][:, :, i] = Ylms['eslm'][0 : LMAX + 1, 0 : MMAX + 1] # copy date variables grace_Ylms['time'][i] = np.copy(Ylms['time']) grace_Ylms['month'][i] = np.int64(grace_month) @@ -404,12 +415,12 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, FLAGS = [] # Replacing C2,0 with SLR values - if (SLR_C20 == 'CSR'): - if (DREL == 'RL04'): + if SLR_C20 == 'CSR': + if DREL == 'RL04': SLR_file = base_dir.joinpath('TN-05_C20_SLR.txt') - elif (DREL == 'RL05'): + elif DREL == 'RL05': SLR_file = base_dir.joinpath('TN-07_C20_SLR.txt') - elif (DREL == 'RL06'): + elif DREL == 'RL06': # SLR_file = base_dir.joinpath('TN-11_C20_SLR.txt') SLR_file = base_dir.joinpath('C20_RL06.txt') # log SLR file if debugging @@ -418,7 +429,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C20_input = gravity_toolkit.SLR.C20(SLR_file) FLAGS.append('_wCSR_C20') attributes['SLR C20'] = ('CSR', SLR_file.name) - elif (SLR_C20 == 'GFZ'): + elif SLR_C20 == 'GFZ': SLR_file = base_dir.joinpath(f'GFZ_{DREL}_C20_SLR.dat') # log SLR file if debugging logging.debug(f'Reading SLR C20 file: {str(SLR_file)}') @@ -426,7 +437,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C20_input = gravity_toolkit.SLR.C20(SLR_file) FLAGS.append('_wGFZ_C20') attributes['SLR C20'] = ('GFZ', SLR_file.name) - elif (SLR_C20 == 'GSFC'): + elif SLR_C20 == 'GSFC': SLR_file = base_dir.joinpath('TN-14_C30_C20_GSFC_SLR.txt') # log SLR file if debugging logging.debug(f'Reading SLR C20 file: {str(SLR_file)}') @@ -436,7 +447,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, attributes['SLR C20'] = ('GSFC', SLR_file.name) # Replacing C2,1/S2,1 with SLR values - if (kwargs['SLR_21'] == 'CSR'): + if kwargs['SLR_21'] == 'CSR': SLR_file = base_dir.joinpath(f'C21_S21_{DREL}.txt') # log SLR file if debugging logging.debug(f'Reading SLR C21/S21 file: {str(SLR_file)}') @@ -444,7 +455,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C21_input = gravity_toolkit.SLR.CS2(SLR_file) FLAGS.append('_wCSR_21') attributes['SLR 21'] = ('CSR', SLR_file.name) - elif (kwargs['SLR_21'] == 'GFZ'): + elif kwargs['SLR_21'] == 'GFZ': GravIS_file = 'GRAVIS-2B_GFZOP_GRACE+SLR_LOW_DEGREES_0003.dat' SLR_file = base_dir.joinpath(GravIS_file) # log SLR file if debugging @@ -453,19 +464,20 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C21_input = gravity_toolkit.SLR.CS2(SLR_file) FLAGS.append('_wGFZ_21') attributes['SLR 21'] = ('GFZ GravIS', SLR_file.name) - elif (kwargs['SLR_21'] == 'GSFC'): + elif kwargs['SLR_21'] == 'GSFC': # calculate monthly averages from 7-day arcs SLR_file = base_dir.joinpath('gsfc_slr_5x5c61s61.txt') # log SLR file if debugging logging.debug(f'Reading SLR C21/S21 file: {str(SLR_file)}') # read SLR file - C21_input = gravity_toolkit.SLR.CS2(SLR_file, - DATE=grace_Ylms['time'], ORDER=1) + C21_input = gravity_toolkit.SLR.CS2( + SLR_file, DATE=grace_Ylms['time'], ORDER=1 + ) FLAGS.append('_wGSFC_21') attributes['SLR 21'] = ('GSFC', SLR_file.name) # Replacing C2,2/S2,2 with SLR values - if (kwargs['SLR_22'] == 'CSR'): + if kwargs['SLR_22'] == 'CSR': SLR_file = base_dir.joinpath(f'C22_S22_{DREL}.txt') # log SLR file if debugging logging.debug(f'Reading SLR C22/S22 file: {str(SLR_file)}') @@ -473,18 +485,19 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C22_input = gravity_toolkit.SLR.CS2(SLR_file) FLAGS.append('_wCSR_22') attributes['SLR 22'] = ('CSR', SLR_file.name) - elif (kwargs['SLR_22'] == 'GSFC'): + elif kwargs['SLR_22'] == 'GSFC': SLR_file = base_dir.joinpath('gsfc_slr_5x5c61s61.txt') # log SLR file if debugging logging.debug(f'Reading SLR C22/S22 file: {str(SLR_file)}') # read SLR file - C22_input = gravity_toolkit.SLR.CS2(SLR_file, - DATE=grace_Ylms['time'], ORDER=2) + C22_input = gravity_toolkit.SLR.CS2( + SLR_file, DATE=grace_Ylms['time'], ORDER=2 + ) FLAGS.append('_wGSFC_22') attributes['SLR 22'] = ('GSFC', SLR_file.name) # Replacing C3,0 with SLR values - if (kwargs['SLR_C30'] == 'CSR'): + if kwargs['SLR_C30'] == 'CSR': SLR_file = base_dir.joinpath('CSR_Monthly_5x5_Gravity_Harmonics.txt') # log SLR file if debugging logging.debug(f'Reading SLR C30 file: {str(SLR_file)}') @@ -492,7 +505,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C30_input = gravity_toolkit.SLR.C30(SLR_file) FLAGS.append('_wCSR_C30') attributes['SLR C30'] = ('CSR', SLR_file.name) - elif (kwargs['SLR_C30'] == 'LARES'): + elif kwargs['SLR_C30'] == 'LARES': SLR_file = base_dir.joinpath('C30_LARES_filtered.txt') # log SLR file if debugging logging.debug(f'Reading SLR C30 file: {str(SLR_file)}') @@ -500,7 +513,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C30_input = gravity_toolkit.SLR.C30(SLR_file) FLAGS.append('_wLARES_C30') attributes['SLR_C30'] = ('CSR LARES', SLR_file.name) - elif (kwargs['SLR_C30'] == 'GFZ'): + elif kwargs['SLR_C30'] == 'GFZ': GravIS_file = 'GRAVIS-2B_GFZOP_GRACE+SLR_LOW_DEGREES_0003.dat' SLR_file = base_dir.joinpath(GravIS_file) # log SLR file if debugging @@ -509,7 +522,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C30_input = gravity_toolkit.SLR.C30(SLR_file) FLAGS.append('_wGFZ_C30') attributes['SLR C30'] = ('GFZ GravIS', SLR_file.name) - elif (kwargs['SLR_C30'] == 'GSFC'): + elif kwargs['SLR_C30'] == 'GSFC': SLR_file = base_dir.joinpath('TN-14_C30_C20_GSFC_SLR.txt') # log SLR file if debugging logging.debug(f'Reading SLR C30 file: {str(SLR_file)}') @@ -519,7 +532,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, attributes['SLR C30'] = ('GSFC', SLR_file.name) # Replacing C4,0 with SLR values - if (kwargs['SLR_C40'] == 'CSR'): + if kwargs['SLR_C40'] == 'CSR': SLR_file = base_dir.joinpath('CSR_Monthly_5x5_Gravity_Harmonics.txt') # log SLR file if debugging logging.debug(f'Reading SLR C40 file: {str(SLR_file)}') @@ -527,7 +540,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C40_input = gravity_toolkit.SLR.C40(SLR_file) FLAGS.append('_wCSR_C40') attributes['SLR C40'] = ('CSR', SLR_file.name) - elif (kwargs['SLR_C40'] == 'LARES'): + elif kwargs['SLR_C40'] == 'LARES': SLR_file = base_dir.joinpath('C40_LARES_filtered.txt') # log SLR file if debugging logging.debug(f'Reading SLR C40 file: {str(SLR_file)}') @@ -535,18 +548,17 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C40_input = gravity_toolkit.SLR.C40(SLR_file) FLAGS.append('_wLARES_C40') attributes['SLR C40'] = ('CSR LARES', SLR_file.name) - elif (kwargs['SLR_C40'] == 'GSFC'): + elif kwargs['SLR_C40'] == 'GSFC': SLR_file = base_dir.joinpath('gsfc_slr_5x5c61s61.txt') # log SLR file if debugging logging.debug(f'Reading SLR C40 file: {str(SLR_file)}') # read SLR file - C40_input = gravity_toolkit.SLR.C40(SLR_file, - DATE=grace_Ylms['time']) + C40_input = gravity_toolkit.SLR.C40(SLR_file, DATE=grace_Ylms['time']) FLAGS.append('_wGSFC_C40') attributes['SLR C40'] = ('GSFC', SLR_file.name) # Replacing C5,0 with SLR values - if (kwargs['SLR_C50'] == 'CSR'): + if kwargs['SLR_C50'] == 'CSR': SLR_file = base_dir.joinpath('CSR_Monthly_5x5_Gravity_Harmonics.txt') # log SLR file if debugging logging.debug(f'Reading SLR C50 file: {str(SLR_file)}') @@ -554,7 +566,7 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C50_input = gravity_toolkit.SLR.C50(SLR_file) FLAGS.append('_wCSR_C50') attributes['SLR C50'] = ('CSR', SLR_file.name) - elif (kwargs['SLR_C50'] == 'LARES'): + elif kwargs['SLR_C50'] == 'LARES': SLR_file = base_dir.joinpath('C50_LARES_filtered.txt') # log SLR file if debugging logging.debug(f'Reading SLR C50 file: {str(SLR_file)}') @@ -562,39 +574,40 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, C50_input = gravity_toolkit.SLR.C50(SLR_file) FLAGS.append('_wLARES_C50') attributes['SLR C50'] = ('CSR LARES', SLR_file.name) - elif (kwargs['SLR_C50'] == 'GSFC'): + elif kwargs['SLR_C50'] == 'GSFC': # SLR_file = base_dir.joinpath('GSFC_SLR_C20_C30_C50_GSM_replacement.txt') SLR_file = base_dir.joinpath('gsfc_slr_5x5c61s61.txt') # log SLR file if debugging logging.debug(f'Reading SLR C50 file: {str(SLR_file)}') # read SLR file - C50_input = gravity_toolkit.SLR.C50(SLR_file, - DATE=grace_Ylms['time']) + C50_input = gravity_toolkit.SLR.C50(SLR_file, DATE=grace_Ylms['time']) FLAGS.append('_wGSFC_C50') attributes['SLR C50'] = ('GSFC', SLR_file.name) # Correcting for Degree 1 (geocenter variations) # reading degree 1 file for given release if specified - if (DEG1 == 'Tellus'): + if DEG1 == 'Tellus': # Tellus (PO.DAAC) degree 1 - if DREL in ('RL04','RL05'): + if DREL in ('RL04', 'RL05'): # old degree one files - default_geocenter = base_dir.joinpath('geocenter', - f'deg1_coef_{DREL}.txt') + default_geocenter = base_dir.joinpath( + 'geocenter', f'deg1_coef_{DREL}.txt' + ) JPL = False else: # new TN-13 degree one files - default_geocenter = base_dir.joinpath('geocenter', - f'TN-13_GEOC_{PROC}_{DREL}.txt') + default_geocenter = base_dir.joinpath( + 'geocenter', f'TN-13_GEOC_{PROC}_{DREL}.txt' + ) JPL = True # read degree one files from JPL GRACE Tellus DEG1_file = kwargs.get('DEG1_FILE') or default_geocenter # log geocenter file if debugging logging.debug(f'Reading Geocenter file: {DEG1_file}') - DEG1_input = gravity_toolkit.geocenter().from_tellus(DEG1_file,JPL=JPL) + DEG1_input = gravity_toolkit.geocenter().from_tellus(DEG1_file, JPL=JPL) FLAGS.append(f'_w{DEG1}_DEG1') attributes['geocenter'] = ('JPL Tellus', DEG1_file.name) - elif (DEG1 == 'SLR'): + elif DEG1 == 'SLR': # CSR Satellite Laser Ranging (SLR) degree 1 # # SLR-derived degree-1 mass variations # # ftp://ftp.csr.utexas.edu/pub/slr/geocenter/ @@ -613,22 +626,38 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, # new file of degree-1 mass variations from Minkang Cheng # http://download.csr.utexas.edu/outgoing/cheng/gct2est.220_5s - DEG1_file = base_dir.joinpath('geocenter','gct2est.220_5s') - COLUMNS = ['MJD','time','X','Y','Z','XM','YM','ZM', - 'X_sigma','Y_sigma','Z_sigma','XM_sigma','YM_sigma','ZM_sigma'] + DEG1_file = base_dir.joinpath('geocenter', 'gct2est.220_5s') + COLUMNS = [ + 'MJD', + 'time', + 'X', + 'Y', + 'Z', + 'XM', + 'YM', + 'ZM', + 'X_sigma', + 'Y_sigma', + 'Z_sigma', + 'XM_sigma', + 'YM_sigma', + 'ZM_sigma', + ] # log geocenter file if debugging logging.debug(f'Reading Geocenter file: {DEG1_file}') # read degree one files from CSR satellite laser ranging DEG1_input = gravity_toolkit.geocenter(radius=6.378136e9).from_SLR( - DEG1_file, AOD=True, release=DREL, header=15, columns=COLUMNS) + DEG1_file, AOD=True, release=DREL, header=15, columns=COLUMNS + ) FLAGS.append(f'_w{DEG1}_DEG1') attributes['geocenter'] = ('CSR SLR', DEG1_file.name) - elif DEG1 in ('SLF','UCI'): + elif DEG1 in ('SLF', 'UCI'): # degree one files from Sutterley and Velicogna (2019) # default: iterated and with self-attraction and loading effects args = (PROC, DREL, 'MPIOM', 'SLF_iter') - default_geocenter = base_dir.joinpath('geocenter', - '{0}_{1}_{2}_{3}.txt'.format(*args)) + default_geocenter = base_dir.joinpath( + 'geocenter', '{0}_{1}_{2}_{3}.txt'.format(*args) + ) # read degree one files from Sutterley and Velicogna (2019) DEG1_file = kwargs.get('DEG1_FILE') or default_geocenter # log geocenter file if debugging @@ -636,10 +665,11 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, DEG1_input = gravity_toolkit.geocenter().from_UCI(DEG1_file) FLAGS.append(f'_w{DEG1}_DEG1') attributes['geocenter'] = ('UCI', DEG1_file.name) - elif (DEG1 == 'Swenson'): + elif DEG1 == 'Swenson': # degree 1 coefficients provided by Sean Swenson in mm w.e. - default_geocenter = base_dir.joinpath('geocenter', - f'gad_gsm.{DREL}.txt') + default_geocenter = base_dir.joinpath( + 'geocenter', f'gad_gsm.{DREL}.txt' + ) # read degree one files from Swenson et al. (2008) DEG1_file = kwargs.get('DEG1_FILE') or default_geocenter # log geocenter file if debugging @@ -647,11 +677,12 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, DEG1_input = gravity_toolkit.geocenter().from_swenson(DEG1_file) FLAGS.append(f'_w{DEG1}_DEG1') attributes['geocenter'] = ('Swenson', DEG1_file.name) - elif (DEG1 == 'GFZ'): + elif DEG1 == 'GFZ': # degree 1 coefficients provided by GFZ GravIS # http://gravis.gfz-potsdam.de/corrections - default_geocenter = base_dir.joinpath('geocenter', - 'GRAVIS-2B_GFZOP_GEOCENTER_0003.dat') + default_geocenter = base_dir.joinpath( + 'geocenter', 'GRAVIS-2B_GFZOP_GEOCENTER_0003.dat' + ) # read degree one files from GFZ GravIS DEG1_file = kwargs.get('DEG1_FILE') or default_geocenter # log geocenter file if debugging @@ -672,98 +703,98 @@ def grace_input_months(base_dir, PROC, DREL, DSET, LMAX, start_mon, end_mon, grace_Ylms['title'] = ''.join(FLAGS) # Replace C20 with SLR coefficients - if SLR_C20 in ('CSR','GFZ','GSFC'): + if SLR_C20 in ('CSR', 'GFZ', 'GSFC'): # verify that there are replacement C20 months for specified range months_test = sorted(set(months) - set(C20_input['month'])) if months_test: gm = ','.join(f'{gm:03d}' for gm in months_test) raise IOError(f'No Matching C20 Months ({gm})') # replace C20 with SLR coefficients - for i,grace_month in enumerate(months): + for i, grace_month in enumerate(months): count = np.count_nonzero(C20_input['month'] == grace_month) - if (count != 0): - k, = np.nonzero(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.nonzero(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.nonzero(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.nonzero(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.nonzero(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.nonzero(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.nonzero(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 00531d70..a294b44d 100644 --- a/gravity_toolkit/grace_months_index.py +++ b/gravity_toolkit/grace_months_index.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_months_index.py Written by Tyler Sutterley (05/2023) @@ -63,15 +63,17 @@ Updated 05/2013: added years to month label Written 07/2012 """ + from __future__ import print_function import pathlib import argparse import calendar import numpy as np -from gravity_toolkit.time import grace_to_calendar +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 = 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,30 +194,45 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 8e0704b2..8b4704cd 100644 --- a/gravity_toolkit/harmonic_gradients.py +++ b/gravity_toolkit/harmonic_gradients.py @@ -1,8 +1,8 @@ #!/usr/bin/env python -u""" +""" harmonic_gradients.py Original IDL code calc_grad.pro written by Sean Swenson -Adapted by Tyler Sutterley (03/2023) +Adapted by Tyler Sutterley (07/2026) Calculates the zonal and meridional gradients of a scalar field from a series of spherical harmonics @@ -29,6 +29,8 @@ Legendre functions UPDATE HISTORY: + Updated 07/2026: use np.einsum for spherical harmonic summations + use np.radians to convert from degrees to radians Updated 03/2023: improve typing for variables in docstrings added geostrophic currents program from Wahr et al. (2002) Updated 10/2022: cleaned up program for public release @@ -37,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 @@ -44,11 +47,11 @@ 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 + spherical harmonics :cite:p:`Driscoll:1994bp` Parameters ---------- @@ -74,94 +77,72 @@ 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) # Longitude in radians - phi = (np.squeeze(lon)*np.pi/180.0)[np.newaxis,:] + phi = np.radians(np.squeeze(lon)) # Colatitude in radians - th = (90.0 - np.squeeze(lat))*np.pi/180.0 - thmax = len(np.squeeze(lat)) - phimax = len(np.squeeze(lon)) + th = np.radians(90.0 - np.squeeze(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 + # real (cosine) and imaginary (sine) components + Ylm = np.zeros((LMAX + 1, MMAX + 1), dtype=np.complex128) # Truncating harmonics to degree and order LMAX # removing coefficients below LMIN and above MMAX - mm = np.arange(0,MMAX+1) - clm = np.zeros((LMAX+1, MMAX+1)) - slm = np.zeros((LMAX+1, MMAX+1)) - clm[LMIN:LMAX+1,mm] = clm1[LMIN:LMAX+1,mm] - slm[LMIN:LMAX+1,mm] = slm1[LMIN:LMAX+1,mm] - # spherical harmonic degree and order - ll = np.arange(0,LMAX+1)[np.newaxis, :]# lmax+1 to include lmax - mm = np.arange(0,MMAX+1)[:, np.newaxis]# mmax+1 to include 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) # generate Vlm coefficients (vlm and wlm) - vlm, wlm = legendre_gradient(LMAX, MMAX) - - dlm = np.zeros((LMAX+1,LMAX+1,2)) - # minus sign is because lat and theta change with opposite sign - for l in range(0,LMAX+1): - dlm[l,:,0] = -clm[l,:]*np.sqrt((l+1.0)*l) - dlm[l,:,1] = -slm[l,:]*np.sqrt((l+1.0)*l) - - m_even = np.arange(0,MMAX+2,2) - m_odd = np.arange(1,MMAX,2) - + Vlmk, Wlmk = legendre_gradient(LMAX, MMAX) + # even and odd spherical harmonic orders + 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)) # Calculate fourier coefficients from legendre coefficients - d_cos = np.zeros((LMAX+1,thmax,2)) - d_sin = np.zeros((LMAX+1,thmax,2)) - cnk = np.cos(np.dot(th[:,np.newaxis],ll)) - snk = np.sin(np.dot(th[:,np.newaxis],ll)) - - wtmp = np.zeros((len(m_even),LMAX+1,2)) - vtmp = np.zeros((len(m_even),LMAX+1,2)) - # m = even terms (vlm,wlm sine series) - for n in range(0,LMAX+1): - wtmp[:,n,0] = np.sum(wlm[:,m_even,n]*dlm[:,m_even,0],axis=0) - wtmp[:,n,1] = np.sum(wlm[:,m_even,n]*dlm[:,m_even,1],axis=0) - vtmp[:,n,0] = np.sum(vlm[:,m_even,n]*dlm[:,m_even,0],axis=0) - vtmp[:,n,1] = np.sum(vlm[:,m_even,n]*dlm[:,m_even,1],axis=0) - - d_cos[m_even,:,0] = np.dot(wtmp[:,:,1],np.transpose(snk)) - d_sin[m_even,:,0] = np.dot(-wtmp[:,:,0],np.transpose(snk)) - d_cos[m_even,:,1] = np.dot(vtmp[:,:,1],np.transpose(snk)) - d_sin[m_even,:,1] = np.dot(-vtmp[:,:,0],np.transpose(snk)) - - # m = odd terms (vlm,wlm cosine series) - wtmp = np.zeros((len(m_odd),LMAX+1,2)) - vtmp = np.zeros((len(m_odd),LMAX+1,2)) - for n in range(0,LMAX+1): - wtmp[:,n,0] = np.sum(wlm[:,m_odd,n]*dlm[:,m_odd,0],axis=0) - wtmp[:,n,1] = np.sum(wlm[:,m_odd,n]*dlm[:,m_odd,1],axis=0) - vtmp[:,n,0] = np.sum(vlm[:,m_odd,n]*dlm[:,m_odd,0],axis=0) - vtmp[:,n,1] = np.sum(vlm[:,m_odd,n]*dlm[:,m_odd,1],axis=0) - - d_cos[m_odd,:,0] = np.dot(wtmp[:,:,1],np.transpose(cnk)) - d_sin[m_odd,:,0] = np.dot(-wtmp[:,:,0],np.transpose(cnk)) - d_cos[m_odd,:,1] = np.dot(vtmp[:,:,1],np.transpose(cnk)) - d_sin[m_odd,:,1] = np.dot(-vtmp[:,:,0],np.transpose(cnk)) - - # Calculating cos(m*phi) and sin(m*phi) - ccos = np.cos(np.dot(mm,phi)) - ssin = np.sin(np.dot(mm,phi)) - # Final signal recovery from fourier coefficients - gradients = np.zeros((phimax,thmax,2)) - gradients[:,:,0] = np.dot(np.transpose(ccos), d_cos[:,:,0]) + \ - np.dot(np.transpose(ssin), d_sin[:,:,0]) - gradients[:,:,1] = np.dot(np.transpose(ccos), d_cos[:,:,1]) + \ - np.dot(np.transpose(ssin), d_sin[:,:,1]) - # return the gradient fields - return gradients - -def geostrophic_currents(clm1, slm1, lon, lat, - LMIN=0, LMAX=60, MMAX=None, RAD=0, - DENSITY=1.035, LOVE=None, PLM=None): + 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) + # 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, +): r""" - Converts data from spherical harmonic coefficients to a - spatial fields of ocean geostrophic currents following + Converts data from spherical harmonic coefficients to spatial + fields of approximate ocean geostrophic currents following :cite:p:`Wahr:1998hy,Wahr:2002ie` Parameters @@ -198,85 +179,90 @@ 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) # Longitude in radians - phi = (np.squeeze(lon)*np.pi/180.0)[np.newaxis,:] + phi = np.radians(np.squeeze(lon)) phmax = len(lon) # colatitude in radians - th = (90.0 - np.squeeze(lat))*np.pi/180.0 + th = np.radians(90.0 - np.squeeze(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)) - # zonal flow harmonics + 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]) - # meridional flow harmonics - for l in range(0, LMAX+1): + 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): # 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) - clm[LMIN:LMAX+1,mm,:] = clm[LMIN:LMAX+1,mm,:] - slm[LMIN:LMAX+1,mm,:] = slm[LMIN:LMAX+1,mm,:] - - # Calculate fourier coefficients from legendre coefficients - d_cos = np.zeros((MMAX+1,thmax,2))# [m,th] - d_sin = np.zeros((MMAX+1,thmax,2))# [m,th] - for k in range(0,thmax): - # summation over all spherical harmonic degrees - temp = 1.0/(np.cos(th[k])*np.sin(th[k])) - # for each direction of flow - for d in range(2): - d_cos[:,k,d] = temp*np.sum(PLM[:,mm,k]*clm[:,mm,d],axis=0) - d_sin[:,k,d] = temp*np.sum(PLM[:,mm,k]*slm[:,mm,d],axis=0) - - # Final signal recovery from fourier coefficients - m = np.arange(0,MMAX+1)[:,np.newaxis] - # Calculating cos(m*phi) and sin(m*phi) - ccos = np.cos(np.dot(m,phi)) - ssin = np.sin(np.dot(m,phi)) + 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, :] + ) + # 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, :] + ) + # summation over all spherical harmonic degrees + 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)) # output geostrophic current fields - currents = np.zeros((phmax,thmax,2)) - # for each direction of flow - for d in range(2): - # summation of cosine and sine harmonics - currents[:,:,d] = np.dot(np.transpose(ccos),d_cos[:,:,d]) + \ - np.dot(np.transpose(ssin),d_sin[:,:,d]) + currents = np.empty((phmax, thmax, 2)) + # summation of cosine and sine harmonics + currents[:] = np.einsum('mp...,mhd...->phd...', m_phi, pconv) - # return the current fields - return currents + # 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 3eed02ce..47c92b8f 100755 --- a/gravity_toolkit/harmonic_summation.py +++ b/gravity_toolkit/harmonic_summation.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" harmonic_summation.py -Written by Tyler Sutterley (03/2023) +Written by Tyler Sutterley (07/2026) Returns the spatial field for a series of spherical harmonics @@ -30,6 +30,8 @@ units.py: class for converting spherical harmonic data to specific units UPDATE HISTORY: + Updated 07/2026: use np.einsum for spherical harmonic summations + use np.radians to convert from degrees to radians Updated 04/2023: allow love numbers to be None for custom units case Updated 03/2023: allow units inputs to be strings for named types improve typing for variables in docstrings @@ -43,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 @@ -79,50 +84,45 @@ 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) - # Longitude in radians - phi = (np.squeeze(lon)*np.pi/180.0)[np.newaxis,:] + # longitude in radians + phi = np.radians(np.squeeze(lon)) # colatitude in radians - th = (90.0 - np.squeeze(lat))*np.pi/180.0 - thmax = len(th) + th = np.radians(90.0 - np.squeeze(lat)) # if plms are not pre-computed: calculate Legendre polynomials if PLM is None: PLM, dPLM = plm_holmes(LMAX, np.cos(th)) + # spherical harmonic order + 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) # Truncating harmonics to degree and order LMAX # removing coefficients below LMIN and above MMAX - mm = np.arange(0, MMAX+1) - clm = np.zeros((LMAX+1, MMAX+1)) - slm = np.zeros((LMAX+1, MMAX+1)) - clm[LMIN:LMAX+1,mm] = clm1[LMIN:LMAX+1,mm] - slm[LMIN:LMAX+1,mm] = slm1[LMIN:LMAX+1,mm] + 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 - d_cos = np.zeros((MMAX+1,thmax))# [m,th] - d_sin = np.zeros((MMAX+1,thmax))# [m,th] - for k in range(0,thmax): - # summation over all spherical harmonic degrees - d_cos[:,k] = np.sum(PLM[:,mm,k]*clm[:,mm],axis=0) - d_sin[:,k] = np.sum(PLM[:,mm,k]*slm[:,mm],axis=0) - - # Final signal recovery from fourier coefficients - m = np.arange(0,MMAX+1)[:,np.newaxis] - # Calculating cos(m*phi) and sin(m*phi) - ccos = np.cos(np.dot(m,phi)) - ssin = np.sin(np.dot(m,phi)) + # summation over all spherical harmonic degrees + 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)) # summation of cosine and sine harmonics - s = np.dot(np.transpose(ccos),d_cos) + np.dot(np.transpose(ssin),d_sin) + spatial = np.einsum('mp...,mh...->ph...', m_phi, pconv) + # return output data and drop imaginary component + return spatial.real - # return output data - return s -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 @@ -152,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) @@ -164,38 +164,48 @@ def harmonic_transform(clm1, slm1, lon, lat, # number of longitudinal points phimax = len(np.squeeze(lon)) # colatitude in radians - th = (90.0 - np.squeeze(lat))*np.pi/180.0 + th = np.radians(90.0 - np.squeeze(lat)) thmax = len(th) # if plms are not pre-computed: calculate Legendre polynomials if PLM is None: - PLM, dPLM = plm_holmes(LMAX, np.cos(th)) + PLM, _ = plm_holmes(LMAX, np.cos(th)) - # combined Ylms and Fourier coefficients (complex) - Ylms = np.zeros((LMAX+1, MMAX+1),dtype=np.complex128) - delta_M = np.zeros((MMAX+1,thmax),dtype=np.complex128)# [m,th] - # Real (cosine) and imaginary (sine) components + # real (cosine) and imaginary (sine) components + Ylm = np.zeros((LMAX + 1, MMAX + 1), dtype=np.complex128) # Truncating harmonics to degree and order LMAX # removing coefficients below LMIN and above MMAX - Ylms[LMIN:LMAX+1,:MMAX+1] = clm1[LMIN:LMAX+1,0:MMAX+1] - \ - slm1[LMIN:LMAX+1,0:MMAX+1]*1j + 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 - for k in range(0,thmax): - # summation over all spherical harmonic degrees - delta_M[:,k] = np.sum(PLM[:,:,k]*Ylms[:,:],axis=0)/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)) + 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(delta_M,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` @@ -241,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): @@ -269,15 +279,13 @@ def stokes_summation(clm1, slm1, lon, lat, else: raise ValueError(f'Unknown units {UNITS}') - # truncate to degree and order - mm = np.arange(0, MMAX+1) + # spherical harmonic order + mm = np.arange(0, MMAX + 1) # mmax+1 to include mmax # smooth harmonics and convert to output units - clm = np.zeros((LMAX+1, MMAX+1)) - slm = np.zeros((LMAX+1, MMAX+1)) - for l in range(0, LMAX+1):# LMAX+1 to include LMAX - clm[l,:] = wl[l]*dfactor[l]*clm1[l,mm] - slm[l,:] = wl[l]*dfactor[l]*slm1[l,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 877c0edf..b90d090f 100644 --- a/gravity_toolkit/harmonics.py +++ b/gravity_toolkit/harmonics.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" harmonics.py -Written by Tyler Sutterley (06/2024) +Written by Tyler Sutterley (07/2026) Contributions by Hugo Lecomte Spherical harmonic data class for processing GRACE/GRACE-FO Level-2 data @@ -25,6 +25,7 @@ destripe_harmonics.py: filters spherical harmonics for correlated errors UPDATE HISTORY: + Updated 07/2026: add dunder (magic) methods for mathematical operations Updated 06/2024: use wrapper to importlib for optional dependencies Updated 05/2024: make subscriptable and allow item assignment Updated 10/2023: place time and month variables in try/except block @@ -91,6 +92,7 @@ add options to flatten and expand harmonics matrices or arrays Written 03/2020 """ + from __future__ import print_function, division import re @@ -104,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 @@ -116,6 +118,7 @@ netCDF4 = import_dependency('netCDF4') sparse = import_dependency('sparse') + class harmonics(object): """ Data class for reading, writing and processing spherical harmonic data @@ -141,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 @@ -182,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) @@ -234,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: @@ -264,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]) @@ -281,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 @@ -313,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: @@ -346,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()) @@ -366,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'] = {} @@ -403,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()) @@ -417,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 @@ -435,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: @@ -448,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()) @@ -468,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() @@ -505,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())) @@ -554,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 @@ -591,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 @@ -606,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): """ @@ -636,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) @@ -705,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) @@ -733,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): @@ -762,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') @@ -771,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() @@ -816,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 @@ -848,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'] @@ -890,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) @@ -902,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())) @@ -961,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 @@ -995,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'] @@ -1028,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())) @@ -1081,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 @@ -1123,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) @@ -1146,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): @@ -1163,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 @@ -1199,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): @@ -1215,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): @@ -1241,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): @@ -1267,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): @@ -1293,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): @@ -1319,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)) @@ -1347,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 @@ -1364,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)) @@ -1388,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() @@ -1433,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) @@ -1447,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 @@ -1494,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 @@ -1532,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() @@ -1568,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 @@ -1616,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 @@ -1649,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])) @@ -1692,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 @@ -1726,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 @@ -1748,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) @@ -1759,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 @@ -1778,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 @@ -1809,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 @@ -1836,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 @@ -1864,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 @@ -1879,38 +1968,99 @@ 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): + """Add values to a ``harmonics`` object""" + temp = self.copy() + return temp.add(other) + + def __div__(self, other): + """Divide values from a ``harmonics`` object""" + return self.__truediv__(other) + + def __iadd__(self, other): + """In-place add values to a ``harmonics`` object""" + return self.add(other) + + def __idiv__(self, other): + """In-place divide values from a ``harmonics`` object""" + return self.__itruediv__(other) + + def __imul__(self, other): + """In-place multiply values from a ``harmonics`` object""" + if isinstance(other, (int, float, np.ndarray)): + return self.scale(other) + else: + return self.multiply(other) + + def __ipow__(self, other): + """In-place raise values from a ``harmonics`` object to a power""" + return self.power(other) + + def __isub__(self, other): + """In-place subtract values from a ``harmonics`` object""" + return self.subtract(other) + + def __itruediv__(self, other): + """In-place divide values from a ``harmonics`` object""" + if isinstance(other, (int, float, np.ndarray)): + return self.scale(1.0 / other) + else: + return self.divide(other) + + def __mul__(self, other): + """Multiply values from a ``harmonics`` object""" + temp = self.copy() + if isinstance(other, (int, float, np.ndarray)): + 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() + return temp.subtract(other) + + def __truediv__(self, other): + """Divide values from a ``harmonics`` object""" + temp = self.copy() + if isinstance(other, (int, float, np.ndarray)): + return temp.scale(1.0 / other) + else: + return temp.divide(other) + def __len__(self): - """Number of months - """ + """Number of months""" return len(self.month) if np.any(self.month) else 0 def __iter__(self): - """Iterate over GRACE/GRACE-FO months - """ + """Iterate over GRACE/GRACE-FO months""" self.__index__ = 0 return self def __next__(self): - """Get the next month of data - """ + """Get the next month of data""" temp = harmonics(lmax=np.copy(self.lmax), mmax=np.copy(self.mmax)) try: - temp.clm = self.clm[:,:,self.__index__].copy() - temp.slm = self.slm[:,:,self.__index__].copy() + temp.clm = self.clm[:, :, self.__index__].copy() + temp.slm = self.slm[:, :, self.__index__].copy() except IndexError as exc: raise StopIteration from exc # subset output spatial time and month diff --git a/gravity_toolkit/legendre.py b/gravity_toolkit/legendre.py index af6b192c..12bb92cc 100644 --- a/gravity_toolkit/legendre.py +++ b/gravity_toolkit/legendre.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" legendre.py Written by Tyler Sutterley (03/2023) Computes associated Legendre functions of degree l evaluated for elements x @@ -40,8 +40,10 @@ Updated 03/2019: calculate twocot separately to avoid divide warning Written 08/2016 """ + import numpy as np + def legendre(l, x, NORMALIZE=False): """ Computes associated Legendre functions for a particular degree @@ -71,91 +73,93 @@ def legendre(l, x, NORMALIZE=False): nx = len(x) # for the l = 0 case - if (l == 0): - Pl = np.ones((1,nx), dtype=np.float64) + if l == 0: + Pl = np.ones((1, nx), dtype=np.float64) return Pl # for all other degrees greater than 0 - rootl = np.sqrt(np.arange(0,2*l+1))# +1 to include 2*l + rootl = np.sqrt(np.arange(0, 2 * l + 1)) # +1 to include 2*l # s is sine of colatitude (cosine of latitude) so that 0 <= s <= 1 - s = np.sqrt(1.0 - x**2)# for x=cos(th): s=sin(th) - P = np.zeros((l+3,nx), dtype=np.float64) + s = np.sqrt(1.0 - x**2) # for x=cos(th): s=sin(th) + P = np.zeros((l + 3, nx), dtype=np.float64) # Find values of x,s for which there will be underflow - sn = (-s)**l + sn = (-s) ** l tol = np.sqrt(np.finfo(np.float64).tiny) count = np.count_nonzero((s > 0) & (np.abs(sn) <= tol)) - if (count > 0): - ind, = np.nonzero((s > 0) & (np.abs(sn) <= tol)) + if count > 0: + (ind,) = np.nonzero((s > 0) & (np.abs(sn) <= tol)) # Approximate solution of x*ln(x) = Pl - v = 9.2 - np.log(tol)/(l*s[ind]) - w = 1.0/np.log(v) - m1 = 1+l*s[ind]*v*w*(1.0058+ w*(3.819 - w*12.173)) + v = 9.2 - np.log(tol) / (l * s[ind]) + w = 1.0 / np.log(v) + m1 = 1 + l * s[ind] * v * w * (1.0058 + w * (3.819 - w * 12.173)) m1 = np.where(l < np.floor(m1), l, np.floor(m1)).astype(np.int64) # Column-by-column recursion - for k,mm1 in enumerate(m1): + for k, mm1 in enumerate(m1): col = ind[k] # Calculate two*cotangent for underflow case - twocot = -2.0*x[col]/s[col] - P[mm1-1:l+1,col] = 0.0 + twocot = -2.0 * x[col] / s[col] + P[mm1 - 1 : l + 1, col] = 0.0 # Start recursion with proper sign tstart = np.finfo(np.float64).eps - P[mm1-1,col] = np.sign(np.fmod(mm1,2)-0.5)*tstart - if (x[col] < 0): - P[mm1-1,col] = np.sign(np.fmod(l+1,2)-0.5)*tstart + P[mm1 - 1, col] = np.sign(np.fmod(mm1, 2) - 0.5) * tstart + if x[col] < 0: + P[mm1 - 1, col] = np.sign(np.fmod(l + 1, 2) - 0.5) * tstart # Recur from m1 to m = 0, accumulating normalizing factor. sumsq = tol.copy() - for m in range(mm1-2,-1,-1): - P[m,col] = ((m+1)*twocot*P[m+1,col] - \ - rootl[l+m+2]*rootl[l-m-1]*P[m+2,col]) / \ - (rootl[l+m+1]*rootl[l-m]) - sumsq += P[m,col]**2 + for m in range(mm1 - 2, -1, -1): + P[m, col] = ( + (m + 1) * twocot * P[m + 1, col] + - rootl[l + m + 2] * rootl[l - m - 1] * P[m + 2, col] + ) / (rootl[l + m + 1] * rootl[l - m]) + sumsq += P[m, col] ** 2 # calculate scale - scale = 1.0/np.sqrt(2.0*sumsq - P[0,col]**2) - P[0:mm1+1,col] = scale*P[0:mm1+1,col] + scale = 1.0 / np.sqrt(2.0 * sumsq - P[0, col] ** 2) + P[0 : mm1 + 1, col] = scale * P[0 : mm1 + 1, col] # Find the values of x,s for which there is no underflow, and (x != +/-1) count = np.count_nonzero((x != 1) & (np.abs(sn) >= tol)) - if (count > 0): - nind, = np.nonzero((x != 1) & (np.abs(sn) >= tol)) + if count > 0: + (nind,) = np.nonzero((x != 1) & (np.abs(sn) >= tol)) # Calculate two*cotangent for normal case - twocot = -2.0*x[nind]/s[nind] + twocot = -2.0 * x[nind] / s[nind] # Produce normalization constant for the m = l function - d = np.arange(2,2*l+2,2) - c = np.prod(1.0 - 1.0/d) + d = np.arange(2, 2 * l + 2, 2) + c = np.prod(1.0 - 1.0 / d) # Use sn = (-s)**l (written above) to write the m = l function - P[l,nind] = np.sqrt(c)*sn[nind] - P[l-1,nind] = P[l,nind]*twocot*l/rootl[-1] + P[l, nind] = np.sqrt(c) * sn[nind] + P[l - 1, nind] = P[l, nind] * twocot * l / rootl[-1] # Recur downwards to m = 0 - for m in range(l-2,-1,-1): - P[m,nind] = (P[m+1,nind]*twocot*(m+1) - \ - P[m+2,nind]*rootl[l+m+2]*rootl[l-m-1]) / \ - (rootl[l+m+1]*rootl[l-m]) + for m in range(l - 2, -1, -1): + P[m, nind] = ( + P[m + 1, nind] * twocot * (m + 1) + - P[m + 2, nind] * rootl[l + m + 2] * rootl[l - m - 1] + ) / (rootl[l + m + 1] * rootl[l - m]) # calculate Pl from P - Pl = np.copy(P[0:l+1,:]) + Pl = np.copy(P[0 : l + 1, :]) # Polar argument (x == +/-1) count = np.count_nonzero(s == 0) - if (count > 0): - s0, = np.nonzero(s == 0) - Pl[0,s0] = x[s0]**l + if count > 0: + (s0,) = np.nonzero(s == 0) + Pl[0, s0] = x[s0] ** l # calculate Fully Normalized Associated Legendre functions if NORMALIZE: - norm = np.zeros((l+1)) - norm[0] = np.sqrt(2.0*l+1) - m = np.arange(1,l+1) - norm[1:] = (-1)**m*np.sqrt(2.0*(2.0*l+1.0)) - Pl *= np.kron(np.ones((1,nx)), norm[:,np.newaxis]) + norm = np.zeros((l + 1)) + norm[0] = np.sqrt(2.0 * l + 1) + m = np.arange(1, l + 1) + norm[1:] = (-1) ** m * np.sqrt(2.0 * (2.0 * l + 1.0)) + Pl *= np.kron(np.ones((1, nx)), norm[:, np.newaxis]) else: # Calculate the unnormalized Legendre functions by multiplying each row # by: sqrt((l+m)!/(l-m)!) == sqrt(prod(n-m+1:n+m)) # following Abramowitz and Stegun - for m in range(1,l): - Pl[m,:] *= np.prod(rootl[l-m+1:l+m+1]) + for m in range(1, l): + Pl[m, :] *= np.prod(rootl[l - m + 1 : l + m + 1]) # sectoral case (l = m) should be done separately to handle 0! - Pl[l,:] *= np.prod(rootl[1:]) + Pl[l, :] *= np.prod(rootl[1:]) return Pl diff --git a/gravity_toolkit/legendre_polynomials.py b/gravity_toolkit/legendre_polynomials.py index 332e6a00..63601cf0 100755 --- a/gravity_toolkit/legendre_polynomials.py +++ b/gravity_toolkit/legendre_polynomials.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" legendre_polynomials.py Written by Tyler Sutterley (11/2024) @@ -44,8 +44,10 @@ added option ASTYPE to output as different variable types e.g. np.float64 Written 03/2013 """ + import numpy as np + def legendre_polynomials(lmax, x, ASTYPE=np.float64): """ Computes fully-normalized Legendre polynomials and their first derivative @@ -76,36 +78,38 @@ def legendre_polynomials(lmax, x, ASTYPE=np.float64): # verify data type of spherical harmonic truncation lmax = np.int64(lmax) # output matrix of normalized legendre polynomials - pl = np.zeros((lmax+1,nx),dtype=ASTYPE) + pl = np.zeros((lmax + 1, nx), dtype=ASTYPE) # output matrix of First derivative of Legendre polynomials - dpl = np.zeros((lmax+1,nx),dtype=ASTYPE) + dpl = np.zeros((lmax + 1, nx), dtype=ASTYPE) # dummy matrix for the recurrence relation - ptemp = np.zeros((lmax+1,nx),dtype=ASTYPE) + ptemp = np.zeros((lmax + 1, nx), dtype=ASTYPE) # u is sine of colatitude (cosine of latitude) so that 0 <= s <= 1 # for x=cos(th): u=sin(th) u = np.sqrt(1.0 - x**2) # update where u==0 to eps of data type to prevent invalid divisions - u0, = np.nonzero(u == 0) + (u0,) = np.nonzero(u == 0) u[u0] = np.finfo(u.dtype).eps # Initialize the recurrence relation - ptemp[0,:] = 1.0 - ptemp[1,:] = x + ptemp[0, :] = 1.0 + ptemp[1, :] = x # Normalization is geodesy convention - pl[0,:] = ptemp[0,:] - pl[1,:] = np.sqrt(3.0)*ptemp[1,:] - for l in range(2,lmax+1): - ptemp[l,:] = (((2.0*l)-1.0)/l)*x*ptemp[l-1,:] - ((l-1.0)/l)*ptemp[l-2,:] + pl[0, :] = ptemp[0, :] + pl[1, :] = np.sqrt(3.0) * ptemp[1, :] + for l in range(2, lmax + 1): + ptemp[l, :] = (((2.0 * l) - 1.0) / l) * x * ptemp[l - 1, :] - ( + (l - 1.0) / l + ) * ptemp[l - 2, :] # Normalization is geodesy convention - pl[l,:] = np.sqrt((2.0*l)+1.0)*ptemp[l,:] + pl[l, :] = np.sqrt((2.0 * l) + 1.0) * ptemp[l, :] # Overwrite polar case (x == +/-1) - pl[l,u0] = np.sqrt((2.0*l)+1.0)*x[u0]**l + pl[l, u0] = np.sqrt((2.0 * l) + 1.0) * x[u0] ** l # First derivative of Legendre polynomials - for l in range(1,lmax+1): - fl = np.sqrt(((l**2.0) * (2.0*l + 1.0)) / (2.0*l - 1.0)) - dpl[l,:] = (1.0/u)*(l*x*pl[l,:] - fl*pl[l-1,:]) + for l in range(1, lmax + 1): + fl = np.sqrt(((l**2.0) * (2.0 * l + 1.0)) / (2.0 * l - 1.0)) + dpl[l, :] = (1.0 / u) * (l * x * pl[l, :] - fl * pl[l - 1, :]) # return the legendre polynomials and their first derivative return (pl, dpl) diff --git a/gravity_toolkit/mascons.py b/gravity_toolkit/mascons.py index 2a8f08c1..c1fa2e71 100644 --- a/gravity_toolkit/mascons.py +++ b/gravity_toolkit/mascons.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" mascons.py -Written by Tyler Sutterley (03/2023) +Written by Tyler Sutterley (07/2026) Conversion routines for publicly available GRACE/GRACE-FO mascon solutions PYTHON DEPENDENCIES: @@ -12,6 +12,7 @@ mascon2grid.m written by Felix Landerer and David Wiese (JPL) UPDATE HISTORY: + Updated 07/2026: use np.radians to convert from degrees to radians Updated 03/2023: improve typing for variables in docstrings Updated 11/2022: use lowercase keyword arguments Updated 04/2022: updated docstrings to numpy documentation format @@ -25,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 @@ -63,15 +66,13 @@ def to_gsfc(gdata, lon, lat, lon_center, lat_center, lon_span, lat_span): # number of mascons nmas = len(lon_center) # convert mascon centers to -180:180 - gt180, = np.nonzero(lon_center > 180) - lon_center[gt180] -= 360.0 + lon_center = np.where(lon_center > 180, lon_center - 360.0, lon_center) # remove singleton dimensions lat = np.squeeze(lat) lon = np.squeeze(lon) # for mascons centered on 180: use 0:360 alon = np.copy(lon) - lt0, = np.nonzero(lon < 0) - alon[lt0] += 360.0 + alon = np.where(alon < 0, alon + 360.0, alon) # loop over each mascon bin and average gdata with the cos-lat weights # for that bin @@ -79,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.nonzero((lat >= lat_bound[0]) & (lat < lat_bound[1])) - J, = np.nonzero((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(lat[I]*np.pi/180.0) / - np.mean(np.cos(lat[I]*np.pi/180.0)))*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 @@ -140,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) @@ -149,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.nonzero((lat >= lat_bound[k,1]) & (lat < lat_bound[k,0])) - J, = np.nonzero((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(lat[I]*np.pi/180.0) / - np.mean(np.cos(lat[I]*np.pi/180.0)))*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` @@ -209,27 +225,32 @@ 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]) # number of mascons nmas = len(lon_center) # convert mascon centers to -180:180 - gt180, = np.nonzero(lon_center > 180) - lon_center[gt180] -= 360.0 + 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) - lt0, = np.nonzero(lon < 0) - alon[lt0] += 360.0 + alon = np.where(alon < 0, alon + 360.0, alon) # loop over each mascon bin and assign value to grid points inside bin: mdata = np.zeros((nlat, nlon)) @@ -237,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.nonzero((lat >= lat_bound[0]) & (lat < lat_bound[1])) - J, = np.nonzero((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']: @@ -263,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 @@ -290,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.nonzero((lat >= lat_bound[k,1]) & (lat < lat_bound[k,0])) - J, = np.nonzero((lon >= lon_bound[k,0]) & (lon < lon_bound[k,2])) - I,J = (I[np.newaxis,:], J[:,np.newaxis]) - mdata[I,J] = mscdata[k] + (I,) = np.flatnonzero( + (lat >= lat_bound[k, 1]) & (lat < lat_bound[k, 0]) + ) + (J,) = np.flatnonzero( + (lon >= lon_bound[k, 0]) & (lon < lon_bound[k, 2]) + ) + I, J = (I[np.newaxis, :], J[:, np.newaxis]) + mdata[I, J] = mscdata[k] # return array if kwargs['transpose']: diff --git a/gravity_toolkit/ocean_stokes.py b/gravity_toolkit/ocean_stokes.py index 1d723329..ab2ace73 100644 --- a/gravity_toolkit/ocean_stokes.py +++ b/gravity_toolkit/ocean_stokes.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" ocean_stokes.py Written by Tyler Sutterley (08/2023) @@ -59,13 +59,16 @@ Updated 05/2015: added parameter MMAX for MMAX != LMAX Written 03/2015 """ + import pathlib import numpy as np from gravity_toolkit.spatial import spatial from gravity_toolkit.gen_stokes import gen_stokes -def ocean_stokes(LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', - SIMPLIFY=False): + +def ocean_stokes( + LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', SIMPLIFY=False +): """ Reads a land-sea mask and converts to a series of spherical harmonics for ocean areas @@ -105,27 +108,36 @@ def ocean_stokes(LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = spatial().from_netCDF4(LANDMASK, - date=False, varname=VARNAME) + landsea = spatial().from_netCDF4(LANDMASK, date=False, varname=VARNAME) # create land function - nth,nphi = landsea.shape - land_function = np.zeros((nth,nphi), dtype=np.float64) + nth, nphi = landsea.shape + land_function = np.zeros((nth, nphi), dtype=np.float64) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + land_function[indx, indy] = 1.0 # remove isolated points if specified if SIMPLIFY: land_function -= find_isolated_points(land_function) # ocean function reciprocal of land function ocean_function = 1.0 - land_function # convert to spherical harmonics (1 cm w.e.) - Ylms = gen_stokes(ocean_function.T, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=LMAX, MMAX=MMAX, LOVE=LOVE) + Ylms = gen_stokes( + ocean_function.T, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=LMAX, + MMAX=MMAX, + LOVE=LOVE, + ) # return the spherical harmonic coefficients return Ylms -def land_stokes(LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', - SIMPLIFY=False): + +def land_stokes( + LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', SIMPLIFY=False +): """ Reads a land-sea mask and converts to a series of spherical harmonics for land areas @@ -165,23 +177,31 @@ def land_stokes(LANDMASK, LMAX, MMAX=None, LOVE=None, VARNAME='LSMASK', # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = spatial().from_netCDF4(LANDMASK, - date=False, varname=VARNAME) + landsea = spatial().from_netCDF4(LANDMASK, date=False, varname=VARNAME) # create land function - nth,nphi = landsea.shape - land_function = np.zeros((nth,nphi), dtype=np.float64) + nth, nphi = landsea.shape + land_function = np.zeros((nth, nphi), dtype=np.float64) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - land_function[indx,indy] = 1.0 + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + land_function[indx, indy] = 1.0 # remove isolated points if specified if SIMPLIFY: land_function -= find_isolated_points(land_function) # convert to spherical harmonics (1 cm w.e.) - Ylms = gen_stokes(land_function.T, landsea.lon, landsea.lat, - UNITS=1, LMIN=0, LMAX=LMAX, MMAX=MMAX, LOVE=LOVE) + Ylms = gen_stokes( + land_function.T, + landsea.lon, + landsea.lat, + UNITS=1, + LMIN=0, + LMAX=LMAX, + MMAX=MMAX, + LOVE=LOVE, + ) # return the spherical harmonic coefficients return Ylms + def find_isolated_points(mask): """ Simplify a mask by removing isolated points @@ -196,16 +216,16 @@ def find_isolated_points(mask): isolated: np.ndarray simplified land-sea mask """ - nth,_ = mask.shape - laplacian = -4.0*np.copy(mask) - laplacian += mask*np.roll(mask,1,axis=1) - laplacian += mask*np.roll(mask,-1,axis=1) - temp = np.roll(mask,1,axis=0) - temp[0,:] = mask[1,:] - laplacian += mask*temp - temp = np.roll(mask,-1,axis=0) - temp[nth-1,:] = mask[nth-2,:] - laplacian += mask*temp + nth, _ = mask.shape + laplacian = -4.0 * np.copy(mask) + laplacian += mask * np.roll(mask, 1, axis=1) + laplacian += mask * np.roll(mask, -1, axis=1) + temp = np.roll(mask, 1, axis=0) + temp[0, :] = mask[1, :] + laplacian += mask * temp + temp = np.roll(mask, -1, axis=0) + temp[nth - 1, :] = mask[nth - 2, :] + laplacian += mask * temp # create mask of isolated points isolated = np.where(np.abs(laplacian) >= 3, 1, 0) return isolated diff --git a/gravity_toolkit/read_GIA_model.py b/gravity_toolkit/read_GIA_model.py index 39da6de0..1ef260c4 100755 --- a/gravity_toolkit/read_GIA_model.py +++ b/gravity_toolkit/read_GIA_model.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" read_GIA_model.py Written by Tyler Sutterley (05/2023) @@ -141,6 +141,7 @@ Updated 12/2012: changed the naming scheme for Simpson and Whitehouse Updated 09/2012: combined several GIA read programs into this standard """ + import re import copy import logging @@ -148,8 +149,21 @@ import numpy as np from gravity_toolkit.harmonics import harmonics -_known_gia_types = ['IJ05-R2', 'W12a', 'SM09', 'ICE6G', 'ICE6G-D', 'Wu10', - 'AW13-ICE6G', 'AW13-IJ05', 'Caron', 'ascii', 'netCDF4', 'HDF5'] +_known_gia_types = [ + 'IJ05-R2', + 'W12a', + 'SM09', + 'ICE6G', + 'ICE6G-D', + 'Wu10', + 'AW13-ICE6G', + 'AW13-IJ05', + 'Caron', + 'ascii', + 'netCDF4', + 'HDF5', +] + def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): """ @@ -221,46 +235,52 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): gia_Ylms['title'] = None # GIA model citations and references - if (GIA == 'IJ05-R2'): + if GIA == 'IJ05-R2': # IJ05-R2: Ivins R2 GIA Models prefix = 'IJ05_R2' gia_Ylms['citation'] = 'Ivins_et_al._(2013)' - gia_Ylms['reference'] = ('E. R. Ivins, T. S. James, J. Wahr, ' + gia_Ylms['reference'] = ( + 'E. R. Ivins, T. S. James, J. Wahr, ' 'E. J. O. Schrama, F. W. Landerer, and K. M. Simon, "Antarctic ' 'contribution to sea level rise observed by GRACE with improved ' 'GIA correction", Journal of Geophysical Research: Solid Earth, ' - '118(6), 3126-3141, (2013). https://doi.org/10.1002/jgrb.50208') + '118(6), 3126-3141, (2013). https://doi.org/10.1002/jgrb.50208' + ) gia_Ylms['url'] = 'https://doi.org/10.1002/jgrb.50208' # regular expression file pattern file_pattern = r'Stokes.R2_(.*?)_L120' # default degree of truncation LMAX = 120 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'ICE6G'): + elif GIA == 'ICE6G': # ICE6G: ICE-6G VM5 GIA Models prefix = 'ICE6G' gia_Ylms['citation'] = 'Peltier_et_al._(2015)' - gia_Ylms['reference'] = ('W. R. Peltier, D. F. Argus, and R. Drummond, ' + gia_Ylms['reference'] = ( + 'W. R. Peltier, D. F. Argus, and R. Drummond, ' '"Space geodesy constrains ice age terminal deglaciation: The ' 'global ICE-6G_C (VM5a) model", Journal of Geophysical Research: ' 'Solid Earth, 120(1), 450-487, (2015). ' - 'https://doi.org/10.1002/2014JB011176') + 'https://doi.org/10.1002/2014JB011176' + ) gia_Ylms['url'] = 'https://doi.org/10.1002/2014JB011176' # regular expression file pattern for test cases - #file_pattern = r'Stokes_G_Rot_60_I6_A_(.*?)_L90' + # file_pattern = r'Stokes_G_Rot_60_I6_A_(.*?)_L90' # regular expression file pattern for VM5 file_pattern = r'Stokes_G_Rot_60_I6_A_(.*)' # default degree of truncation LMAX = 60 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'W12a'): + elif GIA == 'W12a': # W12a: Whitehouse GIA Models prefix = 'W12a' gia_Ylms['citation'] = 'Whitehouse_et_al._(2012)' - gia_Ylms['reference'] = ('P. L. Whitehouse, M. J. Bentley, G. A. Milne, ' + gia_Ylms['reference'] = ( + 'P. L. Whitehouse, M. J. Bentley, G. A. Milne, ' 'M. A. King, I. D. Thomas, "A new glacial isostatic adjustment ' 'model for Antarctica: calibrated and tested using observations ' 'of relative sea-level change and present-day uplift rates", ' 'Geophysical Journal International, 190(3), 1464-1482, (2012). ' - 'https://doi.org/10.1111/j.1365-246X.2012.05557.x') + 'https://doi.org/10.1111/j.1365-246X.2012.05557.x' + ) gia_Ylms['url'] = 'https://doi.org/10.1111/j.1365-246X.2012.05557.x' # for Whitehouse W12a (BEST, LOWER, UPPER): parameters = dict(B='Best', L='Lower', U='Upper') @@ -268,80 +288,92 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): file_pattern = r'grate_(B|L|U).clm' # default degree of truncation LMAX = 120 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'SM09'): + elif GIA == 'SM09': # SM09: Simpson/Milne GIA Models prefix = 'SM09_Huy2' gia_Ylms['citation'] = 'Simpson_et_al._(2009)' - gia_Ylms['reference'] = ('M. J. R. Simpson, G. A. Milne, P. Huybrechts, ' + gia_Ylms['reference'] = ( + 'M. J. R. Simpson, G. A. Milne, P. Huybrechts, ' 'A. J. Long, "Calibrating a glaciological model of the Greenland ' 'ice sheet from the Last Glacial Maximum to present-day using ' 'field observations of relative sea level and ice extent", ' 'Quaternary Science Reviews, 28(17-18), 1631-1657, (2009). ' - 'https://doi.org/10.1016/j.quascirev.2009.03.004') + 'https://doi.org/10.1016/j.quascirev.2009.03.004' + ) gia_Ylms['url'] = 'https://doi.org/10.1016/j.quascirev.2009.03.004' # regular expression file pattern file_pattern = r'grate_(\d+)p(\d)(\d+).clm' # default degree of truncation LMAX = 120 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'Wu10'): + elif GIA == 'Wu10': # Wu10: Wu (2010) GIA Correction gia_Ylms['citation'] = 'Wu_et_al._(2010)' - gia_Ylms['reference'] = ('X. Wu, M. B. Heflin, H. Schotman, B. L. A. ' + gia_Ylms['reference'] = ( + 'X. Wu, M. B. Heflin, H. Schotman, B. L. A. ' 'Vermeersen, D. Dong, R. S. Gross, E. R. Ivins, A. W. Moore, S. E. ' 'Owen, "Simultaneous estimation of global present-day water ' 'transport and glacial isostatic adjustment", Nature Geoscience, ' - '3(9), 642-646, (2010). https://doi.org/10.1038/ngeo938') + '3(9), 642-646, (2010). https://doi.org/10.1038/ngeo938' + ) gia_Ylms['url'] = 'https://doi.org/10.1038/ngeo938' # default degree of truncation LMAX = 60 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'Caron'): + elif GIA == 'Caron': # Caron: Caron JPL GIA Assimilation gia_Ylms['citation'] = 'Caron_et_al._(2018)' - gia_Ylms['reference'] = ('L. Caron, E. R. Ivins, E. Larour, S. Adhikari, ' + gia_Ylms['reference'] = ( + 'L. Caron, E. R. Ivins, E. Larour, S. Adhikari, ' 'J. Nilsson and G. Blewitt, "GIA Model Statistics for GRACE ' 'Hydrology, Cryosphere, and Ocean Science", Geophysical Research ' 'Letters, 45(5), 2203-2212, (2018). ' - 'https://doi.org/10.1002/2017GL076644') + 'https://doi.org/10.1002/2017GL076644' + ) gia_Ylms['url'] = 'https://doi.org/10.1002/2017GL076644' # default degree of truncation LMAX = 89 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'ICE6G-D'): + elif GIA == 'ICE6G-D': # ICE6G-D: ICE-6G Version-D GIA Models prefix = 'ICE6G-D' gia_Ylms['citation'] = 'Peltier_et_al._(2018)' - gia_Ylms['reference'] = ('W. R. Peltier, D. F. Argus, and R. Drummond, ' + gia_Ylms['reference'] = ( + 'W. R. Peltier, D. F. Argus, and R. Drummond, ' '"Comment on "An assessment of the ICE-6G_C (VM5a) glacial ' 'isostatic adjustment model" by Purcell et al.", Journal of ' 'Geophysical Research: Solid Earth, 123(2), 2019-2028, (2018). ' - 'https://doi.org/10.1002/2016JB013844') + 'https://doi.org/10.1002/2016JB013844' + ) gia_Ylms['url'] = 'https://doi.org/10.1002/2016JB013844' # regular expression file pattern for Version-D file_pattern = r'(ICE-6G_)?(.*?)[_]?Stokes_trend[_]?(.*?)\.txt$' # default degree of truncation LMAX = 256 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'AW13-ICE6G'): + elif GIA == 'AW13-ICE6G': # AW13-ICE6G: Geruo A ICE-6G GIA Models prefix = 'AW13' gia_Ylms['citation'] = 'A_et_al._(2013)' - gia_Ylms['reference'] = ('G. A, J. Wahr, and S. Zhong, "Computations of ' + gia_Ylms['reference'] = ( + 'G. A, J. Wahr, and S. Zhong, "Computations of ' 'the viscoelastic response of a 3-D compressible Earth to surface ' 'loading; an application to Glacial Isostatic Adjustment in ' 'Antarctica and Canada", Geophysical Journal International, ' - '192(2), 557-572, (2013). https://doi.org/10.1093/gji/ggs030') + '192(2), 557-572, (2013). https://doi.org/10.1093/gji/ggs030' + ) gia_Ylms['url'] = 'https://doi.org/10.1093/gji/ggs030' # regular expressions file pattern file_pattern = r'stokes\.(ice6g)[\.\_](.*?)(\.txt)?$' # default degree of truncation LMAX = 100 if not kwargs['LMAX'] else kwargs['LMAX'] - elif (GIA == 'AW13-IJ05'): + elif GIA == 'AW13-IJ05': # AW13-IJ05: Geruo A IJ05-R2 GIA Models prefix = 'AW13_IJ05' gia_Ylms['citation'] = 'A_et_al._(2013)' - gia_Ylms['reference'] = ('G. A, J. Wahr, and S. Zhong, "Computations of ' + gia_Ylms['reference'] = ( + 'G. A, J. Wahr, and S. Zhong, "Computations of ' 'the viscoelastic response of a 3-D compressible Earth to surface ' 'loading; an application to Glacial Isostatic Adjustment in ' 'Antarctica and Canada", Geophysical Journal International, ' - '192(2), 557-572, (2013). https://doi.org/10.1093/gji/ggs030') + '192(2), 557-572, (2013). https://doi.org/10.1093/gji/ggs030' + ) gia_Ylms['url'] = 'https://doi.org/10.1093/gji/ggs030' # regular expressions file pattern file_pattern = r'stokes\.(R2)_(.*?)(\_ANT)?$' @@ -360,7 +392,7 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): start = 0 # scale factor for geodesy normalization scale = 1e-11 - elif (GIA == 'ICE6G-D'): + elif GIA == 'ICE6G-D': # ICE-6G Version-D # scale factor for geodesy normalization scale = 1.0 @@ -370,10 +402,10 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): # initially read for spherical harmonic degree up to LMAX # will truncate to MMAX before exiting program - gia_Ylms['clm'] = np.zeros((LMAX+1,LMAX+1)) - gia_Ylms['slm'] = np.zeros((LMAX+1,LMAX+1)) + gia_Ylms['clm'] = np.zeros((LMAX + 1, LMAX + 1)) + gia_Ylms['slm'] = np.zeros((LMAX + 1, LMAX + 1)) # output spherical harmonic degree and order - gia_Ylms['l'],gia_Ylms['m'] = (np.arange(LMAX+1),np.arange(LMAX+1)) + gia_Ylms['l'], gia_Ylms['m'] = (np.arange(LMAX + 1), np.arange(LMAX + 1)) # if reading a GIA model if GIA in _known_gia_types: @@ -398,23 +430,23 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): gia_lines = len(gia_data) # Skipping file header for geruo files with header - for ii in range(start,gia_lines): + for ii in range(start, gia_lines): # check if contents in line - flag = bool(rx.search(gia_data[ii].replace('D','E'))) + flag = bool(rx.search(gia_data[ii].replace('D', 'E'))) if flag: # find numerical instances in line including exponents, # decimal points and negatives # Replacing Double Exponent with Standard Exponent - line = rx.findall(gia_data[ii].replace('D','E')) + line = rx.findall(gia_data[ii].replace('D', 'E')) l1 = np.int64(line[0]) m1 = np.int64(line[1]) # truncate to LMAX if (l1 <= LMAX) and (m1 <= LMAX): # scaling to geodesy normalization - gia_Ylms['clm'][l1,m1] = np.float64(line[2])*scale - gia_Ylms['slm'][l1,m1] = np.float64(line[3])*scale + gia_Ylms['clm'][l1, m1] = np.float64(line[2]) * scale + gia_Ylms['slm'][l1, m1] = np.float64(line[3]) * scale - elif (GIA == 'ICE6G'): + elif GIA == 'ICE6G': # ICE-6G VM5 notes # need to scale by 1e-11 for geodesy-normalization # spherical harmonic degrees listed only on order 0 @@ -426,9 +458,9 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): # counter variable ii = 0 - for l in range(0, LMAX+1): - for m in range(0, l+1): - if ((m % 2) == 0): + for l in range(0, LMAX + 1): + for m in range(0, l + 1): + if (m % 2) == 0: # reading gia line if the order is even # find numerical instances in line including exponents, # decimal points and negatives @@ -437,22 +469,22 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): ii += 1 # if m is even: clm column = 1, slm column = 2 c = 0 - else: # if m is odd: clm column = 3, slm column = 4 + else: # if m is odd: clm column = 3, slm column = 4 c = 2 - if ((m == 0) or (m == 1)): + if (m == 0) or (m == 1): # l is column 1 if m == 0 or 1 # degree is not listed for other SHd: column 1 = clm c += 1 - if (len(line) > 0): + if len(line) > 0: # no empty lines # convert to float and scale - gia_Ylms['clm'][l,m] = np.float64(line[0+c])*scale - gia_Ylms['slm'][l,m] = np.float64(line[1+c])*scale + gia_Ylms['clm'][l, m] = np.float64(line[0 + c]) * scale + gia_Ylms['slm'][l, m] = np.float64(line[1 + c]) * scale - elif (GIA == 'Wu10'): + elif GIA == 'Wu10': # Wu (2010) notes: # Need to convert from mm geoid to fully normalized - rad_e = 6.371e9# Average Radius of the Earth [mm] + rad_e = 6.371e9 # Average Radius of the Earth [mm] # note: the GIA file starts with a header # converting to numerical array (note 64 bit floating point) @@ -467,26 +499,26 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): # 1 1 s # 2 0 c # 2 1 c - for l in range(1, LMAX+1): - for m in range(0, l+1): + for l in range(1, LMAX + 1): + for m in range(0, l + 1): for cs in range(0, 2): # unwrapping GIA file and converting to geoid # Clm - if (cs == 0): - gia_Ylms['clm'][l,m] = gia_data[ii]/rad_e + if cs == 0: + gia_Ylms['clm'][l, m] = gia_data[ii] / rad_e ii += 1 # Slm if (m != 0) and (cs == 1): - gia_Ylms['slm'][l,m] = gia_data[ii]/rad_e + gia_Ylms['slm'][l, m] = gia_data[ii] / rad_e ii += 1 - elif (GIA == 'Caron'): + elif GIA == 'Caron': # Caron et al. (2018) # note: the GIA file starts with a header. # converting to numerical array (note 64 bit floating point) - dtype = {'names':('l','m','Ylms'),'formats':('i','i','f8')} - gia_data=np.loadtxt(input_file, skiprows=4, dtype=dtype) + dtype = {'names': ('l', 'm', 'Ylms'), 'formats': ('i', 'i', 'f8')} + gia_data = np.loadtxt(input_file, skiprows=4, dtype=dtype) # Order of harmonics in the file # 0 0 c # 1 1 s @@ -497,16 +529,15 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): # 2 0 c # 2 1 c # 2 2 c - for l,m,Ylm in zip(gia_data['l'],gia_data['m'],gia_data['Ylms']): + for l, m, Ylm in zip(gia_data['l'], gia_data['m'], gia_data['Ylms']): # unwrapping GIA file - if (m >= 0) and (l <= LMAX) and (m <= LMAX):# Clm - gia_Ylms['clm'][l,m] = Ylm.copy() - elif (m < 0) and (l <= LMAX) and (m <= LMAX):# Slm - gia_Ylms['slm'][l,np.abs(m)] = Ylm.copy() + if (m >= 0) and (l <= LMAX) and (m <= LMAX): # Clm + gia_Ylms['clm'][l, m] = Ylm.copy() + elif (m < 0) and (l <= LMAX) and (m <= LMAX): # Slm + gia_Ylms['slm'][l, np.abs(m)] = Ylm.copy() # Reading ICE-6G Version-D GIA files - elif (GIA == 'ICE6G-D'): - + elif GIA == 'ICE6G-D': # opening GIA data file and read contents with input_file.open(mode='r', encoding='utf8') as f: gia_data = f.read().splitlines() @@ -517,48 +548,47 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): h1 = r'^GRACE Approximation for degrees 0 to 2' h2 = r'^GRACE Approximation\/Absolute Sea-level Values for degrees \> 2' # header lines to skip - header, = [(i+1) for i,l in enumerate(gia_data) if re.match(h1,l)] - start, = [(i+1) for i,l in enumerate(gia_data) if re.match(h2,l)] + (header,) = [(i + 1) for i, l in enumerate(gia_data) if re.match(h1, l)] + (start,) = [(i + 1) for i, l in enumerate(gia_data) if re.match(h2, l)] # Calculating number of cos and sin harmonics to read from header - n_harm = (2**2 + 3*2)//2 + 1 + n_harm = (2**2 + 3 * 2) // 2 + 1 # extract header for GRACE approximation - for ii in range(header,header+n_harm): + for ii in range(header, header + n_harm): # check if contents in line - flag = bool(rx.search(gia_data[ii].replace('D','E'))) + flag = bool(rx.search(gia_data[ii].replace('D', 'E'))) if flag: # find numerical instances in line including exponents, # decimal points and negatives # Replacing Double Exponent with Standard Exponent - line = rx.findall(gia_data[ii].replace('D','E')) + line = rx.findall(gia_data[ii].replace('D', 'E')) l1 = np.int64(line[0]) m1 = np.int64(line[1]) # truncate to LMAX if (l1 <= LMAX) and (m1 <= LMAX): # scaling to geodesy normalization - gia_Ylms['clm'][l1,m1] = np.float64(line[2])*scale - gia_Ylms['slm'][l1,m1] = np.float64(line[3])*scale + gia_Ylms['clm'][l1, m1] = np.float64(line[2]) * scale + gia_Ylms['slm'][l1, m1] = np.float64(line[3]) * scale # Skipping rest of file header - for ii in range(start,gia_lines): + for ii in range(start, gia_lines): # check if contents in line - flag = bool(rx.search(gia_data[ii].replace('D','E'))) + flag = bool(rx.search(gia_data[ii].replace('D', 'E'))) if flag: # find numerical instances in line including exponents, # decimal points and negatives # Replacing Double Exponent with Standard Exponent - line = rx.findall(gia_data[ii].replace('D','E')) + line = rx.findall(gia_data[ii].replace('D', 'E')) l1 = np.int64(line[0]) m1 = np.int64(line[1]) # truncate to LMAX if (l1 <= LMAX) and (m1 <= LMAX): # scaling to geodesy normalization - gia_Ylms['clm'][l1,m1] = np.float64(line[2])*scale - gia_Ylms['slm'][l1,m1] = np.float64(line[3])*scale - + gia_Ylms['clm'][l1, m1] = np.float64(line[2]) * scale + gia_Ylms['slm'][l1, m1] = np.float64(line[3]) * scale # ascii: reformatted GIA in ascii format - elif (GIA == 'ascii'): + elif GIA == 'ascii': # reading GIA data from reformatted (simplified) ascii files Ylms = harmonics().from_ascii(input_file, date=False) Ylms.truncate(LMAX) @@ -571,65 +601,68 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): # netCDF4: reformatted GIA in netCDF4 format # HDF5: reformatted GIA in HDF5 format - elif GIA in ('netCDF4','HDF5'): + elif GIA in ('netCDF4', 'HDF5'): # reading GIA data from reformatted netCDF4 and HDF5 files Ylms = harmonics().from_file(input_file, format=GIA, date=False) Ylms.truncate(LMAX) gia_Ylms.update(Ylms.to_dict()) # copy title and reference for model - for att_name in ('title','citation','reference','url'): + for att_name in ('title', 'citation', 'reference', 'url'): try: - s, = [s for s in Ylms.attributes['ROOT'].keys() if - re.match(att_name,s,re.I)] + (s,) = [ + s + for s in Ylms.attributes['ROOT'].keys() + if re.match(att_name, s, re.I) + ] gia_Ylms[att_name] = Ylms.attributes['ROOT'][s] except (ValueError, KeyError, AttributeError): gia_Ylms[att_name] = None # GIA model parameter strings # extract rheology from the file name - if (GIA == 'IJ05-R2'): + if GIA == 'IJ05-R2': # IJ05-R2: Ivins R2 GIA Models # adding file specific earth parameters - parameters, = re.findall(file_pattern, input_file.name) + (parameters,) = re.findall(file_pattern, input_file.name) gia_Ylms['title'] = f'{prefix}_{parameters}' - elif (GIA == 'ICE6G'): + elif GIA == 'ICE6G': # ICE6G: ICE-6G GIA Models # adding file specific earth parameters - parameters, = re.findall(file_pattern, input_file.name) + (parameters,) = re.findall(file_pattern, input_file.name) gia_Ylms['title'] = f'{prefix}_{parameters}' - elif (GIA == 'W12a'): + elif GIA == 'W12a': # W12a: Whitehouse GIA Models # for Whitehouse W12a (BEST, LOWER, UPPER): model = re.findall(file_pattern, input_file.name).pop() gia_Ylms['title'] = f'{prefix}_{parameters[model]}' - elif (GIA == 'SM09'): + elif GIA == 'SM09': # SM09: Simpson/Milne GIA Models # making parameters in the file similar to IJ05 # split rheological parameters between lithospheric thickness, # upper mantle viscosity and lower mantle viscosity - LTh,UMV,LMV = re.findall(file_pattern, input_file.name).pop() + LTh, UMV, LMV = re.findall(file_pattern, input_file.name).pop() # formatting rheology parameters similar to IJ05 models gia_Ylms['title'] = f'{prefix}_{LTh}_.{UMV}_{LMV}' - elif (GIA == 'Wu10'): + elif GIA == 'Wu10': # Wu10: Wu (2010) GIA Correction gia_Ylms['title'] = 'Wu_2010' - elif (GIA == 'Caron'): + elif GIA == 'Caron': # Caron: Caron JPL GIA Assimilation gia_Ylms['title'] = 'Caron_expt' - elif (GIA == 'ICE6G-D'): + elif GIA == 'ICE6G-D': # ICE6G-D: ICE-6G Version-D GIA Models # adding file specific earth parameters - m1,p1,p2 = re.findall(file_pattern, input_file.name).pop() + m1, p1, p2 = re.findall(file_pattern, input_file.name).pop() gia_Ylms['title'] = f'{prefix}_{p1}{p2}' - elif (GIA == 'AW13-ICE6G'): + elif GIA == 'AW13-ICE6G': # AW13-ICE6G: Geruo A ICE-6G GIA Models # extract the ice history and case flags - hist,case,sf=re.findall(file_pattern, input_file.name).pop() + hist, case, sf = re.findall(file_pattern, input_file.name).pop() gia_Ylms['title'] = f'{prefix}_{hist}_{case}' - elif (GIA == 'AW13-IJ05'): + elif GIA == 'AW13-IJ05': # AW13-IJ05: Geruo A IJ05-R2 GIA Models # adding file specific earth parameters - vrs,param,aux=re.findall(file_pattern, input_file.name).pop() + vrs, param, aux = re.findall(file_pattern, input_file.name).pop() gia_Ylms['title'] = f'{prefix}_{vrs}_{param}' # output harmonics to ascii, netCDF4 or HDF5 file @@ -641,22 +674,28 @@ def read_GIA_model(input_file, GIA=None, MMAX=None, DATAFORM=None, **kwargs): suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') filename = f'stokes_{title}_L{LMAX:d}.{suffix[DATAFORM]}' output_file = input_file.with_name(filename) - Ylms.to_file(output_file, format=DATAFORM, date=False, - title=title, reference=gia_Ylms['reference']) + Ylms.to_file( + output_file, + format=DATAFORM, + date=False, + title=title, + reference=gia_Ylms['reference'], + ) # set permissions level of output file output_file.chmod(mode=kwargs['MODE']) # truncate to MMAX if specified if MMAX is not None: # spherical harmonic variables - gia_Ylms['clm'] = gia_Ylms['clm'][:,:MMAX+1] - gia_Ylms['slm'] = gia_Ylms['slm'][:,:MMAX+1] + gia_Ylms['clm'] = gia_Ylms['clm'][:, : MMAX + 1] + gia_Ylms['slm'] = gia_Ylms['slm'][:, : MMAX + 1] # spherical harmonic order - gia_Ylms['m'] = gia_Ylms['m'][:MMAX+1] + gia_Ylms['m'] = gia_Ylms['m'][: MMAX + 1] # return the harmonics and the parameters return gia_Ylms + class gia(harmonics): """ Inheritance of ``harmonics`` class for reading Glacial @@ -677,7 +716,9 @@ class gia(harmonics): filename: str input or output filename """ + np.seterr(invalid='ignore') + # inherit harmonics class to read GIA models def __init__(self, **kwargs): super().__init__(**kwargs) @@ -714,15 +755,19 @@ def from_GIA(self, filename, **kwargs): # set filename self.case_insensitive_filename(filename) # set default keyword arguments - kwargs.setdefault('GIA',None) - kwargs.setdefault('mmax',None) - kwargs.setdefault('verbose',False) + kwargs.setdefault('GIA', None) + kwargs.setdefault('mmax', None) + kwargs.setdefault('verbose', False) # catch case where MMAX is entered - if ('MMAX' in kwargs.keys()): + if 'MMAX' in kwargs.keys(): kwargs['mmax'] = np.copy(kwargs['mmax']) # read data from GIA file - Ylms = read_GIA_model(self.filename, GIA=kwargs['GIA'], - LMAX=self.lmax, MMAX=kwargs['mmax']) + Ylms = read_GIA_model( + self.filename, + GIA=kwargs['GIA'], + LMAX=self.lmax, + MMAX=kwargs['mmax'], + ) # Output file information logging.info(self.filename) logging.info(list(Ylms.keys())) diff --git a/gravity_toolkit/read_GRACE_harmonics.py b/gravity_toolkit/read_GRACE_harmonics.py index eeb6cc77..70868ad8 100644 --- a/gravity_toolkit/read_GRACE_harmonics.py +++ b/gravity_toolkit/read_GRACE_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" read_GRACE_harmonics.py Written by Tyler Sutterley (11/2024) Contributions by Hugo Lecomte @@ -67,6 +67,7 @@ output file headers and parse new YAML headers for RL06 and GRACE-FO Written 10/2017 for public release """ + import re import io import gzip @@ -75,6 +76,7 @@ import numpy as np import gravity_toolkit.time + # PURPOSE: read Level-2 GRACE and GRACE-FO spherical harmonic files def read_GRACE_harmonics(input_file, LMAX, **kwargs): """ @@ -119,9 +121,9 @@ def read_GRACE_harmonics(input_file, LMAX, **kwargs): kwargs.setdefault('POLE_TIDE', False) # parse filename - PFX,SY,SD,EY,ED,N,PRC,F1,DRL,F2,SFX = parse_file(input_file) + PFX, SY, SD, EY, ED, N, PRC, F1, DRL, F2, SFX = parse_file(input_file) # check if file is compressed - compressed = (SFX == '.gz') + compressed = SFX == '.gz' # extract file contents file_contents = extract_file(input_file, compressed) @@ -141,12 +143,12 @@ def read_GRACE_harmonics(input_file, LMAX, **kwargs): # GFC solutions from the GFZ ICGEM # https://icgem.gfz-potsdam.de/sl/temporal elif PRC in ('COSTG',) or SFX in ('.gfc',): - DSET, = re.findall(r'(GSM|GAA|GAB|GAC|GAD)', PFX) + (DSET,) = re.findall(r'(GSM|GAA|GAB|GAC|GAD)', PFX) DREL = np.int64(DRL) FLAG = r'gfc' # Standard GRACE/GRACE-FO Level-2 solutions else: - DSET, = re.findall(r'(GSM|GAA|GAB|GAC|GAD)', PFX) + (DSET,) = re.findall(r'(GSM|GAA|GAB|GAC|GAD)', PFX) DREL = np.int64(DRL) FLAG = r'GRCOF2' @@ -162,51 +164,71 @@ def read_GRACE_harmonics(input_file, LMAX, **kwargs): dpy = gravity_toolkit.time.calendar_days(start_yr).sum() # For data that crosses years (end_yr - start_yr should be at most 1) - end_cyclic = ((end_yr - start_yr)*dpy+end_day) + end_cyclic = (end_yr - start_yr) * dpy + end_day # Calculate mid-month value mid_day = np.mean([start_day, end_cyclic]) # Calculating the mid-month date in decimal form - grace_L2_input['time'] = start_yr + mid_day/dpy + grace_L2_input['time'] = start_yr + mid_day / dpy # Calculating the Julian dates of the start and end date - grace_L2_input['start'] = 2400000.5 + \ - gravity_toolkit.time.convert_calendar_dates(start_yr,1.0,start_day, - epoch=(1858,11,17,0,0,0)) - grace_L2_input['end'] = 2400000.5 + \ - gravity_toolkit.time.convert_calendar_dates(end_yr,1.0,end_day, - epoch=(1858,11,17,0,0,0)) + grace_L2_input['start'] = ( + 2400000.5 + + gravity_toolkit.time.convert_calendar_dates( + start_yr, 1.0, start_day, epoch=(1858, 11, 17, 0, 0, 0) + ) + ) + grace_L2_input['end'] = ( + 2400000.5 + + gravity_toolkit.time.convert_calendar_dates( + end_yr, 1.0, end_day, epoch=(1858, 11, 17, 0, 0, 0) + ) + ) # set maximum spherical harmonic order - MMAX = np.copy(LMAX) if (kwargs['MMAX'] is None) else np.copy(kwargs['MMAX']) + MMAX = ( + np.copy(LMAX) if (kwargs['MMAX'] is None) else np.copy(kwargs['MMAX']) + ) # output dimensions - grace_L2_input['l'] = np.arange(LMAX+1) - grace_L2_input['m'] = np.arange(MMAX+1) + grace_L2_input['l'] = np.arange(LMAX + 1) + grace_L2_input['m'] = np.arange(MMAX + 1) # Spherical harmonic coefficient matrices to be filled from data file - grace_L2_input['clm'] = np.zeros((LMAX+1, MMAX+1)) - grace_L2_input['slm'] = np.zeros((LMAX+1, MMAX+1)) + grace_L2_input['clm'] = np.zeros((LMAX + 1, MMAX + 1)) + grace_L2_input['slm'] = np.zeros((LMAX + 1, MMAX + 1)) # spherical harmonic uncalibrated standard deviations - grace_L2_input['eclm'] = np.zeros((LMAX+1, MMAX+1)) - grace_L2_input['eslm'] = np.zeros((LMAX+1, MMAX+1)) - if ((DREL == 4) and (DSET == 'GSM')): + grace_L2_input['eclm'] = np.zeros((LMAX + 1, MMAX + 1)) + grace_L2_input['eslm'] = np.zeros((LMAX + 1, MMAX + 1)) + if (DREL == 4) and (DSET == 'GSM'): # clm and slm drift rates for RL04 - drift_c = np.zeros((LMAX+1, MMAX+1)) - drift_s = np.zeros((LMAX+1, MMAX+1)) + drift_c = np.zeros((LMAX + 1, MMAX + 1)) + drift_s = np.zeros((LMAX + 1, MMAX + 1)) # set default degree 0 harmonics for intercomparability between centers grace_L2_input['clm'][0, 0] = 1.0 # extract GRACE and GRACE-FO file headers # replace colons in header if within quotations - head = [re.sub(r'\"(.*?)\:\s(.*?)\"',r'"\1, \2"',l) for l in file_contents - if not re.match(rf'{FLAG}|GRDOTA',l)] + head = [ + re.sub(r'\"(.*?)\:\s(.*?)\"', r'"\1, \2"', l) + for l in file_contents + if not re.match(rf'{FLAG}|GRDOTA', l) + ] if SFX in ('.gfc',): # extract parameters from header - header_parameters = ['modelname','earth_gravity_constant','radius', - 'max_degree','errors','norm','tide_system'] + header_parameters = [ + 'modelname', + 'earth_gravity_constant', + 'radius', + 'max_degree', + 'errors', + 'norm', + 'tide_system', + ] header_regex = re.compile(r'(' + r'|'.join(header_parameters) + r')') header = [l.split(maxsplit=1) for l in head if header_regex.match(l)] - grace_L2_input['header'] = {i[0]:i[1] for i in header} + grace_L2_input['header'] = {i[0]: i[1] for i in header} elif ((N == 'GRAC') and (DREL >= 6)) or (N == 'GRFO'): # parse the YAML header for RL06 or GRACE-FO (specifying yaml loader) - grace_L2_input.update(yaml.load('\n'.join(head),Loader=yaml.BaseLoader)) + grace_L2_input.update( + yaml.load('\n'.join(head), Loader=yaml.BaseLoader) + ) else: # save lines of the GRACE file header removing empty lines grace_L2_input['header'] = [l.rstrip() for l in head if l] @@ -214,80 +236,85 @@ def read_GRACE_harmonics(input_file, LMAX, **kwargs): # for each line in the GRACE/GRACE-FO file for line in file_contents: # find if line starts with data marker flag (e.g. GRCOF2) - if bool(re.match(FLAG,line)): + if bool(re.match(FLAG, line)): # split the line into individual components line_contents = line.split() # degree and order for the line l1 = np.int64(line_contents[1]) m1 = np.int64(line_contents[2]) # if degree and order are below the truncation limits - if ((l1 <= LMAX) and (m1 <= MMAX)): - grace_L2_input['clm'][l1,m1] = np.float64(line_contents[3]) - grace_L2_input['slm'][l1,m1] = np.float64(line_contents[4]) - grace_L2_input['eclm'][l1,m1] = np.float64(line_contents[5]) - grace_L2_input['eslm'][l1,m1] = np.float64(line_contents[6]) + if (l1 <= LMAX) and (m1 <= MMAX): + grace_L2_input['clm'][l1, m1] = np.float64(line_contents[3]) + grace_L2_input['slm'][l1, m1] = np.float64(line_contents[4]) + grace_L2_input['eclm'][l1, m1] = np.float64(line_contents[5]) + grace_L2_input['eslm'][l1, m1] = np.float64(line_contents[6]) # find if line starts with drift rate flag - elif bool(re.match(r'GRDOTA',line)): + elif bool(re.match(r'GRDOTA', line)): # split the line into individual components line_contents = line.split() l1 = np.int64(line_contents[1]) m1 = np.int64(line_contents[2]) # Reading Drift rates for low degree harmonics - drift_c[l1,m1] = np.float64(line_contents[3]) - drift_s[l1,m1] = np.float64(line_contents[4]) + drift_c[l1, m1] = np.float64(line_contents[3]) + drift_s[l1, m1] = np.float64(line_contents[4]) # Adding drift rates to clm and slm for RL04 # if drift rates exist at any time, will add to harmonics # Will convert the secular rates into a stokes contribution # Currently removes 2003.3 to get the temporal average close to 0. - if ((DREL == 4) and (DSET == 'GSM')): + if (DREL == 4) and (DSET == 'GSM'): # time since 2003.3 - dt = (grace_L2_input['time'] - 2003.3) - grace_L2_input['clm'][:,:] += dt*drift_c[:,:] - grace_L2_input['slm'][:,:] += dt*drift_s[:,:] + dt = grace_L2_input['time'] - 2003.3 + grace_L2_input['clm'][:, :] += dt * drift_c[:, :] + grace_L2_input['slm'][:, :] += dt * drift_s[:, :] # Correct Pole Tide following Wahr et al. (2015) 10.1002/2015JB011986 if kwargs['POLE_TIDE'] and (DSET == 'GSM'): # time since 2000.0 - dt = (grace_L2_input['time']-2000.0) + dt = grace_L2_input['time'] - 2000.0 # CSR and JPL Pole Tide Correction - if PRC in ('UTCSR','JPLEM','JPLMSC'): + if PRC in ('UTCSR', 'JPLEM', 'JPLMSC'): # values for IERS mean pole [2010] - if (grace_L2_input['time'] < 2010.0): - a = np.array([0.055974,1.8243e-3,1.8413e-4,7.024e-6]) - b = np.array([-0.346346,-1.7896e-3,1.0729e-4,0.908e-6]) - elif (grace_L2_input['time'] >= 2010.0): - a = np.array([0.023513,7.6141e-3,0.0,0.0]) - b = np.array([-0.358891,0.6287e-3,0.0,0.0]) + if grace_L2_input['time'] < 2010.0: + a = np.array([0.055974, 1.8243e-3, 1.8413e-4, 7.024e-6]) + b = np.array([-0.346346, -1.7896e-3, 1.0729e-4, 0.908e-6]) + elif grace_L2_input['time'] >= 2010.0: + a = np.array([0.023513, 7.6141e-3, 0.0, 0.0]) + b = np.array([-0.358891, 0.6287e-3, 0.0, 0.0]) # calculate m1 and m2 values m1 = np.copy(a[0]) m2 = np.copy(b[0]) - for x in range(1,4): - m1 += a[x]*dt**x - m2 += b[x]*dt**x + for x in range(1, 4): + m1 += a[x] * dt**x + m2 += b[x] * dt**x # pole tide values for CSR and JPL # CSR and JPL both remove the IERS mean pole from m1 and m2 # before computing their harmonic solutions - C21_PT = -1.551e-9*(m1 - 0.62e-3*dt) - 0.012e-9*(m2 + 3.48e-3*dt) - S21_PT = 0.021e-9*(m1 - 0.62e-3*dt) - 1.505e-9*(m2 + 3.48e-3*dt) + C21_PT = -1.551e-9 * (m1 - 0.62e-3 * dt) - 0.012e-9 * ( + m2 + 3.48e-3 * dt + ) + S21_PT = 0.021e-9 * (m1 - 0.62e-3 * dt) - 1.505e-9 * ( + m2 + 3.48e-3 * dt + ) # correct GRACE/GRACE-FO spherical harmonics for pole tide - grace_L2_input['clm'][2,1] -= C21_PT - grace_L2_input['slm'][2,1] -= S21_PT + grace_L2_input['clm'][2, 1] -= C21_PT + grace_L2_input['slm'][2, 1] -= S21_PT # GFZ Pole Tide Correction - elif PRC in ('EIGEN','GFZOP'): + elif PRC in ('EIGEN', 'GFZOP'): # pole tide values for GFZ # GFZ removes only a constant pole position - C21_PT = -1.551e-9*(-0.62e-3*dt) - 0.012e-9*(3.48e-3*dt) - S21_PT = 0.021e-9*(-0.62e-3*dt) - 1.505e-9*(3.48e-3*dt) + C21_PT = -1.551e-9 * (-0.62e-3 * dt) - 0.012e-9 * (3.48e-3 * dt) + S21_PT = 0.021e-9 * (-0.62e-3 * dt) - 1.505e-9 * (3.48e-3 * dt) # correct GRACE/GRACE-FO spherical harmonics for pole tide - grace_L2_input['clm'][2,1] -= C21_PT - grace_L2_input['slm'][2,1] -= S21_PT + grace_L2_input['clm'][2, 1] -= C21_PT + grace_L2_input['slm'][2, 1] -= S21_PT # return the header data, GRACE/GRACE-FO data # GRACE/GRACE-FO date (mid-month in decimal) # and the start and end days as Julian dates return grace_L2_input + # PURPOSE: extract parameters from filename def parse_file(input_file): """ @@ -309,8 +336,10 @@ def parse_file(input_file): # GRGS: CNES Groupe de Recherche de Geodesie Spatiale centers = r'UTCSR|EIGEN|GFZOP|JPLEM|JPLMSC|GRGS|COSTG|GRGS' suffixes = r'\.gz|\.gfc|\.txt' - regex_pattern = (r'(.*?)-2_(\d{4})(\d{3})-(\d{4})(\d{3})_' - rf'(.*?)_({centers})_(.*?)_(\d+)(.*?)({suffixes})?$') + regex_pattern = ( + r'(.*?)-2_(\d{4})(\d{3})-(\d{4})(\d{3})_' + rf'(.*?)_({centers})_(.*?)_(\d+)(.*?)({suffixes})?$' + ) rx = re.compile(regex_pattern, re.VERBOSE) # extract parameters from input filename if isinstance(input_file, io.IOBase): @@ -318,6 +347,7 @@ def parse_file(input_file): else: return rx.findall(pathlib.Path(input_file).name).pop() + # PURPOSE: read input file and extract contents def extract_file(input_file, compressed): """ diff --git a/gravity_toolkit/read_SLR_harmonics.py b/gravity_toolkit/read_SLR_harmonics.py index 37430e6e..dc575eb7 100644 --- a/gravity_toolkit/read_SLR_harmonics.py +++ b/gravity_toolkit/read_SLR_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" read_SLR_harmonics.py Written by Tyler Sutterley (05/2023) @@ -68,6 +68,7 @@ Updated 10/2017: include the 6,0 and 6,1 coefficients in output Ylms Written 10/2017 """ + from __future__ import division import re @@ -75,6 +76,7 @@ import numpy as np import gravity_toolkit.time + # PURPOSE: wrapper function for calling individual readers def read_SLR_harmonics(SLR_file, **kwargs): """ @@ -90,11 +92,14 @@ def read_SLR_harmonics(SLR_file, **kwargs): """ if bool(re.search(r'gsfc_slr_5x5c61s61', SLR_file.name, re.I)): return read_GSFC_weekly_6x1(SLR_file, **kwargs) - elif bool(re.search(r'CSR_Monthly_5x5_Gravity_Harmonics', SLR_file.name, re.I)): + elif bool( + re.search(r'CSR_Monthly_5x5_Gravity_Harmonics', SLR_file.name, re.I) + ): return read_CSR_monthly_6x1(SLR_file, **kwargs) else: raise Exception(f'Unknown SLR file format {SLR_file}') + # PURPOSE: read monthly degree harmonic data from Satellite Laser Ranging (SLR) def read_CSR_monthly_6x1(SLR_file, SCALE=1e-10, HEADER=True): """ @@ -139,7 +144,7 @@ def read_CSR_monthly_6x1(SLR_file, SCALE=1e-10, HEADER=True): # new 5x5 fields no longer include geocenter components LMIN = 2 LMAX = 6 - n_harm = (LMAX**2 + 3*LMAX - LMIN**2 - LMIN)//2 - 5 + n_harm = (LMAX**2 + 3 * LMAX - LMIN**2 - LMIN) // 2 - 5 # counts the number of lines in the header count = 0 @@ -149,8 +154,8 @@ def read_CSR_monthly_6x1(SLR_file, SCALE=1e-10, HEADER=True): # file line at count line = file_contents[count] # find end within line to set HEADER flag to False when found - HEADER = not bool(re.match(r'end\sof\sheader',line)) - if bool(re.match(80*r'=',line)): + HEADER = not bool(re.match(r'end\sof\sheader', line)) + if bool(re.match(80 * r'=', line)): indice = count + 1 # add 1 to counter count += 1 @@ -160,55 +165,55 @@ def read_CSR_monthly_6x1(SLR_file, SCALE=1e-10, HEADER=True): raise Exception('Mean field header not found') # number of dates within the file - n_dates = (file_lines - count)//(n_harm + 1) + n_dates = (file_lines - count) // (n_harm + 1) # read mean fields from the header mean_Ylms = {} mean_Ylm_error = {} - mean_Ylms['clm'] = np.zeros((LMAX+1,LMAX+1)) - mean_Ylms['slm'] = np.zeros((LMAX+1,LMAX+1)) - mean_Ylm_error['clm'] = np.zeros((LMAX+1,LMAX+1)) - mean_Ylm_error['slm'] = np.zeros((LMAX+1,LMAX+1)) + mean_Ylms['clm'] = np.zeros((LMAX + 1, LMAX + 1)) + mean_Ylms['slm'] = np.zeros((LMAX + 1, LMAX + 1)) + mean_Ylm_error['clm'] = np.zeros((LMAX + 1, LMAX + 1)) + mean_Ylm_error['slm'] = np.zeros((LMAX + 1, LMAX + 1)) for i in range(n_harm): # split the line into individual components - line = file_contents[indice+i].split() + line = file_contents[indice + i].split() # degree and order for the line l1 = np.int64(line[0]) m1 = np.int64(line[1]) # fill mean field Ylms - mean_Ylms['clm'][l1,m1] = np.float64(line[2].replace('D','E')) - mean_Ylms['slm'][l1,m1] = np.float64(line[3].replace('D','E')) - mean_Ylm_error['clm'][l1,m1] = np.float64(line[4].replace('D','E')) - mean_Ylm_error['slm'][l1,m1] = np.float64(line[5].replace('D','E')) + mean_Ylms['clm'][l1, m1] = np.float64(line[2].replace('D', 'E')) + mean_Ylms['slm'][l1, m1] = np.float64(line[3].replace('D', 'E')) + mean_Ylm_error['clm'][l1, m1] = np.float64(line[4].replace('D', 'E')) + mean_Ylm_error['slm'][l1, m1] = np.float64(line[5].replace('D', 'E')) # output spherical harmonic fields Ylms = {} Ylms['error'] = {} Ylms['MJD'] = np.zeros((n_dates)) Ylms['time'] = np.zeros((n_dates)) - Ylms['clm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylms['slm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylms['error']['clm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylms['error']['slm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) + Ylms['clm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylms['slm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylms['error']['clm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylms['error']['slm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) # input spherical harmonic anomalies and errors Ylm_anomalies = {} Ylm_anomaly_error = {} - Ylm_anomalies['clm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylm_anomalies['slm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylm_anomaly_error['clm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylm_anomaly_error['slm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) + Ylm_anomalies['clm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylm_anomalies['slm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylm_anomaly_error['clm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylm_anomaly_error['slm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) # for each date for d in range(n_dates): # split the date line into individual components line_contents = file_contents[count].split() # verify arc number from iteration and file IARC = int(line_contents[0]) - assert (IARC == (d+1)) + assert IARC == (d + 1) # modified Julian date of the middle of the month - Ylms['MJD'][d] = np.mean(np.array(line_contents[5:7],dtype=np.float64)) + Ylms['MJD'][d] = np.mean(np.array(line_contents[5:7], dtype=np.float64)) # date of the mid-point of the arc given in years - YY,MM = np.array(line_contents[3:5]) - Ylms['time'][d] = gravity_toolkit.time.convert_calendar_decimal(YY,MM) + YY, MM = np.array(line_contents[3:5]) + Ylms['time'][d] = gravity_toolkit.time.convert_calendar_decimal(YY, MM) # add 1 to counter count += 1 @@ -220,24 +225,33 @@ def read_CSR_monthly_6x1(SLR_file, SCALE=1e-10, HEADER=True): l1 = np.int64(line[0]) m1 = np.int64(line[1]) # fill anomaly field Ylms and rescale to output - Ylm_anomalies['clm'][l1,m1,d] = np.float64(line[2])*SCALE - Ylm_anomalies['slm'][l1,m1,d] = np.float64(line[3])*SCALE - Ylm_anomaly_error['clm'][l1,m1,d] = np.float64(line[6])*SCALE - Ylm_anomaly_error['slm'][l1,m1,d] = np.float64(line[7])*SCALE + Ylm_anomalies['clm'][l1, m1, d] = np.float64(line[2]) * SCALE + Ylm_anomalies['slm'][l1, m1, d] = np.float64(line[3]) * SCALE + Ylm_anomaly_error['clm'][l1, m1, d] = np.float64(line[6]) * SCALE + Ylm_anomaly_error['slm'][l1, m1, d] = np.float64(line[7]) * SCALE # add 1 to counter count += 1 # calculate full coefficients and full errors - Ylms['clm'][:,:,d] = Ylm_anomalies['clm'][:,:,d] + mean_Ylms['clm'][:,:] - Ylms['slm'][:,:,d] = Ylm_anomalies['slm'][:,:,d] + mean_Ylms['slm'][:,:] - Ylms['error']['clm'][:,:,d]=np.sqrt(Ylm_anomaly_error['clm'][:,:,d]**2 + - mean_Ylm_error['clm'][:,:]**2) - Ylms['error']['slm'][:,:,d]=np.sqrt(Ylm_anomaly_error['slm'][:,:,d]**2 + - mean_Ylm_error['slm'][:,:]**2) + Ylms['clm'][:, :, d] = ( + Ylm_anomalies['clm'][:, :, d] + mean_Ylms['clm'][:, :] + ) + Ylms['slm'][:, :, d] = ( + Ylm_anomalies['slm'][:, :, d] + mean_Ylms['slm'][:, :] + ) + Ylms['error']['clm'][:, :, d] = np.sqrt( + Ylm_anomaly_error['clm'][:, :, d] ** 2 + + mean_Ylm_error['clm'][:, :] ** 2 + ) + Ylms['error']['slm'][:, :, d] = np.sqrt( + Ylm_anomaly_error['slm'][:, :, d] ** 2 + + mean_Ylm_error['slm'][:, :] ** 2 + ) # return spherical harmonic fields and date information return Ylms + # PURPOSE: read weekly degree harmonic data from Satellite Laser Ranging (SLR) def read_GSFC_weekly_6x1(SLR_file, SCALE=1.0, HEADER=True): r""" @@ -278,7 +292,7 @@ def read_GSFC_weekly_6x1(SLR_file, SCALE=1.0, HEADER=True): # spherical harmonic degree range (5x5 with 6,1) LMIN = 2 LMAX = 6 - n_harm = (LMAX**2 + 3*LMAX - LMIN**2 - LMIN)//2 - 5 + n_harm = (LMAX**2 + 3 * LMAX - LMIN**2 - LMIN) // 2 - 5 # counts the number of lines in the header count = 0 @@ -288,18 +302,18 @@ def read_GSFC_weekly_6x1(SLR_file, SCALE=1.0, HEADER=True): line = file_contents[count] # find the final line within the header text # to set HEADER flag to False when found - HEADER = not bool(re.search(r'Product:',line)) + HEADER = not bool(re.search(r'Product:', line)) # add 1 to counter count += 1 # number of dates within the file - n_dates = (file_lines - count)//(n_harm + 1) + n_dates = (file_lines - count) // (n_harm + 1) # output spherical harmonic fields Ylms = {} Ylms['MJD'] = np.zeros((n_dates)) Ylms['time'] = np.zeros((n_dates)) - Ylms['clm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) - Ylms['slm'] = np.zeros((LMAX+1,LMAX+1,n_dates)) + Ylms['clm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) + Ylms['slm'] = np.zeros((LMAX + 1, LMAX + 1, n_dates)) # for each date for d in range(n_dates): # split the date line into individual components @@ -319,14 +333,15 @@ def read_GSFC_weekly_6x1(SLR_file, SCALE=1.0, HEADER=True): l1 = np.int64(line_contents[0]) m1 = np.int64(line_contents[1]) # Spherical Harmonic data rescaled to output - Ylms['clm'][l1,m1,d] = np.float64(line_contents[2])*SCALE - Ylms['slm'][l1,m1,d] = np.float64(line_contents[3])*SCALE + Ylms['clm'][l1, m1, d] = np.float64(line_contents[2]) * SCALE + Ylms['slm'][l1, m1, d] = np.float64(line_contents[3]) * SCALE # add 1 to counter count += 1 # return spherical harmonic fields and date information return Ylms + # PURPOSE: interpolate harmonics from 7-day to monthly def convert_weekly(t_in, d_in, DATE=[], NEIGHBORS=28): """ @@ -356,15 +371,15 @@ def convert_weekly(t_in, d_in, DATE=[], NEIGHBORS=28): tdec = np.repeat(t_in, 7) data = np.repeat(d_in, 7) # calculate daily dates to use in centered moving average - tdec += (np.mod(np.arange(len(tdec)),7) - 3.5)/365.25 + tdec += (np.mod(np.arange(len(tdec)), 7) - 3.5) / 365.25 # calculate moving-average solution from 7-day arcs dinput = {} dinput['time'] = np.zeros_like(DATE) - dinput['data'] = np.zeros_like(DATE,dtype='f8') + dinput['data'] = np.zeros_like(DATE, dtype='f8') # for each output monthly date - for i,D in enumerate(DATE): + for i, D in enumerate(DATE): # find all dates within NEIGHBORS days of mid-point - isort = np.argsort((tdec - D)**2)[:NEIGHBORS] + isort = np.argsort((tdec - D) ** 2)[:NEIGHBORS] # calculate monthly mean of date and data dinput['time'][i] = np.mean(tdec[isort]) dinput['data'][i] = np.mean(data[isort]) diff --git a/gravity_toolkit/read_gfc_harmonics.py b/gravity_toolkit/read_gfc_harmonics.py index d3444887..314a0da9 100644 --- a/gravity_toolkit/read_gfc_harmonics.py +++ b/gravity_toolkit/read_gfc_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" read_gfc_harmonics.py Written by Tyler Sutterley (06/2023) Contributions by Hugo Lecomte @@ -68,6 +68,7 @@ Updated 07/2017: include parameters to change the tide system Written 12/2015 """ + import re import pathlib import numpy as np @@ -77,6 +78,7 @@ # attempt imports geoidtk = import_dependency('geoid_toolkit') + # PURPOSE: read spherical harmonic coefficients of a gravity model def read_gfc_harmonics(input_file, TIDE=None, FLAG='gfc'): """ @@ -148,8 +150,10 @@ def read_gfc_harmonics(input_file, TIDE=None, FLAG='gfc'): itsg_products.append(r'Grace2016') itsg_products.append(r'Grace2018') itsg_products.append(r'Grace_operational') - itsg_pattern = (r'(AOD1B_RL\d+|model|ITSG)[-_]({0})(_n\d+)?_' - r'(\d+)-(\d+)(\.gfc)').format(r'|'.join(itsg_products)) + itsg_pattern = ( + r'(AOD1B_RL\d+|model|ITSG)[-_]({0})(_n\d+)?_' + r'(\d+)-(\d+)(\.gfc)' + ).format(r'|'.join(itsg_products)) # regular expression operators for Swarm data and models swarm_data = r'(SW)_(.*?)_(EGF_SHA_2)__(.*?)_(.*?)_(.*?)(\.gfc|\.ZIP)' swarm_model = r'(GAA|GAB|GAC|GAD)_Swarm_(\d+)_(\d{2})_(\d{4})(\.gfc|\.ZIP)' @@ -159,20 +163,22 @@ def read_gfc_harmonics(input_file, TIDE=None, FLAG='gfc'): # GRAZ: Institute of Geodesy from GRAZ University of Technology rx = re.compile(itsg_pattern, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - PFX,PRD,trunc,year,month,SFX = rx.findall(input_file.name).pop() + PFX, PRD, trunc, year, month, SFX = rx.findall(input_file.name).pop() # number of days in each month for the calendar year dpm = gravity_toolkit.time.calendar_days(int(year)) # create start and end date lists - start_date = [int(year),int(month),1,0,0,0] - end_date = [int(year),int(month),dpm[int(month)-1],23,59,59] + start_date = [int(year), int(month), 1, 0, 0, 0] + end_date = [int(year), int(month), dpm[int(month) - 1], 23, 59, 59] elif re.match(swarm_data, input_file.name): # compile numerical expression operator for parameters from files # Swarm: data from Swarm satellite rx = re.compile(swarm_data, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - SAT,tmp,PROD,starttime,endtime,RL,SFX = rx.findall(input_file.name).pop() - start_date,_ = gravity_toolkit.time.parse_date_string(starttime) - end_date,_ = gravity_toolkit.time.parse_date_string(endtime) + SAT, tmp, PROD, starttime, endtime, RL, SFX = rx.findall( + input_file.name + ).pop() + start_date, _ = gravity_toolkit.time.parse_date_string(starttime) + end_date, _ = gravity_toolkit.time.parse_date_string(endtime) # number of days in each month for the calendar year dpm = gravity_toolkit.time.calendar_days(start_date[0]) elif re.match(swarm_model, input_file.name): @@ -180,38 +186,65 @@ def read_gfc_harmonics(input_file, TIDE=None, FLAG='gfc'): # Swarm: dealiasing products for Swarm data rx = re.compile(swarm_data, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - PROD,trunc,month,year,SFX = rx.findall(input_file.name).pop() + PROD, trunc, month, year, SFX = rx.findall(input_file.name).pop() # number of days in each month for the calendar year dpm = gravity_toolkit.time.calendar_days(int(year)) # create start and end date lists - start_date = [int(year),int(month),1,0,0,0] - end_date = [int(year),int(month),dpm[int(month)-1],23,59,59] + start_date = [int(year), int(month), 1, 0, 0, 0] + end_date = [int(year), int(month), dpm[int(month) - 1], 23, 59, 59] # python dictionary with model input and headers ZIP = bool(re.search('ZIP', SFX, re.IGNORECASE)) - model_input = geoidtk.read_ICGEM_harmonics(input_file, TIDE=TIDE, - FLAG=FLAG, ZIP=ZIP) + model_input = geoidtk.read_ICGEM_harmonics( + input_file, TIDE=TIDE, FLAG=FLAG, ZIP=ZIP + ) # start and end day of the year - start_day = np.sum(dpm[:start_date[1]-1]) + start_date[2] + \ - start_date[3]/24.0 + start_date[4]/1440.0 + start_date[5]/86400.0 - end_day = np.sum(dpm[:end_date[1]-1]) + end_date[2] + \ - end_date[3]/24.0 + end_date[4]/1440.0 + end_date[5]/86400.0 + start_day = ( + np.sum(dpm[: start_date[1] - 1]) + + start_date[2] + + start_date[3] / 24.0 + + start_date[4] / 1440.0 + + start_date[5] / 86400.0 + ) + end_day = ( + np.sum(dpm[: end_date[1] - 1]) + + end_date[2] + + end_date[3] / 24.0 + + end_date[4] / 1440.0 + + end_date[5] / 86400.0 + ) # end date taking into account measurements taken on different years - end_cyclic = (end_date[0]-start_date[0])*np.sum(dpm) + end_day + end_cyclic = (end_date[0] - start_date[0]) * np.sum(dpm) + end_day # calculate mid-month value mid_day = np.mean([start_day, end_cyclic]) # Calculating the mid-month date in decimal form - model_input['time'] = start_date[0] + mid_day/np.sum(dpm) + model_input['time'] = start_date[0] + mid_day / np.sum(dpm) # Calculating the Julian dates of the start and end date - model_input['start'] = 2400000.5 + \ - gravity_toolkit.time.convert_calendar_dates(start_date[0], - start_date[1],start_date[2],hour=start_date[3],minute=start_date[4], - second=start_date[5],epoch=(1858,11,17,0,0,0)) - model_input['end'] = 2400000.5 + \ - gravity_toolkit.time.convert_calendar_dates(end_date[0], - end_date[1],end_date[2],hour=end_date[3],minute=end_date[4], - second=end_date[5],epoch=(1858,11,17,0,0,0)) + model_input['start'] = ( + 2400000.5 + + gravity_toolkit.time.convert_calendar_dates( + start_date[0], + start_date[1], + start_date[2], + hour=start_date[3], + minute=start_date[4], + second=start_date[5], + epoch=(1858, 11, 17, 0, 0, 0), + ) + ) + model_input['end'] = ( + 2400000.5 + + gravity_toolkit.time.convert_calendar_dates( + end_date[0], + end_date[1], + end_date[2], + hour=end_date[3], + minute=end_date[4], + second=end_date[5], + epoch=(1858, 11, 17, 0, 0, 0), + ) + ) # return the spherical harmonics and parameters return model_input diff --git a/gravity_toolkit/read_love_numbers.py b/gravity_toolkit/read_love_numbers.py index bb8c9e02..47db2ccf 100755 --- a/gravity_toolkit/read_love_numbers.py +++ b/gravity_toolkit/read_love_numbers.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" read_love_numbers.py Written by Tyler Sutterley (11/2024) @@ -88,6 +88,7 @@ Updated 05/2013: python updates and comment updates Written 01/2012 """ + import io import re import logging @@ -98,9 +99,16 @@ # default maximum degree and order in case of infinite _default_max_degree = 100000 + # PURPOSE: read load Love/Shida numbers from PREM -def read_love_numbers(love_numbers_file, LMAX=None, HEADER=2, - COLUMNS=['l','hl','kl','ll'], REFERENCE='CE', FORMAT='tuple'): +def read_love_numbers( + love_numbers_file, + LMAX=None, + HEADER=2, + COLUMNS=['l', 'hl', 'kl', 'll'], + REFERENCE='CE', + FORMAT='tuple', +): """ Reads PREM load Love/Shida numbers file and applies isomorphic parameters :cite:p:`Dziewonski:1981bz,Blewitt:2003bz` @@ -163,78 +171,80 @@ def read_love_numbers(love_numbers_file, LMAX=None, HEADER=2, # dictionary of output Love/Shida numbers love = {} # spherical harmonic degree - love['l'] = np.arange(LMAX+1) + love['l'] = np.arange(LMAX + 1) # vertical displacement hl # gravitational potential kl # horizontal displacement ll (Shida number) - for n in ('hl','kl','ll'): - love[n] = np.zeros((LMAX+1)) + for n in ('hl', 'kl', 'll'): + love[n] = np.zeros((LMAX + 1)) # check if needing to interpolate between degrees - flag = np.ones((LMAX+1),dtype=bool) + flag = np.ones((LMAX + 1), dtype=bool) # for each line in the file (skipping header lines) for file_line in file_contents[HEADER:]: # find numerical instances in line # replacing fortran double precision exponential - love_numbers = rx.findall(file_line.replace('D','E')) + love_numbers = rx.findall(file_line.replace('D', 'E')) # spherical harmonic degree degree = love_numbers[COLUMNS.index('l')] l = _default_max_degree if (degree == 'inf') else int(degree) # truncate to spherical harmonic degree LMAX - if (l <= LMAX): + if l <= LMAX: # convert Love/Shida numbers to float # vertical displacement hl # gravitational potential kl # horizontal displacement ll (Shida number) - for n in ('hl','kl','ll'): + for n in ('hl', 'kl', 'll'): love[n][l] = np.float64(love_numbers[COLUMNS.index(n)]) # set interpolation flag for degree flag[l] = False # return Love/Shida numbers in output format - if (LMAX == 0): + if LMAX == 0: return love_number_formatter(love, FORMAT=FORMAT) # if needing to linearly interpolate Love/Shida numbers if np.any(flag): # linearly interpolate following Wahr (1998) - for n in ('hl','kl','ll'): - love[n][flag] = np.interp(love['l'][flag], - love['l'][~flag], love[n][~flag]) + for n in ('hl', 'kl', 'll'): + love[n][flag] = np.interp( + love['l'][flag], love['l'][~flag], love[n][~flag] + ) # if needing to linearly extrapolate Love/Shida numbers # NOTE: use caution if extrapolating far beyond the # maximum degree of the Love/Shida numbers dataset - for lint in range(l,LMAX+1): + for lint in range(l, LMAX + 1): # linearly extrapolate to maximum degree - for n in ('hl','kl','ll'): - love[n][lint] = 2.0*love[n][lint-1] - love[n][lint-2] + for n in ('hl', 'kl', 'll'): + love[n][lint] = 2.0 * love[n][lint - 1] - love[n][lint - 2] # calculate isomorphic parameters for different reference frames # From Blewitt (2003), Wahr (1998), Trupin (1992) and Farrell (1972) - if (REFERENCE.upper() == 'CF'): + if REFERENCE.upper() == 'CF': # Center of Surface Figure - alpha = (love['hl'][1] + 2.0*love['ll'][1])/3.0 - elif (REFERENCE.upper() == 'CL'): + alpha = (love['hl'][1] + 2.0 * love['ll'][1]) / 3.0 + elif REFERENCE.upper() == 'CL': # Center of Surface Lateral Figure alpha = love['ll'][1].copy() - elif (REFERENCE.upper() == 'CH'): + elif REFERENCE.upper() == 'CH': # Center of Surface Height Figure alpha = love['hl'][1].copy() - elif (REFERENCE.upper() == 'CM'): + elif REFERENCE.upper() == 'CM': # Center of Mass of Earth System alpha = 1.0 - elif (REFERENCE.upper() == 'CE'): + elif REFERENCE.upper() == 'CE': # Center of Mass of Solid Earth alpha = 0.0 else: raise Exception(f'Invalid Reference Frame {REFERENCE}') # apply isomorphic parameters - for n in ('hl','kl','ll'): + for n in ('hl', 'kl', 'll'): love[n][1] -= alpha # return Love/Shida numbers in output format return love_number_formatter(love, FORMAT=FORMAT) + # PURPOSE: return load Love/Shida numbers in a particular format def love_number_formatter(love, FORMAT='tuple'): """ @@ -262,15 +272,16 @@ def love_number_formatter(love, FORMAT='tuple'): ll: np.ndarray Love (Shida) number of Horizontal Displacement """ - if (FORMAT == 'dict'): + if FORMAT == 'dict': return love - elif (FORMAT == 'tuple'): + elif FORMAT == 'tuple': return (love['hl'], love['kl'], love['ll']) - elif (FORMAT == 'zip'): + elif FORMAT == 'zip': return zip(love['hl'], love['kl'], love['ll']) - elif (FORMAT == 'class'): + elif FORMAT == 'class': return love_numbers().from_dict(love) + # PURPOSE: read input file and extract contents def extract_love_numbers(love_numbers_file): """ @@ -284,7 +295,9 @@ def extract_love_numbers(love_numbers_file): # check if input Love/Shida numbers are a string or bytesIO object if isinstance(love_numbers_file, (str, pathlib.Path)): # tilde expansion of load love number data file - love_numbers_file = pathlib.Path(love_numbers_file).expanduser().absolute() + love_numbers_file = ( + pathlib.Path(love_numbers_file).expanduser().absolute() + ) # check that load Love/Shida number data file is present in file system if not love_numbers_file.exists(): raise FileNotFoundError(f'{str(love_numbers_file)} not found') @@ -297,6 +310,7 @@ def extract_love_numbers(love_numbers_file): else: raise ValueError('Invalid Love/Shida numbers file input') + # PURPOSE: read load Love/Shida numbers for a range of spherical harmonic degrees def load_love_numbers(LMAX, LOVE_NUMBERS=0, REFERENCE='CF', FORMAT='tuple'): """ @@ -340,48 +354,52 @@ def load_love_numbers(LMAX, LOVE_NUMBERS=0, REFERENCE='CF', FORMAT='tuple'): Love (Shida) number of Horizontal Displacement """ # load Love/Shida numbers file - if (LOVE_NUMBERS == 0): + if LOVE_NUMBERS == 0: # PREM outputs from Han and Wahr (1995) # https://doi.org/10.1111/j.1365-246X.1995.tb01819.x - love_numbers_file = get_data_path(['data','love_numbers']) + love_numbers_file = get_data_path(['data', 'love_numbers']) model = 'PREM' citation = 'Han and Wahr (1995)' header = 2 - columns = ['l','hl','kl','ll'] - elif (LOVE_NUMBERS == 1): + columns = ['l', 'hl', 'kl', 'll'] + elif LOVE_NUMBERS == 1: # PREM outputs from Gegout (2005) # http://gemini.gsfc.nasa.gov/aplo/ - love_numbers_file = get_data_path(['data','Load_Love2_CE.dat']) + love_numbers_file = get_data_path(['data', 'Load_Love2_CE.dat']) model = 'PREM' citation = 'Gegout et al. (2010)' header = 3 - columns = ['l','hl','ll','kl'] - elif (LOVE_NUMBERS == 2): + columns = ['l', 'hl', 'll', 'kl'] + elif LOVE_NUMBERS == 2: # PREM outputs from Wang et al. (2012) # https://doi.org/10.1016/j.cageo.2012.06.022 - love_numbers_file = get_data_path(['data','PREM-LLNs-truncated.dat']) + love_numbers_file = get_data_path(['data', 'PREM-LLNs-truncated.dat']) model = 'PREM' citation = 'Wang et al. (2012)' header = 1 - columns = ['l','hl','ll','kl','nl','nk'] - elif (LOVE_NUMBERS == 3): + columns = ['l', 'hl', 'll', 'kl', 'nl', 'nk'] + elif LOVE_NUMBERS == 3: # PREM hard outputs from Wang et al. (2012) # case with 0.46 kilometers thick hard sediment # https://doi.org/10.1016/j.cageo.2012.06.022 - love_numbers_file = get_data_path(['data','PREMhard-LLNs-truncated.dat']) + love_numbers_file = get_data_path( + ['data', 'PREMhard-LLNs-truncated.dat'] + ) model = 'PREMhard' citation = 'Wang et al. (2012)' header = 1 - columns = ['l','hl','ll','kl','nl','nk'] - elif (LOVE_NUMBERS == 4): + columns = ['l', 'hl', 'll', 'kl', 'nl', 'nk'] + elif LOVE_NUMBERS == 4: # PREM soft outputs from Wang et al. (2012) # case with 0.52 kilometers thick soft sediment # https://doi.org/10.1016/j.cageo.2012.06.022 - love_numbers_file = get_data_path(['data','PREMsoft-LLNs-truncated.dat']) + love_numbers_file = get_data_path( + ['data', 'PREMsoft-LLNs-truncated.dat'] + ) model = 'PREMsoft' citation = 'Wang et al. (2012)' header = 1 - columns = ['l','hl','ll','kl','nl','nk'] + columns = ['l', 'hl', 'll', 'kl', 'nl', 'nk'] else: raise ValueError(f'Unknown Love Numbers Type {LOVE_NUMBERS:d}') # validate as pathlib object @@ -393,10 +411,16 @@ def load_love_numbers(LMAX, LOVE_NUMBERS=0, REFERENCE='CF', FORMAT='tuple'): # however, as we are linearly extrapolating out, do not make # LMAX too much larger than 696 # read arrays of kl, hl, and ll Love/Shida Numbers - love = read_love_numbers(love_numbers_file, LMAX=LMAX, HEADER=header, - COLUMNS=columns, REFERENCE=REFERENCE, FORMAT=FORMAT) + love = read_love_numbers( + love_numbers_file, + LMAX=LMAX, + HEADER=header, + COLUMNS=columns, + REFERENCE=REFERENCE, + FORMAT=FORMAT, + ) # append model and filename attributes to class - if (FORMAT == 'class'): + if FORMAT == 'class': love.filename = love_numbers_file.name love.reference = REFERENCE love.model = model @@ -404,6 +428,7 @@ def load_love_numbers(LMAX, LOVE_NUMBERS=0, REFERENCE='CF', FORMAT='tuple'): # return the load love numbers return love + class love_numbers(object): """ Data class for Load Love/Shida numbers @@ -434,21 +459,23 @@ class love_numbers(object): filename: str input filename of Load Love/Shida Numbers """ + np.seterr(invalid='ignore') + def __init__(self, **kwargs): # set default keyword arguments - kwargs.setdefault('lmax',None) + kwargs.setdefault('lmax', None) # set default class attributes - self.hl=[] - self.kl=[] - self.ll=[] - self.lmax=kwargs['lmax'] + self.hl = [] + self.kl = [] + self.ll = [] + self.lmax = kwargs['lmax'] # calculate spherical harmonic degree (0 is falsy) - self.l=np.arange(self.lmax+1) if (self.lmax is not None) else None - self.reference=None - self.model=None - self.citation=None - self.filename=None + self.l = np.arange(self.lmax + 1) if (self.lmax is not None) else None + self.reference = None + self.model = None + self.citation = None + self.filename = None def from_dict(self, d): """ @@ -460,7 +487,7 @@ def from_dict(self, d): dictionary object to be converted """ # retrieve each Load Love/Shida Number - for key in ('hl','kl','ll'): + for key in ('hl', 'kl', 'll'): setattr(self, key, d.get(key)) self.lmax = len(self.hl) - 1 # calculate spherical harmonic degree @@ -478,7 +505,7 @@ def to_dict(self): """ # retrieve each Load Love/Shida Number d = {} - for key in ('hl','kl','ll'): + for key in ('hl', 'kl', 'll'): d[key] = getattr(self, key) return d @@ -513,19 +540,19 @@ def transform(self, reference): """ # calculate isomorphic parameters for different reference frames # From Blewitt (2003), Wahr (1998), Trupin (1992) and Farrell (1972) - if (reference.upper() == 'CF'): + if reference.upper() == 'CF': # Center of Surface Figure - alpha = (self.hl[1] + 2.0*self.ll[1])/3.0 - elif (reference.upper() == 'CL'): + alpha = (self.hl[1] + 2.0 * self.ll[1]) / 3.0 + elif reference.upper() == 'CL': # Center of Surface Lateral Figure alpha = self.ll[1].copy() - elif (reference.upper() == 'CH'): + elif reference.upper() == 'CH': # Center of Surface Height Figure alpha = self.hl[1].copy() - elif (reference.upper() == 'CM'): + elif reference.upper() == 'CM': # Center of Mass of Earth System alpha = 1.0 - elif (reference.upper() == 'CE'): + elif reference.upper() == 'CE': # Center of Mass of Solid Earth alpha = 0.0 else: @@ -543,27 +570,24 @@ def update_dimensions(self): Update the dimensions of the ``love_numbers`` object """ # calculate spherical harmonic degree (0 is falsy) - self.l=np.arange(self.lmax+1) if (self.lmax is not None) else None + self.l = np.arange(self.lmax + 1) if (self.lmax is not None) else None return self def __str__(self): - """String representation of the ``love_numbers`` object - """ + """String representation of the ``love_numbers`` object""" properties = ['gravity_toolkit.love_numbers'] - properties.append(f" citation: {self.citation}") - properties.append(f" earth_model: {self.model}") - properties.append(f" max_degree: {self.lmax}") - properties.append(f" reference: {self.reference}") + properties.append(f' citation: {self.citation}') + properties.append(f' earth_model: {self.model}') + properties.append(f' max_degree: {self.lmax}') + properties.append(f' reference: {self.reference}') return '\n'.join(properties) def __len__(self): - """Number of degrees - """ + """Number of degrees""" return len(self.l) def __iter__(self): - """Iterate over load Love/Shida numbers variables - """ + """Iterate over load Love/Shida numbers variables""" yield self.hl yield self.kl yield self.ll diff --git a/gravity_toolkit/sea_level_equation.py b/gravity_toolkit/sea_level_equation.py index 24b53231..88a10ddb 100644 --- a/gravity_toolkit/sea_level_equation.py +++ b/gravity_toolkit/sea_level_equation.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -u""" -sea_level_equation.py (06/2025) +""" +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 @@ -37,7 +37,6 @@ ITERATIONS: maximum number of iterations for the solver PLM: input Legendre polynomials FILL_VALUE: value used over land points - ASTYPE: floating point precision for calculating Clenshaw summation SCALE: scaling factor to prevent underflow in Clenshaw summation PYTHON DEPENDENCIES: @@ -91,6 +90,8 @@ https://doi.org/10.1029/JB090iB11p09363 UPDATE HISTORY: + Updated 07/2026: use np.einsum for spherical harmonic summations + use np.radians to convert from degrees to radians Updated 06/2025: added option to set the density of sea water (g/cm^3) Updated 03/2023: improve typing for variables in docstrings Updated 01/2023: refactored associated legendre polynomials @@ -122,17 +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, ASTYPE=np.longdouble, 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` @@ -180,8 +197,6 @@ def sea_level_equation(loadClm, loadSlm, glon, glat, land_function, LMAX=0, Legendre polynomials FILL_VALUE: float, default 0 Invalid value used over land points - ASTYPE: np.dtype, default np.longdouble - Floating point precision for calculating Clenshaw summation SCALE: float, default 1e-280 Scaling factor to prevent underflow in Clenshaw summation @@ -192,17 +207,17 @@ def sea_level_equation(loadClm, loadSlm, glon, glat, land_function, LMAX=0, """ # dimensions of land function - nphi,nth = np.shape(land_function) + nphi, nth = np.shape(land_function) # calculate colatitude and longitude in radians - th = (90.0 - glat)*np.pi/180.0 - phi = np.squeeze(glon*np.pi/180.0) + th = np.radians(90.0 - glat) + phi = np.radians(np.squeeze(glon)) # 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 + hl, kl, ll = LOVE # density of water [g/cm^3] rho_water = np.float64(DENSITY) # Earth Parameters @@ -213,111 +228,125 @@ 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): + 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] # coefh and coefp will be the same for all orders except for degree 2 # and order 1 (if POLAR motion feedback is included) - m = np.arange(0,l+1) - coefh[l,m] = 3.0*rho_water*(1.0 + kl[l] - hl[l])/rho_e/np.float64(2*l+1) - coefp[l,m] = (1.0 + kl[l] - hl[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 # 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 = (1.0 + k2b - h2b)*(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: # calculate Legendre polynomials using Holmes and Featherstone relation PLM, dPLM = plm_holmes(LMAX, np.cos(th)) # calculate sin of colatitudes - gth,gphi = np.meshgrid(th, phi) - u = np.sin(gth[ii,jj]) + gth, gphi = np.meshgrid(th, phi) + 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) + 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}') logging.info(f'Uniform Ocean Height: {sea_height:0.10g}') + # allocate for output sea level field + sea_level = np.empty((nphi, nth)) + # complex load spherical harmonics + loadYlms = loadClm - 1j * loadSlm # distribute sea height over ocean harmonics - height_Ylms = ocean_Ylms.scale(sea_height) + 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) @@ -325,52 +354,36 @@ def sea_level_equation(loadClm, loadSlm, glon, glat, land_function, LMAX=0, eps = np.inf eps_max = 1e-4 while (eps > eps_max) and (n_iter <= ITERATIONS): - # allocate for sea level field of iteration - sea_level = np.zeros((nphi,nth)) + # zero out the sea level field for this iteration + sea_level[:, :] = 0.0 # calculate combined spherical harmonics for Clenshaw summation - clm1 = coefh*height_Ylms.clm + rad_e*coefp*loadClm - slm1 = coefh*height_Ylms.slm + rad_e*coefp*loadSlm - # calculate clenshaw summations over colatitudes - s_m_c = np.zeros((nth,LMAX*2+2)) - for m in range(LMAX, -1, -1): - s_m_c[:,2*m:2*m+2] = clenshaw_s_m(np.cos(th), m, clm1, slm1, LMAX, - ASTYPE=ASTYPE, SCALE=SCALE) - - # calculate cos(phi) - cos_phi_2 = 2.0*np.cos(phi) - # matrix of cos/sin m*phi summation - cos_m_phi = np.zeros((nphi,LMAX+2),dtype=ASTYPE) - sin_m_phi = np.zeros((nphi,LMAX+2),dtype=ASTYPE) - # initialize matrix with values at lmax+1 and lmax - cos_m_phi[:,LMAX+1] = np.cos(ASTYPE(LMAX + 1)*phi) - sin_m_phi[:,LMAX+1] = np.sin(ASTYPE(LMAX + 1)*phi) - cos_m_phi[:,LMAX] = np.cos(ASTYPE(LMAX)*phi) - sin_m_phi[:,LMAX] = np.sin(ASTYPE(LMAX)*phi) - # calculate summation - gc=np.multiply(s_m_c[np.newaxis,:,2*LMAX],cos_m_phi[:,np.newaxis,LMAX]) - gs=np.multiply(s_m_c[np.newaxis,:,2*LMAX+1],sin_m_phi[:,np.newaxis,LMAX]) - s_m = gc[ii,jj] + gs[ii,jj] + 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): - cos_m_phi[:,m] = cos_phi_2*cos_m_phi[:,m+1] - cos_m_phi[:,m+2] - sin_m_phi[:,m] = cos_phi_2*sin_m_phi[:,m+1] - sin_m_phi[:,m+2] - a_m = np.sqrt((2.0*m+3.0)/(2.0*m+2.0)) - gc=np.multiply(s_m_c[np.newaxis,:,2*m],cos_m_phi[:,np.newaxis,m]) - gs=np.multiply(s_m_c[np.newaxis,:,2*m+1],sin_m_phi[:,np.newaxis,m]) - s_m = a_m*u*s_m + gc[ii,jj] + gs[ii,jj] + for m in range(LMAX, 0, -1): + # calculate summation for order 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 + # add the l=0/m=0 term + 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 - gsmc,gcmp = np.meshgrid(s_m_c[:,0],cos_m_phi[:,0]) - sea_level[ii,jj] = np.sqrt(3.0)*u*s_m + gsmc[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}') @@ -382,37 +395,35 @@ 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[:,:] - uniform_Ylms = ocean_Ylms.scale(sea_height) - Ylms.add(uniform_Ylms) + sea_level += sea_height * ocean_function[:, :] + Ylms += ocean_Ylms * sea_height # calculate eps to determine if solution is appropriately converged - mod1 = np.sqrt(height_Ylms.clm**2 + height_Ylms.slm**2) - mod2 = np.sqrt(Ylms.clm**2 + Ylms.slm**2) - eps = np.abs(np.sum(mod2[l1,m1] - mod1[l1,m1])/np.sum(mod1[l1,m1])) + 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])) # 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('Original Total Ocean Mass: {0:0.10g}'.format(-tmass/1e15)) - logging.info('Final Iterated Ocean Mass: {0:0.10g}'.format(omass/1e15)) + 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_s_m(t, m, clm1, slm1, lmax, - ASTYPE=np.longdouble, SCALE=1e-280 - ): +def _clenshaw(t, m, Ylm1, lmax, SCALE=1e-280): """ Compute conditioned arrays for Clenshaw summation from the fully-normalized associated Legendre's function for an order m @@ -423,14 +434,10 @@ def clenshaw_s_m(t, m, clm1, slm1, lmax, elements ranging from -1 to 1, typically cos(th) m: int spherical harmonic order - clm1: np.ndarray - cosine spherical harmonics - slm1: np.ndarray - sine spherical harmonics + Ylm1: np.ndarray + complex form of spherical harmonics lmax: int maximum spherical harmonic degree - ASTYPE: np.dtype, default np.longdouble - floating point precision for calculating Clenshaw summation SCALE: float, default 1e-280 scaling factor to prevent underflow in Clenshaw summation @@ -441,49 +448,68 @@ def clenshaw_s_m(t, m, clm1, slm1, lmax, """ # allocate for output matrix N = len(t) - s_m = np.zeros((N,2),dtype=ASTYPE) + s_m = np.zeros((N), dtype=np.clongdouble) # scaling to prevent overflow - clm = SCALE*clm1.astype(ASTYPE) - slm = SCALE*slm1.astype(ASTYPE) + ylm = SCALE * Ylm1.astype(np.clongdouble) # convert lmax and m to float - lm = ASTYPE(lmax) - mm = ASTYPE(m) - if (m == lmax): - s_m[:,0] = np.copy(clm[lmax,lmax]) - s_m[:,1] = np.copy(slm[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[:,0] = a_lm*clm[lmax,lmax-1] + clm[lmax-1,lmax-1] - s_m[:,1] = a_lm*slm[lmax,lmax-1] + slm[lmax-1,lmax-1] - elif ((m <= (lmax-2)) and (m >= 1)): - s_mm_c_pre_2 = np.copy(clm[lmax,m]) - s_mm_s_pre_2 = np.copy(slm[lmax,m]) - a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/((lm-mm)*(lm+mm)))*t - s_mm_c_pre_1 = a_lm*s_mm_c_pre_2 + clm[lmax-1,m] - s_mm_s_pre_1 = a_lm*s_mm_s_pre_2 + slm[lmax-1,m] - for l in range(lmax-2, m-1, -1): - ll = ASTYPE(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_c = a_lm * s_mm_c_pre_1 - b_lm * s_mm_c_pre_2 + clm[l,m] - s_mm_s = a_lm * s_mm_s_pre_1 - b_lm * s_mm_s_pre_2 + slm[l,m] - s_mm_c_pre_2 = np.copy(s_mm_c_pre_1) - s_mm_s_pre_2 = np.copy(s_mm_s_pre_1) - s_mm_c_pre_1 = np.copy(s_mm_c) - s_mm_s_pre_1 = np.copy(s_mm_s) - s_m[:,0] = np.copy(s_mm_c) - s_m[:,1] = np.copy(s_mm_s) - elif (m == 0): - s_mm_c_pre_2 = np.copy(clm[lmax,0]) - a_lm = np.sqrt(((2.0*lm-1.0)*(2.0*lm+1.0))/(lm*lm))*t - s_mm_c_pre_1 = a_lm * s_mm_c_pre_2 + clm[lmax-1,0] - for l in range(lmax-2, m-1, -1): - ll = ASTYPE(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_c = a_lm * s_mm_c_pre_1 - b_lm * s_mm_c_pre_2 + clm[l,0] - s_mm_c_pre_2 = np.copy(s_mm_c_pre_1) - s_mm_c_pre_1 = np.copy(s_mm_c) - s_m[:,0] = np.copy(s_mm_c) + 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): + 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.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): + 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] + 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 adc80ea5..56539735 100644 --- a/gravity_toolkit/spatial.py +++ b/gravity_toolkit/spatial.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" spatial.py Written by Tyler Sutterley (10/2024) @@ -20,6 +20,8 @@ time.py: utilities for calculating time operations UPDATE HISTORY: + Updated 07/2026: add dunder (magic) methods for mathematical operations + add option to change the output format for ascii files Updated 10/2024: allow 2D and 3D arrays in output netCDF4 files Updated 06/2024: use wrapper to importlib for optional dependencies Updated 05/2024: make subscriptable and allow item assignment @@ -74,6 +76,7 @@ Updated 06/2020: added zeros_like() for creating an empty spatial object Written 06/2020 """ + import re import io import copy @@ -92,6 +95,7 @@ h5py = import_dependency('h5py') netCDF4 = import_dependency('netCDF4') + class spatial(object): """ Data class for reading, writing and processing spatial data @@ -118,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 @@ -155,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) @@ -221,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: @@ -263,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'])) @@ -283,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: @@ -335,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: @@ -372,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][:])) @@ -390,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'] = {} @@ -407,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 @@ -451,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()) @@ -470,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 @@ -488,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: @@ -498,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][:])) @@ -520,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() @@ -531,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 @@ -566,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 @@ -581,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): """ @@ -611,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)) @@ -638,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) @@ -689,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) @@ -711,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): @@ -734,32 +779,44 @@ def to_ascii(self, filename, **kwargs): full path of output ascii file date: bool, default True ``spatial`` objects contain date information + float_format: str, default '12.4f' + format for floating point numbers in output ascii file verbose: bool, default False Output file and variable information """ 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 = '{0:10.4f} {1:10.4f} {2:12.4f} {3:12.4f} {4:10.4f}' + file_format += ' '.join( + [f'{{{i}:' + float_format + '}' for i in (2, 3, 4)] + ) elif hasattr(self, 'error'): - file_format = '{0:10.4f} {1:10.4f} {2:12.4f} {3:12.4f}' + file_format += ' '.join( + [f'{{{i}:' + float_format + '}' for i in (2, 3)] + ) elif kwargs['date']: - file_format = '{0:10.4f} {1:10.4f} {2:12.4f} {4:10.4f}' + file_format += ' '.join( + [f'{{{i}:' + float_format + '}' for i in (2, 4)] + ) else: - file_format = '{0:10.4f} {1:10.4f} {2:12.4f}' + 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() @@ -803,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'] @@ -869,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())) @@ -947,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 @@ -972,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'] @@ -1012,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())) @@ -1076,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 @@ -1118,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) @@ -1164,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'): @@ -1186,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) @@ -1209,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) @@ -1228,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 @@ -1258,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: @@ -1296,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: @@ -1319,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 @@ -1360,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 @@ -1407,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 @@ -1442,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 @@ -1478,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 @@ -1512,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: @@ -1540,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: @@ -1559,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 @@ -1581,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): @@ -1589,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 @@ -1606,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 @@ -1633,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 @@ -1651,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 @@ -1664,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): @@ -1684,49 +1807,96 @@ 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): + """Add values to a ``spatial`` object""" + temp = self.copy() + return temp.offset(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) + + def __idiv__(self, other): + """In-place divide values from a ``spatial`` object""" + return self.__itruediv__(other) + + def __imul__(self, other): + """In-place multiply values from a ``spatial`` object""" + return self.scale(other) + + def __ipow__(self, other): + """In-place raise values from a ``spatial`` object to a power""" + return self.power(other) + + def __isub__(self, other): + """In-place subtract values from a ``spatial`` object""" + return self.offset(-other) + + def __itruediv__(self, other): + """In-place divide values from a ``spatial`` object""" + return self.scale(1.0 / other) + + 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() + return temp.offset(-other) + + def __truediv__(self, other): + """Divide values from a ``spatial`` object""" + temp = self.copy() + 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 @@ -1737,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 @@ -1753,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): @@ -1793,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 @@ -1833,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: @@ -1876,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'])) @@ -1896,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']) @@ -1926,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() @@ -1972,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 @@ -1993,7 +2180,7 @@ def update_mask(self): Update the mask of the ``scaling_factors`` object """ if self.fill_value is not None: - self.mask |= (self.data == self.fill_value) + self.mask |= self.data == self.fill_value self.mask |= np.isnan(self.data) self.data[self.mask] = self.fill_value # replace fill values within scaling factor errors diff --git a/gravity_toolkit/time.py b/gravity_toolkit/time.py index 8ad52ced..09e56ac2 100644 --- a/gravity_toolkit/time.py +++ b/gravity_toolkit/time.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" time.py Written by Tyler Sutterley (06/2024) Contributions by Hugo Lecomte @@ -37,6 +37,7 @@ Updated 08/2020: added NASA Earthdata routines for downloading from CDDIS Written 07/2020 """ + from __future__ import annotations import re @@ -51,18 +52,36 @@ import gravity_toolkit.utilities # conversion factors between time units and seconds -_to_sec = {'microseconds': 1e-6, 'microsecond': 1e-6, - 'microsec': 1e-6, 'microsecs': 1e-6, - 'milliseconds': 1e-3, 'millisecond': 1e-3, - 'millisec': 1e-3, 'millisecs': 1e-3, - 'msec': 1e-3, 'msecs': 1e-3, 'ms': 1e-3, - 'seconds': 1.0, 'second': 1.0, 'sec': 1.0, - 'secs': 1.0, 's': 1.0, - 'minutes': 60.0, 'minute': 60.0, - 'min': 60.0, 'mins': 60.0, - 'hours': 3600.0, 'hour': 3600.0, - 'hr': 3600.0, 'hrs': 3600.0, 'h': 3600.0, - 'day': 86400.0, 'days': 86400.0, 'd': 86400.0} +_to_sec = { + 'microseconds': 1e-6, + 'microsecond': 1e-6, + 'microsec': 1e-6, + 'microsecs': 1e-6, + 'milliseconds': 1e-3, + 'millisecond': 1e-3, + 'millisec': 1e-3, + 'millisecs': 1e-3, + 'msec': 1e-3, + 'msecs': 1e-3, + 'ms': 1e-3, + 'seconds': 1.0, + 'second': 1.0, + 'sec': 1.0, + 'secs': 1.0, + 's': 1.0, + 'minutes': 60.0, + 'minute': 60.0, + 'min': 60.0, + 'mins': 60.0, + 'hours': 3600.0, + 'hour': 3600.0, + 'hr': 3600.0, + 'hrs': 3600.0, + 'h': 3600.0, + 'day': 86400.0, + 'days': 86400.0, + 'd': 86400.0, +} # approximate conversions for longer periods _to_sec['mon'] = 30.0 * 86400.0 _to_sec['month'] = 30.0 * 86400.0 @@ -79,6 +98,7 @@ _gps_epoch = (1980, 1, 6, 0, 0, 0) _j2000_epoch = (2000, 1, 1, 12, 0, 0) + # PURPOSE: parse a date string and convert to a datetime object in UTC def parse(date_string): """ @@ -103,6 +123,7 @@ def parse(date_string): # return the datetime object return date + # PURPOSE: parse a date string into epoch and units scale def parse_date_string(date_string): """ @@ -138,6 +159,7 @@ def parse_date_string(date_string): # return the epoch (as list) and the time unit conversion factors return (datetime_to_list(epoch), _to_sec[units]) + # PURPOSE: split a date string into units and epoch def split_date_string(date_string): """ @@ -149,12 +171,13 @@ def split_date_string(date_string): time-units since yyyy-mm-dd hh:mm:ss """ try: - units,_,epoch = date_string.split(None, 2) + units, _, epoch = date_string.split(None, 2) except ValueError: raise ValueError(f'Invalid format: {date_string}') else: return (units.lower(), parse(epoch)) + # PURPOSE: convert a datetime object into a list def datetime_to_list(date): """ @@ -170,7 +193,15 @@ def datetime_to_list(date): date: list [year,month,day,hour,minute,second] """ - return [date.year,date.month,date.day,date.hour,date.minute,date.second] + return [ + date.year, + date.month, + date.day, + date.hour, + date.minute, + date.second, + ] + # PURPOSE: extract parameters from filename def parse_grace_file(granule): @@ -195,13 +226,18 @@ def parse_grace_file(granule): # GRGS: CNES Groupe de Recherche de Geodesie Spatiale centers = r'UTCSR|EIGEN|GFZOP|JPLEM|JPLMSC|GRGS|COSTG|GRGS' suffixes = r'\.gz|\.gfc|\.txt' - regex_pattern = (r'(.*?)-2_(\d{4})(\d{3})-(\d{4})(\d{3})_' - rf'(.*?)_({centers})_(.*?)_(\d+)(.*?)({suffixes})?$') + regex_pattern = ( + r'(.*?)-2_(\d{4})(\d{3})-(\d{4})(\d{3})_' + rf'(.*?)_({centers})_(.*?)_(\d+)(.*?)({suffixes})?$' + ) rx = re.compile(regex_pattern, re.VERBOSE) # extract parameters from input filename - PFX,SY,SD,EY,ED,AUX,PRC,F1,DRL,F2,SFX = rx.findall(file_basename).pop() + PFX, SY, SD, EY, ED, AUX, PRC, F1, DRL, F2, SFX = rx.findall( + file_basename + ).pop() # return the start and end date lists - return ((SY,SD),(EY,ED)) + return ((SY, SD), (EY, ED)) + # PURPOSE: extract dates from GRAZ or Swarm files with regular expressions def parse_gfc_file(granule, PROC, DSET): @@ -229,7 +265,7 @@ def parse_gfc_file(granule, PROC, DSET): # verify that filename is reduced to basename file_basename = pathlib.Path(granule).name # extract parameters from input filename - if (PROC == 'GRAZ'): + if PROC == 'GRAZ': # regular expression operators for ITSG data and models itsg_products = [] itsg_products.append(r'atmosphere') @@ -240,42 +276,51 @@ def parse_gfc_file(granule, PROC, DSET): itsg_products.append(r'Grace2016') itsg_products.append(r'Grace2018') itsg_products.append(r'Grace_operational') - regex_pattern=(r'(AOD1B_RL\d+|model|ITSG)[-_]({0})(_n\d+)?_' - r'(\d+)-(\d+)(\.gfc)').format(r'|'.join(itsg_products)) + regex_pattern = ( + r'(AOD1B_RL\d+|model|ITSG)[-_]({0})(_n\d+)?_' + r'(\d+)-(\d+)(\.gfc)' + ).format(r'|'.join(itsg_products)) # compile regular expression operator for parameters from files rx = re.compile(regex_pattern, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - PFX,PRD,trunc,year,month,SFX = rx.findall(file_basename).pop() + PFX, PRD, trunc, year, month, SFX = rx.findall(file_basename).pop() # number of days in each month for the calendar year dpm = calendar_days(int(year)) # create start and end date lists - start_date = [int(year),int(month),1,0,0,0] - end_date = [int(year),int(month),dpm[int(month)-1],23,59,59] + start_date = [int(year), int(month), 1, 0, 0, 0] + end_date = [int(year), int(month), dpm[int(month) - 1], 23, 59, 59] elif (PROC == 'Swarm') and (DSET == 'GSM'): # regular expression operators for Swarm data - regex_pattern=r'(SW)_(.*?)_(EGF_SHA_2)__(.*?)_(.*?)_(.*?)(\.gfc|\.ZIP)' + regex_pattern = ( + r'(SW)_(.*?)_(EGF_SHA_2)__(.*?)_(.*?)_(.*?)(\.gfc|\.ZIP)' + ) # compile regular expression operator for parameters from files rx = re.compile(regex_pattern, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - SAT,tmp,PROD,starttime,endtime,RL,SFX = rx.findall(file_basename).pop() - start_date,_ = parse_date_string(starttime) - end_date,_ = parse_date_string(endtime) + SAT, tmp, PROD, starttime, endtime, RL, SFX = rx.findall( + file_basename + ).pop() + start_date, _ = parse_date_string(starttime) + end_date, _ = parse_date_string(endtime) elif (PROC == 'Swarm') and (DSET != 'GSM'): # regular expression operators for Swarm models - regex_pattern=(r'(GAA|GAB|GAC|GAD)_Swarm_(\d+)_(\d{2})_(\d{4})' - r'(\.gfc|\.ZIP)') + regex_pattern = ( + r'(GAA|GAB|GAC|GAD)_Swarm_(\d+)_(\d{2})_(\d{4})' + r'(\.gfc|\.ZIP)' + ) # compile regular expression operator for parameters from files rx = re.compile(regex_pattern, re.VERBOSE | re.IGNORECASE) # extract parameters from input filename - PROD,trunc,month,year,SFX = rx.findall(file_basename).pop() + PROD, trunc, month, year, SFX = rx.findall(file_basename).pop() # number of days in each month for the calendar year dpm = calendar_days(int(year)) # create start and end date lists - start_date = [int(year),int(month),1,0,0,0] - end_date = [int(year),int(month),dpm[int(month)-1],23,59,59] + start_date = [int(year), int(month), 1, 0, 0, 0] + end_date = [int(year), int(month), dpm[int(month) - 1], 23, 59, 59] # return the start and end date lists return (start_date, end_date) + def reduce_by_date(granules): """ Reduce list of GRACE/GRACE-FO files by date to the newest version @@ -297,15 +342,17 @@ def reduce_by_date(granules): # GRGS: French Centre National D'Etudes Spatiales (CNES) # COSTG: International Combined Time-variable Gravity Fields args = r'UTCSR|EIGEN|GFZOP|JPLEM|JPLMSC|GRGS|COSTG' - regex_pattern = (r'(.*?)-2_(\d{{4}})(\d{{3}})-(\d{{4}})(\d{{3}})_(.*?)_' - r'({0})_(.*?)_(\d{{2}})(\d{{2}})(.*?)(\.gz|\.gfc)?$').format(args) + regex_pattern = ( + r'(.*?)-2_(\d{{4}})(\d{{3}})-(\d{{4}})(\d{{3}})_(.*?)_' + r'({0})_(.*?)_(\d{{2}})(\d{{2}})(.*?)(\.gz|\.gfc)?$' + ).format(args) rx = re.compile(regex_pattern, re.VERBOSE) # for each unique date for d in sorted(set(date_list)): - if (date_list.count(d) == 1): + if date_list.count(d) == 1: i = date_list.index(d) unique_list.append(granules[i]) - elif (date_list.count(d) >= 2): + elif date_list.count(d) >= 2: # if more than 1 file with date use newest version indices = [i for i, dt in enumerate(date_list) if (dt == d)] # find each version within the file @@ -314,8 +361,9 @@ def reduce_by_date(granules): # verify that filename is reduced to basename file_basename = pathlib.Path(granules[i]).name # parse filename to get file version - PFX,SY,SD,EY,ED,AUX,PRC,F1,DRL,VER,F2,SFX = \ + PFX, SY, SD, EY, ED, AUX, PRC, F1, DRL, VER, F2, SFX = ( rx.findall(file_basename).pop() + ) # append to list of file versions versions.append(int(VER)) # find file with newest version @@ -324,6 +372,7 @@ def reduce_by_date(granules): # return the sorted list of files with unique dates return unique_list + # PURPOSE: Adjust GRACE/GRACE-FO months to fix "Special Cases" def adjust_months(grace_month): """ @@ -355,29 +404,29 @@ def adjust_months(grace_month): # create temporary months object m = np.zeros_like(grace_month) # find unique months - _,i,c = np.unique(grace_month,return_inverse=True,return_counts=True) + _, i, c = np.unique(grace_month, return_inverse=True, return_counts=True) # simple unique months case - case1, = np.nonzero(c[i] == 1) + (case1,) = np.nonzero(c[i] == 1) m[case1] = grace_month[case1] # Special Months cases - case2, = np.nonzero(c[i] == 2) + (case2,) = np.nonzero(c[i] == 2) # for each special case month for j in case2: # prior month, current month, subsequent 2 months - mm1 = grace_month[j-1] + mm1 = grace_month[j - 1] mon = grace_month[j] - mp1 = grace_month[j+1] if (j < (nmon-1)) else (mon + 1) - mp2 = grace_month[j+2] if (j < (nmon-2)) else (mp1 + 1) + mp1 = grace_month[j + 1] if (j < (nmon - 1)) else (mon + 1) + mp2 = grace_month[j + 2] if (j < (nmon - 2)) else (mp1 + 1) # determine the months which meet the criteria need to be adjusted - if (mon == (mm1 + 1)): + if mon == (mm1 + 1): # case where month is correct # but subsequent month needs to be +1 m[j] = np.copy(grace_month[j]) - elif (mon == mm1) and (mon != m[j-1]): + elif (mon == mm1) and (mon != m[j - 1]): # case where prior month needed to be -1 # but current month is correct m[j] = np.copy(grace_month[j]) - elif (mon == mm1): + elif mon == mm1: # case where month should be +1 m[j] = grace_month[j] + 1 elif (mon == mp1) and ((mon == (mm1 + 2)) or (mp2 == (mp1 + 1))): @@ -386,8 +435,9 @@ def adjust_months(grace_month): # update months and remove singleton dimensions if necessary return np.squeeze(m) + # PURPOSE: convert calendar dates to GRACE/GRACE-FO months -def calendar_to_grace(year,month=1,around=np.floor): +def calendar_to_grace(year, month=1, around=np.floor): """ Converts calendar dates to GRACE/GRACE-FO months @@ -405,9 +455,10 @@ def calendar_to_grace(year,month=1,around=np.floor): grace_month: np.ndarray GRACE/GRACE-FO month """ - grace_month = around(12.0*(year - 2002.0)) + month + grace_month = around(12.0 * (year - 2002.0)) + month return np.array(grace_month, dtype=int) + # PURPOSE: convert GRACE/GRACE-FO months to calendar dates def grace_to_calendar(grace_month): """ @@ -425,10 +476,11 @@ def grace_to_calendar(grace_month): month: np.ndarray calendar month """ - year = np.array(2002 + (grace_month-1)//12).astype(int) - month = np.mod(grace_month-1,12) + 1 + year = np.array(2002 + (grace_month - 1) // 12).astype(int) + month = np.mod(grace_month - 1, 12) + 1 return (year, month) + # PURPOSE: convert calendar dates to Julian days def calendar_to_julian(year_decimal): """ @@ -448,18 +500,25 @@ def calendar_to_julian(year_decimal): year = np.floor(year_decimal) # calculation of day of the year dpy = calendar_days(year).sum() - DofY = dpy*(year_decimal % 1) + DofY = dpy * (year_decimal % 1) # Calculation of the Julian date from year and DofY - JD = np.array(367.0*year - np.floor(7.0*year/4.0) - - np.floor(3.0*(np.floor((7.0*year - 1.0)/700.0) + 1.0)/4.0) + - DofY + 1721058.5, dtype=np.float64) + JD = np.array( + 367.0 * year + - np.floor(7.0 * year / 4.0) + - np.floor(3.0 * (np.floor((7.0 * year - 1.0) / 700.0) + 1.0) / 4.0) + + DofY + + 1721058.5, + dtype=np.float64, + ) return JD + # days per month in a leap and a standard year # only difference is February (29 vs. 28) _dpm_leap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] _dpm_stnd = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + # PURPOSE: gets the number of days per month for a given year def calendar_days(year): """ @@ -482,16 +541,17 @@ def calendar_days(year): # Subtracting a leap year every 100 years ==> average 365.24 # Adding a leap year back every 400 years ==> average 365.2425 # Subtracting a leap year every 4000 years ==> average 365.24225 - m4 = (year % 4) - m100 = (year % 100) - m400 = (year % 400) - m4000 = (year % 4000) + m4 = year % 4 + m100 = year % 100 + m400 = year % 400 + m4000 = year % 4000 # find indices for standard years and leap years using criteria - if ((m4 == 0) & (m100 != 0) | (m400 == 0) & (m4000 != 0)): + if (m4 == 0) & (m100 != 0) | (m400 == 0) & (m4000 != 0): return np.array(_dpm_leap, dtype=np.float64) - elif ((m4 != 0) | (m100 == 0) & (m400 != 0) | (m4000 == 0)): + elif (m4 != 0) | (m100 == 0) & (m400 != 0) | (m4000 == 0): return np.array(_dpm_stnd, dtype=np.float64) + # PURPOSE: convert a numpy datetime array to delta times since an epoch def convert_datetime(date, epoch=_unix_epoch): """ @@ -517,6 +577,7 @@ def convert_datetime(date, epoch=_unix_epoch): # convert to delta time return (date - epoch) / np.timedelta64(1, 's') + # PURPOSE: convert times from seconds since epoch1 to time since epoch2 def convert_delta_time(delta_time, epoch1=None, epoch2=None, scale=1.0): """ @@ -545,12 +606,21 @@ def convert_delta_time(delta_time, epoch1=None, epoch2=None, scale=1.0): # calculate the total difference in time in seconds delta_time_epochs = (epoch2 - epoch1) / np.timedelta64(1, 's') # subtract difference in time and rescale to output units - return scale*(delta_time - delta_time_epochs) + return scale * (delta_time - delta_time_epochs) + # PURPOSE: calculate the delta time from calendar date # http://scienceworld.wolfram.com/astronomy/JulianDate.html -def convert_calendar_dates(year, month, day, hour=0.0, minute=0.0, second=0.0, - epoch=(1992,1,1,0,0,0), scale=1.0): +def convert_calendar_dates( + year, + month, + day, + hour=0.0, + minute=0.0, + second=0.0, + epoch=(1992, 1, 1, 0, 0, 0), + scale=1.0, +): """ Calculate the time in units since ``epoch`` from calendar dates @@ -580,10 +650,20 @@ def convert_calendar_dates(year, month, day, hour=0.0, minute=0.0, second=0.0, """ # calculate date in Modified Julian Days (MJD) from calendar date # MJD: days since November 17, 1858 (1858-11-17T00:00:00) - MJD = 367.0*year - np.floor(7.0*(year + np.floor((month+9.0)/12.0))/4.0) - \ - np.floor(3.0*(np.floor((year + (month - 9.0)/7.0)/100.0) + 1.0)/4.0) + \ - np.floor(275.0*month/9.0) + day + hour/24.0 + minute/1440.0 + \ - second/86400.0 + 1721028.5 - 2400000.5 + MJD = ( + 367.0 * year + - np.floor(7.0 * (year + np.floor((month + 9.0) / 12.0)) / 4.0) + - np.floor( + 3.0 * (np.floor((year + (month - 9.0) / 7.0) / 100.0) + 1.0) / 4.0 + ) + + np.floor(275.0 * month / 9.0) + + day + + hour / 24.0 + + minute / 1440.0 + + second / 86400.0 + + 1721028.5 + - 2400000.5 + ) # convert epochs to datetime variables epoch1 = np.datetime64(datetime.datetime(*_mjd_epoch)) if isinstance(epoch, (tuple, list)): @@ -593,11 +673,13 @@ def convert_calendar_dates(year, month, day, hour=0.0, minute=0.0, second=0.0, # calculate the total difference in time in days delta_time_epochs = (epoch - epoch1) / np.timedelta64(1, 'D') # return the date in units (default days) since epoch - return scale*np.array(MJD - delta_time_epochs, dtype=np.float64) + return scale * np.array(MJD - delta_time_epochs, dtype=np.float64) + # PURPOSE: Converts from calendar dates into decimal years -def convert_calendar_decimal(year, month, day=None, hour=None, minute=None, - second=None, DofY=None): +def convert_calendar_decimal( + year, month, day=None, hour=None, minute=None, second=None, DofY=None +): """ Converts from calendar date into decimal years taking into account leap years :cite:p:`Dershowitz:2007cc` @@ -658,34 +740,34 @@ def convert_calendar_decimal(year, month, day=None, hour=None, minute=None, # Subtracting a leap year every 100 years ==> average 365.24 # Adding a leap year back every 400 years ==> average 365.2425 # Subtracting a leap year every 4000 years ==> average 365.24225 - m4 = (cal_date['year'] % 4) - m100 = (cal_date['year'] % 100) - m400 = (cal_date['year'] % 400) - m4000 = (cal_date['year'] % 4000) + m4 = cal_date['year'] % 4 + m100 = cal_date['year'] % 100 + m400 = cal_date['year'] % 400 + m4000 = cal_date['year'] % 4000 # find indices for standard years and leap years using criteria - leap, = np.nonzero((m4 == 0) & (m100 != 0) | (m400 == 0) & (m4000 != 0)) - stnd, = np.nonzero((m4 != 0) | (m100 == 0) & (m400 != 0) | (m4000 == 0)) + (leap,) = np.nonzero((m4 == 0) & (m100 != 0) | (m400 == 0) & (m4000 != 0)) + (stnd,) = np.nonzero((m4 != 0) | (m100 == 0) & (m400 != 0) | (m4000 == 0)) # calculate the day of the year if DofY is not None: # if entered directly as an input # remove 1 so day 1 (Jan 1st) = 0.0 in decimal format - cal_date['DofY'][:] = np.squeeze(DofY)-1 + cal_date['DofY'][:] = np.squeeze(DofY) - 1 else: # use calendar month and day of the month to calculate day of the year # month minus 1: January = 0, February = 1, etc (indice of month) # in decimal form: January = 0.0 - month_m1 = np.array(cal_date['month'],dtype=np.int64) - 1 + month_m1 = np.array(cal_date['month'], dtype=np.int64) - 1 # day of month if day is not None: # remove 1 so 1st day of month = 0.0 in decimal format - cal_date['day'][:] = np.squeeze(day)-1.0 + cal_date['day'][:] = np.squeeze(day) - 1.0 else: # if not entering days as an input # will use the mid-month value - cal_date['day'][leap] = dpm_leap[month_m1[leap]]/2.0 - cal_date['day'][stnd] = dpm_stnd[month_m1[stnd]]/2.0 + cal_date['day'][leap] = dpm_leap[month_m1[leap]] / 2.0 + cal_date['day'][stnd] = dpm_stnd[month_m1[stnd]] / 2.0 # create matrix with the lower half = 1 # this matrix will be used in a matrix multiplication @@ -693,7 +775,7 @@ def convert_calendar_decimal(year, month, day=None, hour=None, minute=None, # the -1 will make the diagonal == 0 # i.e. first row == all zeros and the # last row == ones for all but the last element - mon_mat=np.tri(12,12,-1) + mon_mat = np.tri(12, 12, -1) # using a dot product to calculate total number of days # for the months before the input date # basically is sum(i*dpm) @@ -705,10 +787,12 @@ def convert_calendar_decimal(year, month, day=None, hour=None, minute=None, # calculate the day of the year for leap and standard # use total days of all months before date # and add number of days before date in month - cal_date['DofY'][stnd] = cal_date['day'][stnd] + \ - np.dot(mon_mat[month_m1[stnd],:],dpm_stnd) - cal_date['DofY'][leap] = cal_date['day'][leap] + \ - np.dot(mon_mat[month_m1[leap],:],dpm_leap) + cal_date['DofY'][stnd] = cal_date['day'][stnd] + np.dot( + mon_mat[month_m1[stnd], :], dpm_stnd + ) + cal_date['DofY'][leap] = cal_date['day'][leap] + np.dot( + mon_mat[month_m1[leap], :], dpm_leap + ) # hour of day (else is zero) if hour is not None: @@ -726,18 +810,23 @@ def convert_calendar_decimal(year, month, day=None, hour=None, minute=None, # convert hours, minutes and seconds into days # convert calculated fractional days into decimal fractions of the year # Leap years - t_date[leap] = cal_date['year'][leap] + \ - (cal_date['DofY'][leap] + cal_date['hour'][leap]/24. + \ - cal_date['minute'][leap]/1440. + \ - cal_date['second'][leap]/86400.)/np.sum(dpm_leap) + t_date[leap] = cal_date['year'][leap] + ( + cal_date['DofY'][leap] + + cal_date['hour'][leap] / 24.0 + + cal_date['minute'][leap] / 1440.0 + + cal_date['second'][leap] / 86400.0 + ) / np.sum(dpm_leap) # Standard years - t_date[stnd] = cal_date['year'][stnd] + \ - (cal_date['DofY'][stnd] + cal_date['hour'][stnd]/24. + \ - cal_date['minute'][stnd]/1440. + \ - cal_date['second'][stnd]/86400.)/np.sum(dpm_stnd) + t_date[stnd] = cal_date['year'][stnd] + ( + cal_date['DofY'][stnd] + + cal_date['hour'][stnd] / 24.0 + + cal_date['minute'][stnd] / 1440.0 + + cal_date['second'][stnd] / 86400.0 + ) / np.sum(dpm_stnd) return t_date + # PURPOSE: Converts from Julian day to calendar date and time def convert_julian(JD, **kwargs): """ @@ -777,15 +866,18 @@ def convert_julian(JD, **kwargs): kwargs.setdefault('format', 'dict') # raise warnings for deprecated keyword arguments deprecated_keywords = dict(ASTYPE='astype', FORMAT='format') - for old,new in deprecated_keywords.items(): + for old, new in deprecated_keywords.items(): if old in kwargs.keys(): - warnings.warn(f"""Deprecated keyword argument {old}. - Changed to '{new}'""", DeprecationWarning) + warnings.warn( + f"""Deprecated keyword argument {old}. + Changed to '{new}'""", + DeprecationWarning, + ) # set renamed argument to not break workflows kwargs[new] = copy.copy(kwargs[old]) # convert to array if only a single value was imported - if (np.ndim(JD) == 0): + if np.ndim(JD) == 0: JD = np.atleast_1d(JD) single_value = True else: @@ -796,24 +888,24 @@ def convert_julian(JD, **kwargs): C = np.zeros_like(JD) # calculate C for dates before and after the switch to Gregorian IGREG = 2299161.0 - ind1, = np.nonzero(JDO < IGREG) + (ind1,) = np.nonzero(JDO < IGREG) C[ind1] = JDO[ind1] + 1524.0 - ind2, = np.nonzero(JDO >= IGREG) - B = np.floor((JDO[ind2] - 1867216.25)/36524.25) - C[ind2] = JDO[ind2] + B - np.floor(B/4.0) + 1525.0 + (ind2,) = np.nonzero(JDO >= IGREG) + B = np.floor((JDO[ind2] - 1867216.25) / 36524.25) + C[ind2] = JDO[ind2] + B - np.floor(B / 4.0) + 1525.0 # calculate coefficients for date conversion - D = np.floor((C - 122.1)/365.25) - E = np.floor((365.0 * D) + np.floor(D/4.0)) - F = np.floor((C - E)/30.6001) + D = np.floor((C - 122.1) / 365.25) + E = np.floor((365.0 * D) + np.floor(D / 4.0)) + F = np.floor((C - E) / 30.6001) # calculate day, month, year and hour - day = np.floor(C - E + 0.5) - np.floor(30.6001*F) - month = F - 1.0 - 12.0*np.floor(F/14.0) - year = D - 4715.0 - np.floor((7.0 + month)/10.0) - hour = np.floor(24.0*(JD + 0.5 - JDO)) + day = np.floor(C - E + 0.5) - np.floor(30.6001 * F) + month = F - 1.0 - 12.0 * np.floor(F / 14.0) + year = D - 4715.0 - np.floor((7.0 + month) / 10.0) + hour = np.floor(24.0 * (JD + 0.5 - JDO)) # calculate minute and second - G = (JD + 0.5 - JDO) - hour/24.0 - minute = np.floor(G*1440.0) - second = (G - minute/1440.0) * 86400.0 + G = (JD + 0.5 - JDO) - hour / 24.0 + minute = np.floor(G * 1440.0) + second = (G - minute / 1440.0) * 86400.0 # convert all variables to output type (from float) if kwargs['astype'] is not None: @@ -834,10 +926,16 @@ def convert_julian(JD, **kwargs): second = second.item(0) # return date variables in output format - if (kwargs['format'] == 'dict'): - return dict(year=year, month=month, day=day, - hour=hour, minute=minute, second=second) - elif (kwargs['format'] == 'tuple'): + if kwargs['format'] == 'dict': + return dict( + year=year, + month=month, + day=day, + hour=hour, + minute=minute, + second=second, + ) + elif kwargs['format'] == 'tuple': return (year, month, day, hour, minute, second) - elif (kwargs['format'] == 'zip'): + elif kwargs['format'] == 'zip': return zip(year, month, day, hour, minute, second) diff --git a/gravity_toolkit/time_series/amplitude.py b/gravity_toolkit/time_series/amplitude.py index 7d174cb2..5a5b6661 100755 --- a/gravity_toolkit/time_series/amplitude.py +++ b/gravity_toolkit/time_series/amplitude.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" amplitude.py -Written by Tyler Sutterley (01/2023) +Written by Tyler Sutterley (07/2026) Calculate the amplitude and phase of a harmonic function from calculated sine and cosine of a series of measurements @@ -21,6 +21,7 @@ numpy: Scientific Computing Tools For Python (https://numpy.org) UPDATE HISTORY: + Updated 07/2026: use np.radians to convert from degrees to radians Updated 01/2023: refactored time series analysis functions Updated 04/2022: updated docstrings to numpy documentation format Updated 07/2020: added function docstrings @@ -28,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 @@ -49,5 +52,5 @@ def amplitude(bsin, bcos): phase from the harmonic functions in degrees """ ampl = np.sqrt(bsin**2.0 + bcos**2.0) - ph = 180.0*np.arctan2(bcos, bsin)/np.pi - return (ampl,ph) + ph = np.degrees(np.arctan2(bcos, bsin)) + return (ampl, ph) diff --git a/gravity_toolkit/time_series/fit.py b/gravity_toolkit/time_series/fit.py index 99af58d2..d01431af 100644 --- a/gravity_toolkit/time_series/fit.py +++ b/gravity_toolkit/time_series/fit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" fit.py Written by Tyler Sutterley (06/2023) Utilities for fitting time-series data with regression models @@ -11,9 +11,11 @@ Updated 06/2023: made the tidal aliasing period an option Written 05/2023 """ + from __future__ import annotations import numpy as np + # PURPOSE: build a list of tidal aliasing terms for regression fit def aliasing_terms(t_in: np.ndarray, period=161.0): """ @@ -37,19 +39,19 @@ def aliasing_terms(t_in: np.ndarray, period=161.0): # number of time points nmax = len(t_in) # create custom terms for tidal aliasing during GRACE period - ii, = np.nonzero(t_in[0:nmax] < 2018.0) + (ii,) = np.nonzero(t_in[0:nmax] < 2018.0) SIN = np.zeros((nmax)) COS = np.zeros((nmax)) - SIN[ii] = np.sin(np.pi*t_in[ii]*730.50/period) - COS[ii] = np.cos(np.pi*t_in[ii]*730.50/period) + SIN[ii] = np.sin(np.pi * t_in[ii] * 730.50 / period) + COS[ii] = np.cos(np.pi * t_in[ii] * 730.50 / period) TERMS.append(SIN) TERMS.append(COS) # create custom terms for tidal aliasing during GRACE-FO period - ii, = np.nonzero(t_in[0:nmax] >= 2018.0) + (ii,) = np.nonzero(t_in[0:nmax] >= 2018.0) SIN = np.zeros((nmax)) COS = np.zeros((nmax)) - SIN[ii] = np.sin(np.pi*t_in[ii]*730.50/period) - COS[ii] = np.cos(np.pi*t_in[ii]*730.50/period) + SIN[ii] = np.sin(np.pi * t_in[ii] * 730.50 / period) + COS[ii] = np.cos(np.pi * t_in[ii] * 730.50 / period) TERMS.append(SIN) TERMS.append(COS) # return the fit terms diff --git a/gravity_toolkit/time_series/lomb_scargle.py b/gravity_toolkit/time_series/lomb_scargle.py index 8b52a413..03ccc3a2 100755 --- a/gravity_toolkit/time_series/lomb_scargle.py +++ b/gravity_toolkit/time_series/lomb_scargle.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" lomb_scargle.py Written by Tyler Sutterley (06/2024) @@ -53,9 +53,11 @@ Updated 01/2015: added centroid output Written 08/2013 """ + import numpy as np import scipy.signal + def lomb_scargle(t_in, d_in, **kwargs): """ Computes periodograms for least-squares spectral analysis following @@ -110,15 +112,15 @@ def lomb_scargle(t_in, d_in, **kwargs): # number of independent measurements nmax = np.count_nonzero(np.isfinite(d_in)) - nyquist = 1.0/(2.0*np.mean(t_in[1:] - t_in[0:-1])) + nyquist = 1.0 / (2.0 * np.mean(t_in[1:] - t_in[0:-1])) # angular frequency range if kwargs['OMEGA']: OMEGA = np.atleast_1d(kwargs['OMEGA']) elif kwargs['FREQUENCY']: - OMEGA = np.atleast_1d(kwargs['FREQUENCY'])/(2.0*np.pi) + OMEGA = np.atleast_1d(kwargs['FREQUENCY']) / (2.0 * np.pi) elif kwargs['PERIOD']: - OMEGA = (2.0*np.pi)/np.atleast_1d(kwargs['PERIOD']) + OMEGA = (2.0 * np.pi) / np.atleast_1d(kwargs['PERIOD']) else: raise ValueError('Frequency range must be defined') @@ -132,34 +134,42 @@ def lomb_scargle(t_in, d_in, **kwargs): # analysis based on sample size. From Horne and Baliunas, # "A Prescription for Period Analysis of Unevenly Sampled Time Series", # The Astrophysical Journal, 392: 757-763, 1986. - independent_freq = np.round(-6.362 + 1.193*nmax + 0.00098*nmax**2) + independent_freq = np.round(-6.362 + 1.193 * nmax + 0.00098 * nmax**2) # if less than 1 independent frequency: set equal to 1 independent_freq = np.maximum(independent_freq, 1) # scaling the date (t[0] = 0) t = t_in - t_in[0] # periods and frequencies considered - frequency = angular_freq/(2.0*np.pi) - period = 2.0*np.pi/angular_freq + frequency = angular_freq / (2.0 * np.pi) + period = 2.0 * np.pi / angular_freq # scaling the data to be mean 0 with variance 1 - data_norm = (d_in - d_in.mean())/d_in.std() + data_norm = (d_in - d_in.mean()) / d_in.std() # computing the lomb-scargle periodogram # "normalized" spectral density refers to variance term in denominator # PowerDensity has exponential probability distribution with unit mean # can calculate normalized as described in Scipy reference - PowerDensity = scipy.signal.lombscargle(t, data_norm, angular_freq, - normalize=kwargs['NORMALIZE']) + PowerDensity = scipy.signal.lombscargle( + t, data_norm, angular_freq, normalize=kwargs['NORMALIZE'] + ) # probability of frequencies (NULL test, significance of peak) - probability = 1.0 - (1.0-np.exp(-PowerDensity))**independent_freq + probability = 1.0 - (1.0 - np.exp(-PowerDensity)) ** independent_freq # probability contours p = np.atleast_1d(kwargs['p']) - contour = -np.log(1.0 - (1 - p)**(1.0/independent_freq)) + contour = -np.log(1.0 - (1 - p) ** (1.0 / independent_freq)) # period at peak (maximum probability) ipeak = np.argmax(PowerDensity) peak = period[ipeak] # period at signal centroid - centroid = np.sum(period*PowerDensity)/np.sum(PowerDensity) - - return {'PowerDensity':PowerDensity, 'Probability':probability, - 'frequency':frequency, 'period':period, 'contour':contour, - 'Nyquist':nyquist, 'peak':peak, 'centroid':centroid} + centroid = np.sum(period * PowerDensity) / np.sum(PowerDensity) + + return { + 'PowerDensity': PowerDensity, + 'Probability': probability, + 'frequency': frequency, + 'period': period, + 'contour': contour, + 'Nyquist': nyquist, + 'peak': peak, + 'centroid': centroid, + } diff --git a/gravity_toolkit/time_series/piecewise.py b/gravity_toolkit/time_series/piecewise.py index e2349007..2fd9e08b 100755 --- a/gravity_toolkit/time_series/piecewise.py +++ b/gravity_toolkit/time_series/piecewise.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" piecewise.py -Written by Tyler Sutterley (04/2023) +Written by Tyler Sutterley (07/2026) Fits a synthetic signal to data over a time period by ordinary or weighted least-squares for breakpoint analysis @@ -61,6 +61,7 @@ scipy: Scientific Tools for Python (https://docs.scipy.org/doc/) UPDATE HISTORY: + Updated 07/2026: use np.hypot to calculate the sum of two squares Updated 04/2023: option to include extra fit terms in the design matrix Updated 01/2023: refactored time series analysis functions Updated 04/2022: updated docstrings to numpy documentation format @@ -92,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` @@ -196,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) @@ -207,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 @@ -249,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.sqrt(temp_err[1]**2 + temp_err[2]**2) + 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.sqrt(temp_err[1]**2 + temp_err[2]**2) + 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: @@ -351,20 +404,34 @@ 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) - st_err[2] = np.sqrt(temp_std[1]**2 + temp_std[2]**2) + st_err[2] = np.hypot(temp_std[1], temp_std[2]) # Recalculating beta2 error beta_err = np.copy(temp_err) - beta_err[2] = np.sqrt(temp_err[1]**2 + temp_err[2]**2) + beta_err[2] = np.hypot(temp_err[1], temp_err[2]) - return {'beta':beta_out, 'error':beta_err, 'std_err':st_err, 'R2':rsquare, - 'R2Adj':rsq_adj, 'MSE':MSE, 'NRMSE':NRMSE, 'AIC':AIC, 'BIC':BIC, - 'LOGLIK':log_lik, 'model':mod, 'simple': simple, 'residual':res, - 'N':n_terms, 'DOF': nu, 'cov_mat':Hinv} + return { + 'beta': beta_out, + 'error': beta_err, + 'std_err': st_err, + 'R2': rsquare, + 'R2Adj': rsq_adj, + 'MSE': MSE, + 'NRMSE': NRMSE, + 'AIC': AIC, + 'BIC': BIC, + 'LOGLIK': log_lik, + 'model': mod, + 'simple': simple, + 'residual': res, + 'N': n_terms, + 'DOF': nu, + 'cov_mat': Hinv, + } diff --git a/gravity_toolkit/time_series/regress.py b/gravity_toolkit/time_series/regress.py index bca9ecd4..ddb3d9c2 100755 --- a/gravity_toolkit/time_series/regress.py +++ b/gravity_toolkit/time_series/regress.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" regress.py Written by Tyler Sutterley (04/2023) @@ -101,13 +101,25 @@ Updated 01/2012: added std weighting for a error weighted least-squares Written 10/2011 """ + import numpy as np import scipy.stats import scipy.special -def regress(t_in, d_in, ORDER=1, CYCLES=[0.5,1.0], TERMS=[], - DATA_ERR=0, WEIGHT=False, RELATIVE=Ellipsis, STDEV=0, CONF=0, - AICc=True): + +def regress( + t_in, + d_in, + ORDER=1, + CYCLES=[0.5, 1.0], + TERMS=[], + DATA_ERR=0, + WEIGHT=False, + RELATIVE=Ellipsis, + STDEV=0, + CONF=0, + AICc=True, +): r""" Fits a synthetic signal to data over a time period by ordinary or weighted least-squares @@ -196,19 +208,19 @@ def regress(t_in, d_in, ORDER=1, CYCLES=[0.5,1.0], TERMS=[], t_rel = t_in[RELATIVE].mean() elif isinstance(RELATIVE, (float, int, np.float64, np.int_)): t_rel = np.copy(RELATIVE) - elif (RELATIVE == Ellipsis): + elif RELATIVE == Ellipsis: t_rel = t_in[RELATIVE].mean() # create design matrix based on polynomial order and harmonics # with any additional fit terms DMAT = [] # add polynomial orders (0=constant, 1=linear, 2=quadratic) - for o in range(ORDER+1): - DMAT.append((t_in-t_rel)**o) + for o in range(ORDER + 1): + DMAT.append((t_in - t_rel) ** o) # add cyclical terms (0.5=semi-annual, 1=annual) for c in CYCLES: - DMAT.append(np.sin(2.0*np.pi*t_in/np.float64(c))) - DMAT.append(np.cos(2.0*np.pi*t_in/np.float64(c))) + DMAT.append(np.sin(2.0 * np.pi * t_in / np.float64(c))) + DMAT.append(np.cos(2.0 * np.pi * t_in / np.float64(c))) # add additional terms to the design matrix for t in TERMS: DMAT.append(t) @@ -218,36 +230,36 @@ def regress(t_in, d_in, ORDER=1, CYCLES=[0.5,1.0], TERMS=[], # Calculating Least-Squares Coefficients if WEIGHT: # Weighted Least-Squares fitting - if (np.ndim(DATA_ERR) == 0): + if np.ndim(DATA_ERR) == 0: raise ValueError('Input DATA_ERR for Weighted Least-Squares') # check if any error values are 0 (prevent infinite weights) if np.count_nonzero(DATA_ERR == 0.0): # change to minimum floating point value DATA_ERR[DATA_ERR == 0.0] = np.finfo(np.float64).eps # Weight Precision - wi = np.squeeze(DATA_ERR**(-2)) + wi = np.squeeze(DATA_ERR ** (-2)) # If uncorrelated weights are the diagonal W = np.diag(wi) # Least-Squares fitting # Temporary Matrix: Inv(X'.W.X) - TM1 = np.linalg.inv(np.dot(np.transpose(DMAT),np.dot(W,DMAT))) + TM1 = np.linalg.inv(np.dot(np.transpose(DMAT), np.dot(W, DMAT))) # Temporary Matrix: (X'.W.Y) - TM2 = np.dot(np.transpose(DMAT),np.dot(W,d_in)) + TM2 = np.dot(np.transpose(DMAT), np.dot(W, d_in)) # Least Squares Solutions: Inv(X'.W.X).(X'.W.Y) - beta_mat = np.dot(TM1,TM2) - else:# Standard Least-Squares fitting (the [0] denotes coefficients output) - beta_mat = np.linalg.lstsq(DMAT,d_in,rcond=-1)[0] + beta_mat = np.dot(TM1, TM2) + else: # Standard Least-Squares fitting (the [0] denotes coefficients output) + beta_mat = np.linalg.lstsq(DMAT, d_in, rcond=-1)[0] # Weights are equal wi = 1.0 # number of terms in least-squares solution n_terms = len(beta_mat) # modelled time-series - mod = np.dot(DMAT,beta_mat) + mod = np.dot(DMAT, beta_mat) # residual - res = d_in[0:nmax] - np.dot(DMAT,beta_mat) + res = d_in[0:nmax] - np.dot(DMAT, beta_mat) # Fitted Values without (and with) climate oscillations - simple = np.dot(DMAT[:,0:(ORDER+1)],beta_mat[0:(ORDER+1)]) + simple = np.dot(DMAT[:, 0 : (ORDER + 1)], beta_mat[0 : (ORDER + 1)]) season = mod - simple # nu = Degrees of Freedom @@ -255,94 +267,138 @@ def regress(t_in, d_in, ORDER=1, CYCLES=[0.5,1.0], TERMS=[], # calculating R^2 values # SStotal = sum((Y-mean(Y))**2) - SStotal = np.dot(np.transpose(d_in[0:nmax] - np.mean(d_in[0:nmax])), - (d_in[0:nmax] - np.mean(d_in[0:nmax]))) + SStotal = np.dot( + np.transpose(d_in[0:nmax] - np.mean(d_in[0:nmax])), + (d_in[0:nmax] - np.mean(d_in[0:nmax])), + ) # SSerror = sum((Y-X*B)**2) - SSerror = np.dot(np.transpose(d_in[0:nmax] - np.dot(DMAT,beta_mat)), - (d_in[0:nmax] - np.dot(DMAT,beta_mat))) + SSerror = np.dot( + np.transpose(d_in[0:nmax] - np.dot(DMAT, beta_mat)), + (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) # R**2 term = 1- SSerror/SStotal - rsquare = 1.0 - (SSerror/SStotal) + rsquare = 1.0 - (SSerror / SStotal) # Adjusted R**2 term: weighted by degrees of freedom - rsq_adj = 1.0 - (SSerror/SStotal)*np.float64((nmax-1.0)/nu) + rsq_adj = 1.0 - (SSerror / SStotal) * np.float64((nmax - 1.0) / nu) # Fit Criterion # number of parameters including the intercept and the variance K = np.float64(n_terms + 1) # Log-Likelihood with weights (if unweighted, weight portions == 0) # log(L) = -0.5*n*log(sigma^2) - 0.5*n*log(2*pi) - 0.5*n - #log_lik = -0.5*nmax*(np.log(2.0 * np.pi) + 1.0 + np.log(np.sum((res**2)/nmax))) - log_lik = 0.5*(np.sum(np.log(wi)) - nmax*(np.log(2.0 * np.pi) + 1.0 - - np.log(nmax) + np.log(np.sum(wi * (res**2))))) + # log_lik = -0.5*nmax*(np.log(2.0 * np.pi) + 1.0 + np.log(np.sum((res**2)/nmax))) + log_lik = 0.5 * ( + np.sum(np.log(wi)) + - nmax + * ( + np.log(2.0 * np.pi) + + 1.0 + - np.log(nmax) + + np.log(np.sum(wi * (res**2))) + ) + ) # Aikaike's Information Criterion - AIC = -2.0*log_lik + 2.0*K + AIC = -2.0 * log_lik + 2.0 * K if AICc: # Second-Order AIC correcting for small sample sizes (restricted) # Burnham and Anderson (2002) advocate use of AICc where # ratio num/K is small # A small ratio is defined in the definition at approximately < 40 - AIC += (2.0*K*(K+1.0))/(nmax - K - 1.0) + AIC += (2.0 * K * (K + 1.0)) / (nmax - K - 1.0) # Bayesian Information Criterion (Schwarz Criterion) - BIC = -2.0*log_lik + np.log(nmax)*K + BIC = -2.0 * log_lik + np.log(nmax) * K # Error Analysis if WEIGHT: # WEIGHTED LEAST-SQUARES CASE (unequal error) # Covariance Matrix - Hinv = np.linalg.inv(np.dot(np.transpose(DMAT),np.dot(W,DMAT))) + Hinv = np.linalg.inv(np.dot(np.transpose(DMAT), np.dot(W, DMAT))) # Normal Equations - NORMEQ = np.dot(Hinv,np.transpose(np.dot(W,DMAT))) + NORMEQ = np.dot(Hinv, np.transpose(np.dot(W, DMAT))) beta_err = np.zeros((n_terms)) # Propagating RMS errors - for i in range(0,n_terms): - beta_err[i] = np.sqrt(np.sum((NORMEQ[i,:]*DATA_ERR)**2)) + for i in range(0, n_terms): + beta_err[i] = np.sqrt(np.sum((NORMEQ[i, :] * DATA_ERR) ** 2)) # Weighted sum of squares Error - WSSE = np.dot(np.transpose(wi*(d_in[0:nmax] - np.dot(DMAT,beta_mat))), - wi*(d_in[0:nmax] - np.dot(DMAT,beta_mat)))/np.float64(nu) + WSSE = np.dot( + np.transpose(wi * (d_in[0:nmax] - np.dot(DMAT, beta_mat))), + wi * (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) / np.float64(nu) - return {'beta':beta_mat, 'error':beta_err, 'R2':rsquare, - 'R2Adj':rsq_adj, 'WSSE':WSSE, 'AIC':AIC, 'BIC':BIC, - 'LOGLIK':log_lik, 'model':mod, 'residual':res, 'simple':simple, - 'season':season, 'N':n_terms, 'DOF':nu, 'cov_mat':Hinv} + return { + 'beta': beta_mat, + 'error': beta_err, + 'R2': rsquare, + 'R2Adj': rsq_adj, + 'WSSE': WSSE, + 'AIC': AIC, + 'BIC': BIC, + 'LOGLIK': log_lik, + 'model': mod, + 'residual': res, + 'simple': simple, + 'season': season, + 'N': n_terms, + 'DOF': nu, + 'cov_mat': Hinv, + } - elif ((not WEIGHT) and (DATA_ERR != 0)): + elif (not WEIGHT) and (DATA_ERR != 0): # LEAST-SQUARES CASE WITH KNOWN AND EQUAL ERROR - P_err = DATA_ERR*np.ones((nmax)) - Hinv = np.linalg.inv(np.dot(np.transpose(DMAT),DMAT)) + P_err = DATA_ERR * np.ones((nmax)) + Hinv = np.linalg.inv(np.dot(np.transpose(DMAT), DMAT)) # Normal Equations - NORMEQ = np.dot(Hinv,np.transpose(DMAT)) + NORMEQ = np.dot(Hinv, np.transpose(DMAT)) beta_err = np.zeros((n_terms)) - for i in range(0,n_terms): - beta_err[i] = np.sqrt(np.sum((NORMEQ[i,:]*P_err)**2)) + for i in range(0, n_terms): + beta_err[i] = np.sqrt(np.sum((NORMEQ[i, :] * P_err) ** 2)) # Mean square error - MSE = np.dot(np.transpose(d_in[0:nmax] - np.dot(DMAT,beta_mat)), - (d_in[0:nmax] - np.dot(DMAT,beta_mat)))/np.float64(nu) + MSE = np.dot( + np.transpose(d_in[0:nmax] - np.dot(DMAT, beta_mat)), + (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) / np.float64(nu) - return {'beta':beta_mat, 'error':beta_err, 'R2':rsquare, - 'R2Adj':rsq_adj, 'MSE':MSE, 'AIC':AIC, 'BIC':BIC, - 'LOGLIK':log_lik, 'model':mod, 'residual':res, 'simple':simple, - 'season':season,'N':n_terms, 'DOF':nu, 'cov_mat':Hinv} + return { + 'beta': beta_mat, + 'error': beta_err, + 'R2': rsquare, + 'R2Adj': rsq_adj, + 'MSE': MSE, + 'AIC': AIC, + 'BIC': BIC, + 'LOGLIK': log_lik, + 'model': mod, + 'residual': res, + 'simple': simple, + 'season': season, + 'N': n_terms, + 'DOF': nu, + 'cov_mat': Hinv, + } else: # STANDARD LEAST-SQUARES CASE # Regression with Errors with Unknown Standard Deviations # MSE = (1/nu)*sum((Y-X*B)**2) # Mean square error - MSE = np.dot(np.transpose(d_in[0:nmax] - np.dot(DMAT,beta_mat)), - (d_in[0:nmax] - np.dot(DMAT,beta_mat)))/np.float64(nu) + MSE = np.dot( + np.transpose(d_in[0:nmax] - np.dot(DMAT, beta_mat)), + (d_in[0:nmax] - np.dot(DMAT, beta_mat)), + ) / np.float64(nu) # Root mean square error RMSE = np.sqrt(MSE) # Normalized root mean square error - NRMSE = RMSE/(np.max(d_in[0:nmax])-np.min(d_in[0:nmax])) + NRMSE = RMSE / (np.max(d_in[0:nmax]) - np.min(d_in[0:nmax])) # Covariance Matrix # Multiplying the design matrix by itself - Hinv = np.linalg.inv(np.dot(np.transpose(DMAT),DMAT)) + Hinv = np.linalg.inv(np.dot(np.transpose(DMAT), DMAT)) # Taking the diagonal components of the cov matrix hdiag = np.diag(Hinv) # set either the standard deviation or the confidence interval - if (STDEV != 0): + if STDEV != 0: # Setting the standard deviation of the output error - alpha = 1.0 - scipy.special.erf(STDEV/np.sqrt(2.0)) - elif (CONF != 0): + alpha = 1.0 - scipy.special.erf(STDEV / np.sqrt(2.0)) + elif CONF != 0: # Setting the confidence interval of the output error alpha = 1.0 - CONF else: @@ -350,13 +406,28 @@ def regress(t_in, d_in, ORDER=1, CYCLES=[0.5,1.0], TERMS=[], alpha = 1.0 - (0.95) # Student T-Distribution with D.O.F. nu # t.ppf parallels tinv in matlab - tstar = scipy.stats.t.ppf(1.0-(alpha/2.0),nu) + tstar = scipy.stats.t.ppf(1.0 - (alpha / 2.0), nu) # beta_err is the error for each coefficient # beta_err = t(nu,1-alpha/2)*standard error - st_err = np.sqrt(MSE*hdiag) - beta_err = tstar*st_err + st_err = np.sqrt(MSE * hdiag) + beta_err = tstar * st_err - return {'beta':beta_mat, 'error':beta_err, 'std_err':st_err, 'R2':rsquare, - 'R2Adj':rsq_adj, 'MSE':MSE, 'NRMSE':NRMSE, 'AIC':AIC, 'BIC':BIC, - 'LOGLIK':log_lik, 'model':mod, 'residual':res, 'simple':simple, - 'season':season, 'N':n_terms, 'DOF':nu, 'cov_mat':Hinv} + return { + 'beta': beta_mat, + 'error': beta_err, + 'std_err': st_err, + 'R2': rsquare, + 'R2Adj': rsq_adj, + 'MSE': MSE, + 'NRMSE': NRMSE, + 'AIC': AIC, + 'BIC': BIC, + 'LOGLIK': log_lik, + 'model': mod, + 'residual': res, + 'simple': simple, + 'season': season, + 'N': n_terms, + 'DOF': nu, + 'cov_mat': Hinv, + } diff --git a/gravity_toolkit/time_series/savitzky_golay.py b/gravity_toolkit/time_series/savitzky_golay.py index 42ea690f..6ed22a78 100644 --- a/gravity_toolkit/time_series/savitzky_golay.py +++ b/gravity_toolkit/time_series/savitzky_golay.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" savitzky_golay.py Written by Tyler Sutterley (01/2023) Adapted from Numerical Recipes, Third Edition @@ -57,13 +57,16 @@ Updated 08/2015: changed sys.exit to raise ValueError Written 06/2014 """ + from __future__ import division import numpy as np import scipy.special -def savitzky_golay(t_in, y_in, WINDOW=None, ORDER=2, DERIV=0, - RATE=1, DATA_ERR=0): + +def savitzky_golay( + t_in, y_in, WINDOW=None, ORDER=2, DERIV=0, RATE=1, DATA_ERR=0 +): """ Smooth and optionally differentiate data with a Savitzky-Golay filter :cite:p:`Savitzky:1964bn,Press:1988we` @@ -101,43 +104,54 @@ def savitzky_golay(t_in, y_in, WINDOW=None, ORDER=2, DERIV=0, # verify that WINDOW is positive, odd and greater than ORDER+1 if WINDOW is None: - WINDOW = ORDER + -1*(ORDER % 2) + 3 + WINDOW = ORDER + -1 * (ORDER % 2) + 3 if WINDOW % 2 != 1 or WINDOW < 1: - raise ValueError("WINDOW size must be a positive odd number") + raise ValueError('WINDOW size must be a positive odd number') if WINDOW < ORDER + 2: - raise ValueError("WINDOW is too small for the polynomials order") + raise ValueError('WINDOW is too small for the polynomials order') # remove any singleton dimensions t_in = np.squeeze(t_in) y_in = np.squeeze(y_in) nmax = len(t_in) # order range - order_range = np.arange(ORDER+1) + order_range = np.arange(ORDER + 1) # filter half-window half_window = (WINDOW - 1) // 2 # output time-series (removing half-windows on ends) - t_out = t_in[half_window:nmax-half_window] + t_out = t_in[half_window : nmax - half_window] # output smoothed timeseries (or derivative) - y_out = np.zeros((nmax-2*half_window)) - y_err = np.zeros((nmax-2*half_window)) - for n in range(0, (nmax-(2*half_window))): - yran = y_in[n + np.arange(0, 2*half_window+1)] + y_out = np.zeros((nmax - 2 * half_window)) + y_err = np.zeros((nmax - 2 * half_window)) + for n in range(0, (nmax - (2 * half_window))): + yran = y_in[n + np.arange(0, 2 * half_window + 1)] # Vandermonde matrix for the time-series - b = np.mat([[(t_in[k]-t_in[n+half_window])**i for i in order_range] - for k in range(n, n+2*half_window+1)]) + b = np.mat( + [ + [(t_in[k] - t_in[n + half_window]) ** i for i in order_range] + for k in range(n, n + 2 * half_window + 1) + ] + ) # compute the pseudoinverse of the design matrix - m=np.linalg.pinv(b).A[DERIV]*RATE**DERIV*scipy.special.factorial(DERIV) + m = ( + np.linalg.pinv(b).A[DERIV] + * RATE**DERIV + * scipy.special.factorial(DERIV) + ) # pad the signal at the extremes with values taken from the signal - firstvals = yran[0] - np.abs(yran[1:half_window+1][::-1] - yran[0]) - lastvals = yran[-1] + np.abs(yran[-half_window-1:-1][::-1] - yran[-1]) + firstvals = yran[0] - np.abs(yran[1 : half_window + 1][::-1] - yran[0]) + lastvals = yran[-1] + np.abs( + yran[-half_window - 1 : -1][::-1] - yran[-1] + ) yn = np.concatenate((firstvals, yran, lastvals)) # compute the convolution and use middle value y_out[n] = np.convolve(m[::-1], yn, mode='valid')[half_window] - if (DATA_ERR != 0): + if DATA_ERR != 0: # if data error is known and of equal value - P_err = DATA_ERR*np.ones((4*half_window+1)) + P_err = DATA_ERR * np.ones((4 * half_window + 1)) # compute the convolution and use middle value - y_err[n] = np.sqrt(np.convolve(m[::-1]**2, P_err**2, - mode='valid')[half_window]) + y_err[n] = np.sqrt( + np.convolve(m[::-1] ** 2, P_err**2, mode='valid')[half_window] + ) - return {'data':y_out, 'error':y_err, 'time':t_out} + return {'data': y_out, 'error': y_err, 'time': t_out} diff --git a/gravity_toolkit/time_series/smooth.py b/gravity_toolkit/time_series/smooth.py index 247e934c..6ba1d0dd 100755 --- a/gravity_toolkit/time_series/smooth.py +++ b/gravity_toolkit/time_series/smooth.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" smooth.py -Written by Tyler Sutterley (01/2023) +Written by Tyler Sutterley (07/2026) Computes a moving average of a time-series using three possible routines: 1) centered moving average @@ -48,6 +48,7 @@ scipy: Scientific Tools for Python (https://docs.scipy.org/doc/) UPDATE HISTORY: + Updated 07/2026: use np.hypot to calculate the sum of two squares Updated 01/2023: refactored time series analysis functions Updated 04/2022: updated docstrings to numpy documentation format Updated 05/2021: define int/float precision to prevent deprecation warning @@ -77,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 @@ -144,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: @@ -160,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) @@ -178,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)) @@ -200,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.sqrt(AS**2 + AC**2) - annphase[ran] += wi*np.arctan2(AC,AS)*180.0/np.pi + 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.sqrt(SS**2 + SC**2) - semiphase[ran] += wi*np.arctan2(SC,SS)*180.0/np.pi + 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 @@ -245,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) @@ -310,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]) - annphase[i] = np.arctan2(AC,AS)*180.0/np.pi - annamp[i] = np.sqrt(AS**2 + AC**2) + 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]) - semiamp[i] = np.sqrt(SS**2 + SC**2) - semiphase[i] = np.arctan2(SC,SS)*180.0/np.pi + 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 36c93f0e..c31d511a 100644 --- a/gravity_toolkit/tools.py +++ b/gravity_toolkit/tools.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" tools.py -Written by Tyler Sutterley (11/2024) +Written by Tyler Sutterley (07/2026) Jupyter notebook, user interface and plotting tools PYTHON DEPENDENCIES: @@ -27,9 +27,10 @@ utilities.py: download and management utilities for files UPDATE HISTORY: + 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 @@ -41,6 +42,7 @@ Updated 12/2021: added custom colormap function for some common scales Written 09/2021 """ + import os import re import copy @@ -57,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']) @@ -113,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 @@ -148,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() @@ -207,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, @@ -228,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'] @@ -252,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""" @@ -340,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, @@ -349,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, @@ -358,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, @@ -368,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, @@ -377,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, @@ -386,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, @@ -394,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', @@ -425,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): @@ -499,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 @@ -511,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 @@ -538,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]', @@ -559,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 @@ -579,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]', @@ -606,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 @@ -675,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): @@ -745,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, @@ -754,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 @@ -798,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) @@ -812,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, @@ -825,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, @@ -845,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(): @@ -884,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 @@ -943,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 @@ -960,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)) @@ -977,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) @@ -1029,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 @@ -1050,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) @@ -1113,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): """ @@ -1123,9 +1182,10 @@ def wrap_longitudes(lon): lon: np.ndarray longitude (degrees east) """ - phi = np.arctan2(np.sin(lon*np.pi/180.0), np.cos(lon*np.pi/180.0)) + phi = np.arctan2(np.sin(np.radians(lon)), np.cos(np.radians(lon))) # convert phi from radians to degrees - return phi*180.0/np.pi + return np.degrees(phi) + # PURPOSE: parallels the matplotlib basemap shiftgrid function def shift_grid(lon0, data, lon, CYCLIC=360.0): @@ -1153,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): """ @@ -1202,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 @@ -1260,28 +1324,34 @@ def mask_oceans(xin, yin, data=None, order=0, lakes=False, masked data grid """ # read in land/sea mask - lsmask = get_data_path(['data',f'landsea_{resolution}.nc']) + lsmask = get_data_path(['data', f'landsea_{resolution}.nc']) # Land-Sea Mask with Antarctica from Rignot (2017) and Greenland from GEUS # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading landsea = spatial().from_netCDF4(lsmask, date=False, varname='LSMASK') # create land function - nth,nphi = landsea.shape - land_function = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + land_function = np.zeros((nth, nphi), dtype=bool) # extract land function from file # find land values (1) - land_function |= (landsea.data == 1) + land_function |= landsea.data == 1 # find lake values (2) if lakes: - land_function |= (landsea.data == 2) + land_function |= landsea.data == 2 # find small island values (3) - land_function |= (landsea.data == 3) + land_function |= landsea.data == 3 # find Greenland and Antarctic ice shelf values (4) if iceshelves: - land_function |= (landsea.data == 4) + land_function |= landsea.data == 4 # interpolate to output grid - mask = interp_grid(land_function.astype(np.int32), - landsea.lon, landsea.lat, xin, yin, order) + mask = interp_grid( + land_function.astype(np.int32), + landsea.lon, + landsea.lat, + xin, + yin, + order, + ) # mask input data or return the interpolated mask if data is not None: # update data mask with interpolated mask diff --git a/gravity_toolkit/units.py b/gravity_toolkit/units.py index 6fb20109..87324750 100644 --- a/gravity_toolkit/units.py +++ b/gravity_toolkit/units.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" units.py Written by Tyler Sutterley (05/2024) Contributions by Hugo Lecomte @@ -26,10 +26,12 @@ Updated 04/2020: include earth parameters as attributes Written 03/2020 """ + from __future__ import annotations import numpy as np + class units(object): r""" Class for converting spherical harmonic and spatial data to specific units @@ -54,12 +56,15 @@ class units(object): l: int spherical harmonic degree up to ``lmax`` """ + np.seterr(invalid='ignore') - def __init__(self, - lmax: int | None = None, - a_axis: float = 6.378137e8, - flat: float = 1.0/298.257223563 - ): + + def __init__( + self, + lmax: int | None = None, + a_axis: float = 6.378137e8, + flat: float = 1.0 / 298.257223563, + ): # Earth Parameters # universal gravitational constant [dyn*cm^2/g^2] self.G = 6.67430e-8 @@ -91,31 +96,27 @@ def __init__(self, self.Pa = None self.lmax = lmax # calculate spherical harmonic degree (0 is falsy) - self.l = np.arange(self.lmax+1) if (self.lmax is not None) else None + self.l = np.arange(self.lmax + 1) if (self.lmax is not None) else None @property def b_axis(self) -> float: - """semi-minor axis of the Earth's ellipsoid in cm - """ - return (1.0 - self.flat)*self.a_axis + """semi-minor axis of the Earth's ellipsoid in cm""" + return (1.0 - self.flat) * self.a_axis @property def rad_e(self) -> float: - """average radius of the Earth in cm with the same volume as the ellipsoid - """ - return self.a_axis*(1.0 - self.flat)**(1.0/3.0) + """average radius of the Earth in cm with the same volume as the ellipsoid""" + return self.a_axis * (1.0 - self.flat) ** (1.0 / 3.0) @property def rho_e(self) -> float: - r"""average density of the Earth in g/cm\ :sup:`3` - """ - return 0.75*self.GM/(self.G*np.pi*self.rad_e**3) + r"""average density of the Earth in g/cm\ :sup:`3`""" + return 0.75 * self.GM / (self.G * np.pi * self.rad_e**3) @property def mass(self) -> float: - """approximate mass of the Earth in g - """ - return (4.0/3.0)*np.pi*self.rho_e*self.rad_e**3 + """approximate mass of the Earth in g""" + return (4.0 / 3.0) * np.pi * self.rho_e * self.rad_e**3 def harmonic(self, hl, kl, ll, **kwargs): """ @@ -163,36 +164,59 @@ def harmonic(self, hl, kl, ll, **kwargs): # set default keyword arguments kwargs.setdefault('include_elastic', True) kwargs.setdefault('include_ellipsoidal', False) - fraction = np.ones((self.lmax+1)) + fraction = np.ones((self.lmax + 1)) # compensate for elastic deformation within the solid earth if kwargs['include_elastic']: fraction += kl[self.l] # include effects for Earth's oblateness if kwargs['include_ellipsoidal']: - fraction /= (1.0 - self.flat) + fraction /= 1.0 - self.flat # degree dependent coefficients # norm, fully normalized spherical harmonics - self.norm = np.ones((self.lmax+1)) + self.norm = np.ones((self.lmax + 1)) # cmwe, centimeters water equivalent [g/cm^2] - self.cmwe = self.rho_e*self.rad_e*(2.0*self.l+1.0)/fraction/3.0 + self.cmwe = ( + self.rho_e * self.rad_e * (2.0 * self.l + 1.0) / fraction / 3.0 + ) # mmwe, millimeters water equivalent [kg/m^2] - self.mmwe = 10.0*self.rho_e*self.rad_e*(2.0*self.l+1.0)/fraction/3.0 + self.mmwe = ( + 10.0 + * self.rho_e + * self.rad_e + * (2.0 * self.l + 1.0) + / fraction + / 3.0 + ) # mmGH, millimeters geoid height - self.mmGH = np.ones((self.lmax+1))*(10.0*self.rad_e) + self.mmGH = np.ones((self.lmax + 1)) * (10.0 * self.rad_e) # mmCU, millimeters elastic crustal deformation (uplift) - self.mmCU = 10.0*self.rad_e*hl[self.l]/fraction + self.mmCU = 10.0 * self.rad_e * hl[self.l] / fraction # mmCH, millimeters elastic crustal deformation (horizontal) - self.mmCH = 10.0*self.rad_e*ll[self.l]/fraction + self.mmCH = 10.0 * self.rad_e * ll[self.l] / fraction # cmVCU, centimeters viscoelastic crustal uplift - self.cmVCU = self.rad_e*(2.0*self.l+1.0)/2.0 + self.cmVCU = self.rad_e * (2.0 * self.l + 1.0) / 2.0 # mVCU, meters viscoelastic crustal uplift - self.mVCU = self.rad_e*(2.0*self.l+1.0)/200.0 + self.mVCU = self.rad_e * (2.0 * self.l + 1.0) / 200.0 # microGal, microGal gravity perturbations - self.microGal = 1.e6*self.GM*(self.l+1.0)/(self.rad_e**2.0) + self.microGal = 1.0e6 * self.GM * (self.l + 1.0) / (self.rad_e**2.0) # mbar, millibar equivalent surface pressure - self.mbar = self.g_wmo*self.rho_e*self.rad_e*(2.0*self.l+1.0)/fraction/3e3 + self.mbar = ( + self.g_wmo + * self.rho_e + * self.rad_e + * (2.0 * self.l + 1.0) + / fraction + / 3e3 + ) # Pa, pascals equivalent surface pressure - self.Pa = self.g_wmo*self.rho_e*self.rad_e*(2.0*self.l+1.0)/fraction/30.0 + self.Pa = ( + self.g_wmo + * self.rho_e + * self.rad_e + * (2.0 * self.l + 1.0) + / fraction + / 30.0 + ) # return the degree dependent unit conversions return self @@ -228,21 +252,33 @@ def spatial(self, hl, kl, ll, **kwargs): """ # set default keyword arguments kwargs.setdefault('include_elastic', True) - fraction = np.ones((self.lmax+1)) + fraction = np.ones((self.lmax + 1)) # compensate for elastic deformation within the solid earth if kwargs['include_elastic']: fraction += kl[self.l] # degree dependent coefficients # norm, fully normalized spherical harmonics - self.norm = np.ones((self.lmax+1)) + self.norm = np.ones((self.lmax + 1)) # cmwe, centimeters water equivalent [g/cm^2] - self.cmwe = 3.0*fraction/(1.0+2.0*self.l)/(4.0*np.pi*self.rad_e*self.rho_e) + self.cmwe = ( + 3.0 + * fraction + / (1.0 + 2.0 * self.l) + / (4.0 * np.pi * self.rad_e * self.rho_e) + ) # mmwe, millimeters water equivalent [kg/m^2] - self.mmwe = 3.0*fraction/(1.0+2.0*self.l)/(40.0*np.pi*self.rad_e*self.rho_e) + self.mmwe = ( + 3.0 + * fraction + / (1.0 + 2.0 * self.l) + / (40.0 * np.pi * self.rad_e * self.rho_e) + ) # mmGH, millimeters geoid height - self.mmGH = np.ones((self.lmax+1))/(4.0*np.pi*self.rad_e) + self.mmGH = np.ones((self.lmax + 1)) / (4.0 * np.pi * self.rad_e) # microGal, microGal gravity perturbations - self.microGal = (self.rad_e**2.0)/(4.0*np.pi*1.e6*self.GM)/(self.l+1.0) + self.microGal = ( + (self.rad_e**2.0) / (4.0 * np.pi * 1.0e6 * self.GM) / (self.l + 1.0) + ) # return the degree dependent unit conversions return self @@ -271,13 +307,13 @@ def bycode(var: int) -> str: Named unit code for spherical harmonics or spatial fields """ named_units = [ - 'norm', # 0: keep original scale - 'cmwe', # 1: cmwe, centimeters water equivalent - 'mmGH', # 2: mmGH, mm geoid height - 'mmCU', # 3: mmCU, mm elastic crustal deformation - 'microGal', # 4: microGal, microGal gravity perturbations - 'mbar', # 5: mbar, equivalent surface pressure - 'cmVCU' # 6: cmVCU, cm viscoelastic crustal uplift (GIA) + 'norm', # 0: keep original scale + 'cmwe', # 1: cmwe, centimeters water equivalent + 'mmGH', # 2: mmGH, mm geoid height + 'mmCU', # 3: mmCU, mm elastic crustal deformation + 'microGal', # 4: microGal, microGal gravity perturbations + 'mbar', # 5: mbar, equivalent surface pressure + 'cmVCU', # 6: cmVCU, cm viscoelastic crustal uplift (GIA) ] try: return named_units[var] @@ -299,12 +335,12 @@ def get_attributes(var: str) -> str: mmwe=('mm', 'Equivalent_Water_Thickness'), cmwe=('cm', 'Equivalent_Water_Thickness'), mmGH=('mm', 'Geoid_Height'), - mmCU=('mm','Elastic_Crustal_Uplift'), - mmCH=('mm','Horizontal_Elastic_Crustal_Deformation'), - microGal=(u'\u03BCGal', 'Gravitational_Undulation'), + mmCU=('mm', 'Elastic_Crustal_Uplift'), + mmCH=('mm', 'Horizontal_Elastic_Crustal_Deformation'), + microGal=('\u03bcGal', 'Gravitational_Undulation'), mbar=('mbar', 'Equivalent_Surface_Pressure'), cmVCU=('cm', 'Viscoelastic_Crustal_Uplift'), - mVCU=('meters', 'Viscoelastic_Crustal_Uplift') + mVCU=('meters', 'Viscoelastic_Crustal_Uplift'), ) try: return named_attributes[var] diff --git a/gravity_toolkit/utilities.py b/gravity_toolkit/utilities.py index 35348258..da2abf24 100644 --- a/gravity_toolkit/utilities.py +++ b/gravity_toolkit/utilities.py @@ -1,14 +1,18 @@ #!/usr/bin/env python -u""" +""" utilities.py -Written by Tyler Sutterley (10/2025) -Download and management utilities for syncing time and auxiliary files +Written by Tyler Sutterley (07/2026) +Download and management utilities for syncing files PYTHON DEPENDENCIES: lxml: processing XML and HTML in Python https://pypi.python.org/pypi/lxml + platformdirs: Python module for determining platform-specific directories + https://pypi.org/project/platformdirs/ UPDATE HISTORY: + Updated 07/2026: can use an environment variable to set cache directory + this overrides the default platform-specific cache directory Updated 10/2025: switch from_gfz to https as ftp server is being retired Updated 11/2024: simplify unique file name function add function to scrape GSFC website for GRACE mascon urls @@ -58,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 @@ -83,16 +88,20 @@ import posixpath import lxml.etree import subprocess -import calendar,time +import platformdirs +import calendar, time + if sys.version_info[0] == 2: from cookielib import CookieJar from urllib import urlencode + from urlparse import urlparse import urllib2 else: from http.cookiejar import CookieJar - from urllib.parse import urlencode + 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): """ @@ -113,11 +122,50 @@ def get_data_path(relpath: list | str | pathlib.Path): 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', + ensure_exists=True, +): + """ + Get the path to the user cache directory for an application + + Parameters + ---------- + relpath: list, str, pathlib.Path or None + Relative path + appname: str, default 'gravtk' + Application name + ensure_exists: bool, default True + Verify that the cache directory exists + """ + # check for custom environment variable for cache directory + cache_dir = os.environ.get('GRAVTK_CACHE_DIR') + if cache_dir: + # custom environment variable for cache directory + filepath = pathlib.Path(cache_dir).expanduser().absolute() + # ensure that the cache directory exists + filepath.mkdir(parents=True, exist_ok=True) + else: + # platform-specific cache directory + filepath = platformdirs.user_cache_path( + appname=appname, ensure_exists=ensure_exists + ) + # append relative path to cache directory + if isinstance(relpath, list): + # use *splat operator to extract from list + filepath = filepath.joinpath(*relpath) + elif isinstance(relpath, (str, pathlib.Path)): + filepath = filepath.joinpath(relpath) + return pathlib.Path(filepath) + + def import_dependency( - name: str, - extra: str = "", - raise_exception: bool = False - ): + name: str, + extra: str = '', + raise_exception: bool = False, +): """ Import an optional dependency @@ -138,8 +186,8 @@ def import_dependency( Imported module """ # check if the module name is a string - msg = f"Invalid module name: '{name}'; must be a string" - assert isinstance(name, str), msg + if not isinstance(name, str): + 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', (), {}) @@ -154,9 +202,59 @@ def import_dependency( # return the module return module + +def dependency_available( + name: str, + minversion: str | None = None, +): + """ + Checks whether a module is installed without importing it + + Adapted from ``xarray.namedarray.utils.module_available`` + + Parameters + ---------- + name: str + Module name + minversion : str, optional + Minimum version of the module + + Returns + ------- + available : bool + Whether the module is installed + """ + # check if module is available + if importlib.util.find_spec(name) is None: + return False + # check if the version is greater than the minimum required + if minversion is not None: + version = importlib.metadata.version(name) + return version >= minversion + # return if both checks are passed + return True + + +def is_valid_url(url: str) -> bool: + """ + Checks if a string is a valid URL + + Parameters + ---------- + url: str + URL to check + """ + try: + result = urlparse(str(url)) + return all([result.scheme, result.netloc]) + except AttributeError: + return False + + 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__ @@ -169,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 @@ -207,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 @@ -234,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 @@ -247,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): """ @@ -258,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): """ @@ -275,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 @@ -309,6 +403,7 @@ def get_unix_time( else: return parsed_time.timestamp() + # PURPOSE: output a time string in isoformat def isoformat(time_string: str): """ @@ -327,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): """ @@ -337,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): @@ -349,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 @@ -380,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): """ @@ -408,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 @@ -430,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: @@ -438,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 @@ -480,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}') @@ -499,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] @@ -520,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 @@ -574,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] @@ -591,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 @@ -615,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: @@ -642,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 @@ -668,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 @@ -720,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 @@ -819,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 @@ -857,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 @@ -923,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: @@ -944,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 @@ -1008,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 @@ -1016,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 @@ -1052,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') @@ -1073,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 @@ -1109,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) @@ -1130,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 @@ -1164,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 @@ -1187,6 +1309,7 @@ def revoke_token( # verbose response logging.debug(f'Token Revoked: {token}') + # NASA on-prem DAAC providers _daac_providers = { 'gesdisc': 'GES_DISC', @@ -1214,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 @@ -1225,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 @@ -1241,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 @@ -1269,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: """ @@ -1296,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: """ @@ -1315,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']): """ @@ -1333,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 @@ -1380,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 @@ -1394,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: @@ -1404,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 @@ -1474,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 @@ -1519,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 @@ -1566,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 @@ -1598,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 @@ -1631,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 @@ -1672,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: @@ -1680,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 @@ -1713,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 @@ -1759,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 @@ -1856,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}') @@ -1865,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 @@ -1875,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 = [] @@ -1896,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 @@ -1910,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 @@ -1980,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) @@ -2002,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 @@ -2055,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)?$' @@ -2120,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 `_ @@ -2160,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, @@ -2183,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) @@ -2551,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, @@ -2559,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')) @@ -2576,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) `_ @@ -2606,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: @@ -2614,5 +2830,8 @@ def icgem_list( colfiles = tree.xpath('//td[@class="tom-cell-modelfile"]//a/@href') # reduce list of files to find gfc files # return the dict of model files mapped by name - return {re.findall(r'(.*?).gfc',posixpath.basename(f)).pop():url_split(f) - for i,f in enumerate(colfiles) if re.search(r'gfc$',f)} + return { + re.findall(r'(.*?).gfc', posixpath.basename(f)).pop(): url_split(f) + for i, f in enumerate(colfiles) + if re.search(r'gfc$', f) + } diff --git a/gravity_toolkit/version.py b/gravity_toolkit/version.py index aeb2a2ef..9a0df012 100644 --- a/gravity_toolkit/version.py +++ b/gravity_toolkit/version.py @@ -1,15 +1,16 @@ #!/usr/bin/env python -u""" +""" version.py (11/2023) Gets version number of a package """ + import importlib.metadata # package metadata -metadata = importlib.metadata.metadata("gravity_toolkit") +metadata = importlib.metadata.metadata('gravity_toolkit') # get version -version = metadata["version"] +version = metadata['version'] # append "v" before the version -full_version = f"v{version}" +full_version = f'v{version}' # get project name -project_name = metadata["Name"] +project_name = metadata['Name'] diff --git a/mapping/plot_AIS_GrIS_maps.py b/mapping/plot_AIS_GrIS_maps.py index a082fb43..eaaaa51f 100644 --- a/mapping/plot_AIS_GrIS_maps.py +++ b/mapping/plot_AIS_GrIS_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_GrIS_maps.py Written by Tyler Sutterley (10/2023) @@ -63,6 +63,7 @@ Updated 09/2017: add plot scales Written 08/2017 """ + from __future__ import print_function import sys @@ -80,7 +81,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -89,20 +90,21 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # region directory, filename, title and data type region_dir = {} @@ -110,43 +112,92 @@ region_title = {} region_dtype = {} # Greenland ice divides -region_dir['N'] = ['masks','Rignot_GRE'] -region_title['N'] = ['CE','CW','NE','NO','NW','SE','SW'] +region_dir['N'] = ['masks', 'Rignot_GRE'] +region_title['N'] = ['CE', 'CW', 'NE', 'NO', 'NW', 'SE', 'SW'] # Antarctic 2012 basins -region_dir['S'] = ['masks','Rignot_ANT'] -region_title['S'] = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir['S'] = ['masks', 'Rignot_ANT'] +region_title['S'] = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename['N'] = 'divide_{0}_index.ascii' region_filename['S'] = 'basin_{0}_index.ascii' # regional datatypes -region_dtype['N'] = {'names':('lon','lat'),'formats':('f','f')} -region_dtype['S'] = {'names':('lat','lon'),'formats':('f','f')} +region_dtype['N'] = {'names': ('lon', 'lat'), 'formats': ('f', 'f')} +region_dtype['S'] = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins IMBIE_basin_file = {} -IMBIE_basin_file['N']=['masks','GRE_Basins_IMBIE2_v1.3','GRE_Basins_IMBIE2_v1.3.shp'] -IMBIE_basin_file['S']=['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file['N'] = [ + 'masks', + 'GRE_Basins_IMBIE2_v1.3', + 'GRE_Basins_IMBIE2_v1.3.shp', +] +IMBIE_basin_file['S'] = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract IMBIE_title = {} -IMBIE_title['N']=('CW','NE','NO','NW','SE','SW') -IMBIE_title['S']=('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title['N'] = ('CW', 'NE', 'NO', 'NW', 'SE', 'SW') +IMBIE_title['S'] = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics image_file = {} # MODIS mosaic of Greenland -image_file['N'] = ['MOG','mog500_2005_hp1_v1.1.tif'] +image_file['N'] = ['MOG', 'mog500_2005_hp1_v1.1.tif'] # MODIS mosaic of Antarctica -image_file['S'] = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file['S'] = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # coastline files coast_file = {} # Greenland grounded ice -coast_file['N']=['masks','GIMP','grn_ice_sheet_peripheral_glaciers.shp'] +coast_file['N'] = ['masks', 'GIMP', 'grn_ice_sheet_peripheral_glaciers.shp'] # Coastlines for antarctica (islands) -coast_file['S']=['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file['S'] = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # regional plot parameters # x and y limit @@ -165,18 +216,23 @@ projection = {} try: # cartopy transform for polar stereographic south - projection['S'] = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection['S'] = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) # cartopy transform for NSIDC polar stereographic north - projection['N'] = ccrs.Stereographic(central_longitude=-45.0, - central_latitude=+90.0,true_scale_latitude=+70.0) -except (NameError,ValueError) as exc: + projection['N'] = ccrs.Stereographic( + central_longitude=-45.0, + central_latitude=+90.0, + true_scale_latitude=+70.0, + ) +except (NameError, ValueError) as exc: pass # location and size of plot scales scale_params = {} -scale_params['N'] = (700e3,-3408462,800e3,89385,False) -scale_params['S'] = (-292e4,-230e4,1600e3,140e3,False) +scale_params['N'] = (700e3, -3408462, 800e3, 89385, False) +scale_params['S'] = (-292e4, -230e4, 1600e3, 140e3, False) + # PURPOSE: keep track of threads def info(args): @@ -187,18 +243,23 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir, HEM, projection): region_directory = base_dir.joinpath(*region_dir) # for each region for reg in region_title: # read the regional polylines - region_file = region_directory.joinpath(region_filename[HEM].format(reg)) + region_file = region_directory.joinpath( + region_filename[HEM].format(reg) + ) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Greenland and Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir, HEM): @@ -209,50 +270,69 @@ def plot_IMBIE2_basins(ax, base_dir, HEM): shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes - if (HEM == 'S'): + if HEM == 'S': # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title[HEM]] + i = [ + i + for i, a in enumerate(shape_attributes) + if a[1] in IMBIE_title[HEM] + ] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection[HEM]) - elif (HEM == 'N'): + ax.plot( + points[:, 0], points[:, 1], c='k', transform=projection[HEM] + ) + elif HEM == 'N': # no GIC or islands - i=[i for i,a in enumerate(shape_attributes) if a[0] in IMBIE_title[HEM]] + i = [ + i + for i, a in enumerate(shape_attributes) + if a[0] in IMBIE_title[HEM] + ] for indice in i: # extract lat/lon coordinates for record points = np.array(shape_entities[indice].points) # Greenland IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): + for p1, p2 in zip(parts[:-1], parts[1:]): # converting basin lat/lon into plot coordinates - ax.plot(points[p1:p2,0], points[p1:p2,1], color='k', - transform=ccrs.PlateCarree()) + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + color='k', + transform=ccrs.PlateCarree(), + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection['S']) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + c='k', + transform=projection['S'], + ) + # PURPOSE: plot Greenland and Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, HEM, START=1): @@ -260,38 +340,42 @@ def plot_grounded_ice(ax, base_dir, HEM, START=1): shape_input = shapefile.Reader(str(grounded_ice_shape_file)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - if (HEM == 'N'): - for i in range(START,START): + if HEM == 'N': + for i in range(START, START): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) - ax.plot(points[:,0], points[:,1], color='k', - transform=projection[HEM]) - if (HEM == 'S'): - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + ax.plot( + points[:, 0], points[:, 1], color='k', transform=projection[HEM] + ) + if HEM == 'S': + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], color='k', - transform=projection[HEM]) + ax.plot( + points[:, 0], points[:, 1], color='k', transform=projection[HEM] + ) + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, color='k', transform=ccrs.PlateCarree()) + # PURPOSE: plot MODIS mosaic of Antarctica and Greenland as background image def plot_image_mosaic(ax, base_dir, HEM, MASKED=True): # read MODIS mosaic of Antarctica and Greenland @@ -304,12 +388,12 @@ def plot_image_mosaic(ax, base_dir, HEM, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] - if (HEM == 'N'): + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] + if HEM == 'N': # dataset range vmin, vmax = (0, 16386) - elif (HEM == 'S'): + elif HEM == 'S': # dataset range vmin, vmax = (0, 16386) # read as grayscale image @@ -319,36 +403,75 @@ def plot_image_mosaic(ax, base_dir, HEM, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection[HEM]) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection[HEM], + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.2*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, alpha=0.5, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.2 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill( + [x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, alpha=0.5, zorder=4 + ) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=14, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=14, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=14, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=14, + color=fc2, + zorder=6, + ) + # PURPOSE: plot side by side maps of Greenland and Antarctica -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -379,11 +502,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -399,13 +522,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -414,21 +538,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -436,44 +560,48 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # calculate ratio of axes - greenland_ratio = np.float64(region_xlimit['N'][1]-region_xlimit['N'][0]) / \ - np.float64(region_ylimit['N'][1]-region_ylimit['N'][0]) - antarctica_ratio = np.float64(region_xlimit['S'][1]-region_xlimit['S'][0]) / \ - np.float64(region_ylimit['S'][1]-region_ylimit['S'][0]) - width_ratios = greenland_ratio/antarctica_ratio + greenland_ratio = np.float64( + region_xlimit['N'][1] - region_xlimit['N'][0] + ) / np.float64(region_ylimit['N'][1] - region_ylimit['N'][0]) + antarctica_ratio = np.float64( + region_xlimit['S'][1] - region_xlimit['S'][0] + ) / np.float64(region_ylimit['S'][1] - region_ylimit['S'][0]) + width_ratios = greenland_ratio / antarctica_ratio # make figure axes ax1 = {} fig = plt.figure(figsize=(15.5, 7)) - gs = gridspec.GridSpec(1, 2, width_ratios=[1,width_ratios]) - hem_flag = ['S','N'] - for i,HEM in enumerate(hem_flag): + gs = gridspec.GridSpec(1, 2, width_ratios=[1, width_ratios]) + hem_flag = ['S', 'N'] + for i, HEM in enumerate(hem_flag): ax1[i] = plt.subplot(gs[i], projection=projection[HEM]) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 -flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # for each hemisphere - for i,ax in ax1.items(): + for i, ax in ax1.items(): # set hemisphere flag HEM = hem_flag[i] logging.info(f'Hemisphere: {HEM}') @@ -484,20 +612,28 @@ def plot_grid(base_dir, FILENAMES, plot_image_mosaic(ax, base_dir, HEM) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -508,93 +644,137 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection[HEM], - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection[HEM], gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection[HEM]) + im = ax.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection[HEM], + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,163 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -828,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, @@ -857,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 @@ -865,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 352ea0db..701e4682 100644 --- a/mapping/plot_AIS_grid_3maps.py +++ b/mapping/plot_AIS_grid_3maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_grid_3maps.py Written by Tyler Sutterley (05/2023) Creates 3 GMT-like plots of the Antarctic Ice Sheet @@ -46,6 +46,7 @@ Updated 09/2019: added parameter for specifying if netCDF4 or HDF5 Written 09/2019 """ + from __future__ import print_function import sys @@ -63,7 +64,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -71,56 +72,104 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # Antarctica (AIS) # x and y limit (modified from Bamber 1km DEM) -xlimits = np.array([-3100000,3100000]) -ylimits = np.array([-2600000,2600000]) +xlimits = np.array([-3100000, 3100000]) +ylimits = np.array([-2600000, 2600000]) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -130,6 +179,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -139,9 +189,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -153,7 +205,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -161,30 +213,34 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -193,12 +249,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, MASKED=True): @@ -214,8 +270,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -223,40 +279,77 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # image extents - extents=(xmin,xmax,ymin,ymax) + extents = (xmin, xmax, ymin, ymax) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', extent=extents, - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + extent=extents, + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.2*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.2 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -287,11 +380,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -307,13 +400,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -322,21 +416,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -344,55 +438,65 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup polar stereographic maps - fig, (ax[0],ax[1],ax[2]) = plt.subplots(num=1, ncols=3, figsize=(11,3), - subplot_kw=dict(projection=projection)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, ncols=3, figsize=(11, 3), subplot_kw=dict(projection=projection) + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # plot image of MODIS mosaic of Antarctica as base layer if BASEMAP: # plot MODIS mosaic of Antarctica plot_image_mosaic(ax1, base_dir) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -403,93 +507,137 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,162 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -719,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, @@ -748,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 @@ -756,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 550ef497..ea9cbff6 100644 --- a/mapping/plot_AIS_grid_4maps.py +++ b/mapping/plot_AIS_grid_4maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_grid_4maps.py Written by Tyler Sutterley (05/2023) Creates 3 GMT-like plots of the Antarctic Ice Sheet @@ -47,6 +47,7 @@ Updated 09/2019: added parameter for specifying if netCDF4 or HDF5 Written 09/2019 """ + from __future__ import print_function import sys @@ -64,7 +65,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -72,56 +73,104 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # Antarctica (AIS) # x and y limit (modified from Bamber 1km DEM) -xlimits = np.array([-3100000,3100000]) -ylimits = np.array([-2600000,2600000]) +xlimits = np.array([-3100000, 3100000]) +ylimits = np.array([-2600000, 2600000]) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -131,6 +180,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -140,9 +190,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -154,7 +206,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -162,30 +214,34 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -194,12 +250,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, MASKED=True): @@ -215,8 +271,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -224,40 +280,77 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # image extents - extents=(xmin,xmax,ymin,ymax) + extents = (xmin, xmax, ymin, ymax) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', extent=extents, - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + extent=extents, + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.2*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.2 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=10, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=10, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=10, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=10, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -288,11 +381,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -308,13 +401,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -323,21 +417,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -345,54 +439,69 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup polar stereographic maps - fig, ((ax[0],ax[1]),(ax[2],ax[3])) = plt.subplots(num=1, nrows=2, ncols=2, - figsize=(6,6.3), subplot_kw=dict(projection=projection)) + fig, ((ax[0], ax[1]), (ax[2], ax[3])) = plt.subplots( + num=1, + nrows=2, + ncols=2, + figsize=(6, 6.3), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) - for i,ax1 in ax.items(): - # plot image of MODIS mosaic of Antarctica as base layer + for i, ax1 in ax.items(): + # plot image of MODIS mosaic of Antarctica as base layer if BASEMAP: # plot MODIS mosaic of Antarctica plot_image_mosaic(ax1, base_dir) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -403,93 +512,137 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,163 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -721,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, @@ -750,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 @@ -758,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 0125d3b1..3d8c6c2c 100644 --- a/mapping/plot_AIS_grid_maps.py +++ b/mapping/plot_AIS_grid_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_grid_maps.py Written by Tyler Sutterley (05/2023) Creates GMT-like plots for the Antarctic Ice Sheet @@ -60,6 +60,7 @@ updates to parallel new plot_AIS_grid_movie.py code Written 12/2014 """ + from __future__ import print_function import sys @@ -77,7 +78,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -85,56 +86,104 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # Antarctica (AIS) # x and y limit (modified from Bamber 1km DEM) -xlimits = np.array([-3100000,3100000]) -ylimits = np.array([-2600000,2600000]) +xlimits = np.array([-3100000, 3100000]) +ylimits = np.array([-2600000, 2600000]) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -144,6 +193,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -153,9 +203,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -168,7 +220,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -176,31 +228,35 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -209,12 +265,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, MASKED=True): @@ -230,8 +286,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -239,40 +295,77 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # image extents - extents=(xmin,xmax,ymin,ymax) + extents = (xmin, xmax, ymin, ymax) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', extent=extents, - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + extent=extents, + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.2*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.2 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAME, +def plot_grid( + base_dir, + FILENAME, DATAFORM=None, VARIABLES=[], MASK=None, @@ -302,8 +395,8 @@ def plot_grid(base_dir, FILENAME, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -318,13 +411,14 @@ def plot_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -333,71 +427,85 @@ def plot_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAME, date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAME, + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAME, date=False, - field_mapping=field_mapping) - elif (DATAFORM == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAME, date=False, field_mapping=field_mapping + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAME, date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAME, date=False, field_mapping=field_mapping + ) # create masked array if missing values if MASK is not None: # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # setup stereographic map - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(10,7.5), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(10, 7.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # plot image of MODIS mosaic of Antarctica as base layer if BASEMAP: @@ -405,79 +513,133 @@ def plot_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin, latsin, - data=img, order=order, iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2',linestyles='solid', - transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,181 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -710,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, @@ -739,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 @@ -747,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 ce956b41..b11409b4 100644 --- a/mapping/plot_AIS_grid_movie.py +++ b/mapping/plot_AIS_grid_movie.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_grid_movie.py Written by Tyler Sutterley (05/2023) Creates GMT-like animations for the Antarctic Ice Sheet @@ -59,6 +59,7 @@ Updated 11/2015: different date label colors if plotting with MODIS Written 05/2015 """ + from __future__ import print_function import sys @@ -77,7 +78,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -86,61 +87,109 @@ import matplotlib.ticker as ticker import matplotlib.animation as animation import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # output file information suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # output units -unit_list = ['cmwe', 'mmGH', 'mmCU', u'\\u03BCGal', 'mbar'] +unit_list = ['cmwe', 'mmGH', 'mmCU', '\\u03BCGal', 'mbar'] # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # Antarctica (AIS) # x and y limit (modified from Bamber 1km DEM) -xlimits = np.array([-3100000,3100000]) -ylimits = np.array([-2600000,2600000]) +xlimits = np.array([-3100000, 3100000]) +ylimits = np.array([-2600000, 2600000]) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -160,9 +209,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -174,7 +225,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -182,30 +233,34 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -214,12 +269,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, MASKED=True): @@ -235,8 +290,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -244,40 +299,77 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # image extents - extents=(xmin,xmax,ymin,ymax) + extents = (xmin, xmax, ymin, ymax) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', extent=extents, - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + extent=extents, + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.2*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.2 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # animate grid program -def animate_grid(base_dir, FILENAME, +def animate_grid( + base_dir, + FILENAME, DATAFORM=None, MASK=None, INTERPOLATION=None, @@ -306,8 +398,8 @@ def animate_grid(base_dir, FILENAME, DRAW_SCALE=False, FIGURE_FILE=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -322,13 +414,14 @@ def animate_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -337,21 +430,21 @@ def animate_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file @@ -360,15 +453,25 @@ def animate_grid(base_dir, FILENAME, # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - dinput = gravtk.spatial().from_file(FILENAME, - format=DATAFORM, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + dinput = gravtk.spatial().from_file( + FILENAME, + format=DATAFORM, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) elif DATAFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,dataform = DATAFORM.split('-') - dinput = gravtk.spatial().from_index(FILENAME, - format=dataform, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + _, dataform = DATAFORM.split('-') + dinput = gravtk.spatial().from_index( + FILENAME, + format=dataform, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) # replace invalid with a new fill value dinput.replace_invalid(fill_value=FILL_VALUE) @@ -377,41 +480,51 @@ def animate_grid(base_dir, FILENAME, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # create movie writer objects FFMpegWriter = animation.writers['ffmpeg'] - metadata = dict(title=pathlib.Path(sys.argv[0]).name, artist='Matplotlib', - date_created=time.strftime('%Y-%m-%d',time.localtime())) + metadata = dict( + title=pathlib.Path(sys.argv[0]).name, + artist='Matplotlib', + date_created=time.strftime('%Y-%m-%d', time.localtime()), + ) # bitrate to be determined automatically by underlying utility - writer = FFMpegWriter(fps=8, metadata=metadata, bitrate=-1, - extra_args=['-vcodec','libx264']) + writer = FFMpegWriter( + fps=8, metadata=metadata, bitrate=-1, extra_args=['-vcodec', 'libx264'] + ) # setup stereographic map - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(10,7.5), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(10, 7.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # plot image of MODIS mosaic of Antarctica as base layer if BASEMAP: @@ -419,44 +532,55 @@ def animate_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # only plot grounded points if MASK is not None: - mask = gravtk.tools.mask_oceans(lonsin,latsin,order=order) + mask = gravtk.tools.mask_oceans(lonsin, latsin, order=order) # add place holder for figure image - im = ax1.imshow(np.zeros((my,mx)), interpolation='nearest', cmap=cmap, - norm=norm, extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - alpha=ALPHA, origin='lower', transform=projection, animated=True) + im = ax1.imshow( + np.zeros((my, mx)), + interpolation='nearest', + cmap=cmap, + norm=norm, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + alpha=ALPHA, + origin='lower', + transform=projection, + animated=True, + ) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2_subbasin'): + elif BASIN_TYPE == 'IMBIE-2_subbasin': plot_IMBIE2_subbasins(ax1, base_dir) start_indice = 1 else: @@ -466,11 +590,18 @@ def animate_grid(base_dir, FILENAME, if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -480,8 +611,16 @@ def animate_grid(base_dir, FILENAME, # options: neither, both, min, max # shrink = percent size of colorbar # aspect = lengthXwidth aspect of colorbar - cbar = plt.colorbar(im, ax=ax1, pad=0.025, extend=CBEXTEND, - extendfrac=0.0375, shrink=0.925, aspect=20, drawedges=False) + cbar = plt.colorbar( + im, + ax=ax1, + pad=0.025, + extend=CBEXTEND, + extendfrac=0.0375, + shrink=0.925, + aspect=20, + drawedges=False, + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -491,8 +630,9 @@ def animate_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=23, labelsize=24, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=23, labelsize=24, direction='in' + ) # x and y limits, axis = equal ax1.set_xlim(xlimits) @@ -504,38 +644,55 @@ def animate_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=24) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=24) # Add figure label if LABEL is not None: if BASEMAP: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=24,weight='bold')) - at.patch.set_boxstyle("Square,pad=0.25") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=24, weight='bold'), + ) + at.patch.set_boxstyle('Square,pad=0.25') + at.patch.set_edgecolor('white') else: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=False, - prop=dict(size=24,weight='bold')) + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=False, + prop=dict(size=24, weight='bold'), + ) ax1.axes.add_artist(at) # draw map scale to corners if DRAW_SCALE: - add_plot_scale(ax1,-295e4,-236e4,1000e3,85e3,False) + add_plot_scale(ax1, -295e4, -236e4, 1000e3, 85e3, False) # add date label (year-calendar month e.g. 2002-01) # if plotting with a background mosaic: use a white time label # else: use a black time label text_color = 'w' if BASEMAP else 'k' - time_text = ax1.text(0.025, 0.025, '', transform=ax1.transAxes, - color=text_color, size=30, ha='left', va='baseline', usetex=True) + time_text = ax1.text( + 0.025, + 0.025, + '', + transform=ax1.transAxes, + color=text_color, + size=30, + ha='left', + va='baseline', + usetex=True, + ) # stronger linewidth on frame ax1.spines['geo'].set_linewidth(2.0) ax1.spines['geo'].set_zorder(10) ax1.spines['geo'].set_capstyle('projecting') # adjust subplot within figure - fig.subplots_adjust(left=0.02,right=0.98,bottom=0.02,top=0.98) + fig.subplots_adjust(left=0.02, right=0.98, bottom=0.02, top=0.98) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) @@ -543,23 +700,45 @@ def animate_grid(base_dir, FILENAME, # create image for each frame with writer.saving(fig, FIGURE_FILE, FIGURE_DPI): # for each input file - for t,gm in enumerate(dinput.month): + for t, gm in enumerate(dinput.month): # data for time t converted to a masked array data = dinput.subset(gm).to_masked_array() # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,data.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,data.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(data.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(data.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and ( + np.max(dinput.lon) > 180 + ): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, data.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, data.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + data.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + data.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # only plot grounded points @@ -574,28 +753,61 @@ def animate_grid(base_dir, FILENAME, contours = [] if CONTOURS and (np.sum(data**2) > 0): # plot line contours - contours.append(ax1.contour(lon, lat, data, reduce_clevs, - colors='0.2', linestyles='solid', - transform=ccrs.PlateCarree())) - contours.append(ax1.contour(lon, lat, data, 0, - colors='red', linestyles='solid', linewidths=1.5, - transform=ccrs.PlateCarree())) + contours.append( + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ) + contours.append( + ax1.contour( + lon, + lat, + data, + 0, + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,138 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -745,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, @@ -772,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 @@ -780,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 4196dcd9..d44c340b 100644 --- a/mapping/plot_AIS_regional_maps.py +++ b/mapping/plot_AIS_regional_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_regional_maps.py Written by Tyler Sutterley (05/2023) Creates GMT-like plots for sub-regions of Antarctica @@ -64,6 +64,7 @@ updates to parallel new plot_AIS_grid_movie.py code Written 07/2014 """ + from __future__ import print_function import sys @@ -81,7 +82,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -89,44 +90,90 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa125_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa125_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # regional plot parameters # figure size @@ -147,56 +194,58 @@ region_sub_adjust = {} # Amundsen Sea Embayment (ASE) -region_figsize['ASE'] = (10,9.5) +region_figsize['ASE'] = (10, 9.5) region_fontsize['ASE'] = 24 region_labelsize['ASE'] = 24 region_xlimit['ASE'] = np.array([-1900000, -900000]) -region_ylimit['ASE'] = np.array([-900000, 200000]) +region_ylimit['ASE'] = np.array([-900000, 200000]) region_cb_axis['ASE'] = [0.865, 0.015, 0.035, 0.94] region_cb_length['ASE'] = 26 -region_plotscale['ASE'] = (-1870e3,-845e3,200e3,25010,False) +region_plotscale['ASE'] = (-1870e3, -845e3, 200e3, 25010, False) region_sub_adjust['ASE'] = dict(left=0.01, right=0.855, bottom=0.01, top=0.96) # Antarctic Peninsula (IIpp) -region_figsize['IIpp'] = (10,10) +region_figsize['IIpp'] = (10, 10) region_fontsize['IIpp'] = 24 region_labelsize['IIpp'] = 24 -region_xlimit['IIpp'] = np.array([-2710000,-1830000]) -region_ylimit['IIpp'] = np.array([760000,1780000]) +region_xlimit['IIpp'] = np.array([-2710000, -1830000]) +region_ylimit['IIpp'] = np.array([760000, 1780000]) region_cb_axis['IIpp'] = [0.87, 0.015, 0.0325, 0.94] region_cb_length['IIpp'] = 23 -region_plotscale['IIpp'] = (-2685e3,809e3,200e3,25010,False) +region_plotscale['IIpp'] = (-2685e3, 809e3, 200e3, 25010, False) region_sub_adjust['IIpp'] = dict(left=0.01, right=0.86, bottom=0.01, top=0.96) # Totten/Moscow/Frost (CpD) -region_figsize['CpD'] = (9.5,10) +region_figsize['CpD'] = (9.5, 10) region_fontsize['CpD'] = 24 region_labelsize['CpD'] = 24 -region_xlimit['CpD'] = np.array([1200000,2650000]) -region_ylimit['CpD'] = np.array([-1800000,0]) +region_xlimit['CpD'] = np.array([1200000, 2650000]) +region_ylimit['CpD'] = np.array([-1800000, 0]) region_cb_axis['CpD'] = [0.855, 0.015, 0.04, 0.94] region_cb_length['CpD'] = 27 -region_plotscale['CpD'] = (1230e3,-1740e3,200e3,28420,False) +region_plotscale['CpD'] = (1230e3, -1740e3, 200e3, 28420, False) region_sub_adjust['CpD'] = dict(left=0.01, right=0.84, bottom=0.01, top=0.96) # Queen Maud Land (QML) -region_figsize['QML'] = (9.5,4.625) +region_figsize['QML'] = (9.5, 4.625) region_fontsize['QML'] = 20 region_labelsize['QML'] = 24 -region_xlimit['QML'] = np.array([-940000,2400000]) -region_ylimit['QML'] = np.array([530000,2300000]) +region_xlimit['QML'] = np.array([-940000, 2400000]) +region_ylimit['QML'] = np.array([530000, 2300000]) region_cb_axis['QML'] = [0.87, 0.03, 0.03, 0.90] region_cb_length['QML'] = 21 -region_plotscale['QML'] = (1705e3,2125e3,600e3,50e3,False) +region_plotscale['QML'] = (1705e3, 2125e3, 600e3, 50e3, False) region_sub_adjust['QML'] = dict(left=0.01, right=0.85, bottom=0.01, top=0.95) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -206,6 +255,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -215,9 +265,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -229,7 +281,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -237,46 +289,51 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Amundsen Sea basins from Mouginot et al. (2014) def plot_amundsen_basins(ax, base_dir): # read Amundsen Sea basin polylines from shapefile - basin_shapefile = base_dir.joinpath('masks','Basins_Admunsen', - 'Basins_admunsen_match_coastline_and_IS.shp') - basin_title = ['pope_smith','haynes','thwaites','pine_island','kohler'] + basin_shapefile = base_dir.joinpath( + 'masks', 'Basins_Admunsen', 'Basins_admunsen_match_coastline_and_IS.shp' + ) + basin_title = ['pope_smith', 'haynes', 'thwaites', 'pine_island', 'kohler'] logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() # for each shape entity - for i,ent in enumerate(shape_entities): + for i, ent in enumerate(shape_entities): # extract lat/lon coordinates for record points = np.array(ent.points) - ax.plot(points[:,0], points[:,1], c='k', - transform=ccrs.PlateCarree()) + ax.plot(points[:, 0], points[:, 1], c='k', transform=ccrs.PlateCarree()) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -285,12 +342,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, xlimits, ylimits, MASKED=True): @@ -302,55 +359,97 @@ def plot_image_mosaic(ax, base_dir, xlimits, ylimits, MASKED=True): info_geotiff = ds.GetGeoTransform() # reduce input image with GDAL # Specify offset and rows and columns to read - xoff = int((xlimits[0] - info_geotiff[0])/info_geotiff[1]) - yoff = int((ylimits[1] - info_geotiff[3])/info_geotiff[5]) - xsize = int((xlimits[1] - xlimits[0])/info_geotiff[1]) + 1 - ysize = int((ylimits[0] - ylimits[1])/info_geotiff[5]) + 1 + xoff = int((xlimits[0] - info_geotiff[0]) / info_geotiff[1]) + yoff = int((ylimits[1] - info_geotiff[3]) / info_geotiff[5]) + xsize = int((xlimits[1] - xlimits[0]) / info_geotiff[1]) + 1 + ysize = int((ylimits[0] - ylimits[1]) / info_geotiff[5]) + 1 # read as grayscale image reducing to xlimit and ylimit - mosaic = np.ma.array(ds.ReadAsArray(xoff=xoff, yoff=yoff, - xsize=xsize, ysize=ysize)) + mosaic = np.ma.array( + ds.ReadAsArray(xoff=xoff, yoff=yoff, xsize=xsize, ysize=ysize) + ) # mask image mosaic if MASKED: # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # reduced x and y limits of image - xmin = info_geotiff[0] + xoff*info_geotiff[1] - xmax = info_geotiff[0] + xoff*info_geotiff[1] + (xsize-1)*info_geotiff[1] - ymax = info_geotiff[3] + yoff*info_geotiff[5] - ymin = info_geotiff[3] + yoff*info_geotiff[5] + (ysize-1)*info_geotiff[5] + xmin = info_geotiff[0] + xoff * info_geotiff[1] + xmax = ( + info_geotiff[0] + xoff * info_geotiff[1] + (xsize - 1) * info_geotiff[1] + ) + ymax = info_geotiff[3] + yoff * info_geotiff[5] + ymin = ( + info_geotiff[3] + yoff * info_geotiff[5] + (ysize - 1) * info_geotiff[5] + ) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.15*dx,Y-1.8*dy,Y+2.4*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.15 * dx, + Y - 1.8 * dy, + Y + 2.4 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAME, +def plot_grid( + base_dir, + FILENAME, REGION=None, DATAFORM=None, VARIABLES=[], @@ -381,8 +480,8 @@ def plot_grid(base_dir, FILENAME, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -397,13 +496,14 @@ def plot_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -412,72 +512,85 @@ def plot_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAME, date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAME, + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAME, date=False, - field_mapping=field_mapping) - elif (DATAFORM == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAME, date=False, field_mapping=field_mapping + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAME, date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAME, date=False, field_mapping=field_mapping + ) # create masked array if missing values if MASK is not None: # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # setup stereographic map - fig,ax1 = plt.subplots(num=1, nrows=1, ncols=1, + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, figsize=region_figsize[REGION], - subplot_kw=dict(projection=projection)) + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # region x and y limits xlimits = region_xlimit[REGION] ylimits = region_ylimit[REGION] @@ -488,82 +601,136 @@ def plot_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir, xlimits, ylimits) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin, latsin, - data=img, order=order, iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2',linestyles='solid', - transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,156 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -802,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, @@ -832,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 @@ -840,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 ac966ddd..babbf1a4 100644 --- a/mapping/plot_AIS_regional_movie.py +++ b/mapping/plot_AIS_regional_movie.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_ASE_grid_movie.py Written by Tyler Sutterley (05/2023) Creates GMT-like animations for sub-regions of Antarctica @@ -62,6 +62,7 @@ updates to parallel new plot_AIS_grid_movie.py code Written 11/2014 """ + from __future__ import print_function import sys @@ -80,7 +81,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -89,49 +90,95 @@ import matplotlib.ticker as ticker import matplotlib.animation as animation import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # output file information suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # output units -unit_list = ['cmwe', 'mmGH', 'mmCU', u'\\u03BCGal', 'mbar'] +unit_list = ['cmwe', 'mmGH', 'mmCU', '\\u03BCGal', 'mbar'] # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa125_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa125_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # regional plot parameters # figure size @@ -152,56 +199,58 @@ region_sub_adjust = {} # Amundsen Sea Embayment (ASE) -region_figsize['ASE'] = (10,9.5) +region_figsize['ASE'] = (10, 9.5) region_fontsize['ASE'] = 24 region_labelsize['ASE'] = 24 region_xlimit['ASE'] = np.array([-1900000, -900000]) -region_ylimit['ASE'] = np.array([-900000, 200000]) +region_ylimit['ASE'] = np.array([-900000, 200000]) region_cb_axis['ASE'] = [0.865, 0.015, 0.035, 0.94] region_cb_length['ASE'] = 26 -region_plotscale['ASE'] = (-1870e3,-845e3,200e3,25010,False) +region_plotscale['ASE'] = (-1870e3, -845e3, 200e3, 25010, False) region_sub_adjust['ASE'] = dict(left=0.01, right=0.855, bottom=0.01, top=0.96) # Antarctic Peninsula (IIpp) -region_figsize['IIpp'] = (10,10) +region_figsize['IIpp'] = (10, 10) region_fontsize['IIpp'] = 24 region_labelsize['IIpp'] = 24 -region_xlimit['IIpp'] = np.array([-2710000,-1830000]) -region_ylimit['IIpp'] = np.array([760000,1780000]) +region_xlimit['IIpp'] = np.array([-2710000, -1830000]) +region_ylimit['IIpp'] = np.array([760000, 1780000]) region_cb_axis['IIpp'] = [0.87, 0.015, 0.0325, 0.94] region_cb_length['IIpp'] = 23 -region_plotscale['IIpp'] = (-2685e3,809e3,200e3,25010,False) +region_plotscale['IIpp'] = (-2685e3, 809e3, 200e3, 25010, False) region_sub_adjust['IIpp'] = dict(left=0.01, right=0.86, bottom=0.01, top=0.96) # Totten/Moscow/Frost (CpD) -region_figsize['CpD'] = (9.5,10) +region_figsize['CpD'] = (9.5, 10) region_fontsize['CpD'] = 24 region_labelsize['CpD'] = 24 -region_xlimit['CpD'] = np.array([1200000,2650000]) -region_ylimit['CpD'] = np.array([-1800000,0]) +region_xlimit['CpD'] = np.array([1200000, 2650000]) +region_ylimit['CpD'] = np.array([-1800000, 0]) region_cb_axis['CpD'] = [0.855, 0.015, 0.04, 0.94] region_cb_length['CpD'] = 27 -region_plotscale['CpD'] = (1230e3,-1740e3,200e3,28420,False) +region_plotscale['CpD'] = (1230e3, -1740e3, 200e3, 28420, False) region_sub_adjust['CpD'] = dict(left=0.01, right=0.84, bottom=0.01, top=0.96) # Queen Maud Land (QML) -region_figsize['QML'] = (9.5,4.625) +region_figsize['QML'] = (9.5, 4.625) region_fontsize['QML'] = 20 region_labelsize['QML'] = 24 -region_xlimit['QML'] = np.array([-940000,2400000]) -region_ylimit['QML'] = np.array([530000,2300000]) +region_xlimit['QML'] = np.array([-940000, 2400000]) +region_ylimit['QML'] = np.array([530000, 2300000]) region_cb_axis['QML'] = [0.87, 0.03, 0.03, 0.90] region_cb_length['QML'] = 21 -region_plotscale['QML'] = (1705e3,2125e3,600e3,50e3,False) +region_plotscale['QML'] = (1705e3, 2125e3, 600e3, 50e3, False) region_sub_adjust['QML'] = dict(left=0.01, right=0.85, bottom=0.01, top=0.95) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -221,9 +270,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -235,7 +286,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -243,46 +294,51 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Amundsen Sea basins from Mouginot et al. (2014) def plot_amundsen_basins(ax, base_dir): # read Amundsen Sea basin polylines from shapefile - basin_shapefile = base_dir.joinpath('masks','Basins_Admunsen', - 'Basins_admunsen_match_coastline_and_IS.shp') - basin_title = ['pope_smith','haynes','thwaites','pine_island','kohler'] + basin_shapefile = base_dir.joinpath( + 'masks', 'Basins_Admunsen', 'Basins_admunsen_match_coastline_and_IS.shp' + ) + basin_title = ['pope_smith', 'haynes', 'thwaites', 'pine_island', 'kohler'] logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() # for each shape entity - for i,ent in enumerate(shape_entities): + for i, ent in enumerate(shape_entities): # extract lat/lon coordinates for record points = np.array(ent.points) - ax.plot(points[:,0], points[:,1], c='k', - transform=ccrs.PlateCarree()) + ax.plot(points[:, 0], points[:, 1], c='k', transform=ccrs.PlateCarree()) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -291,12 +347,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, xlimits, ylimits, MASKED=True): @@ -308,55 +364,97 @@ def plot_image_mosaic(ax, base_dir, xlimits, ylimits, MASKED=True): info_geotiff = ds.GetGeoTransform() # reduce input image with GDAL # Specify offset and rows and columns to read - xoff = int((xlimits[0] - info_geotiff[0])/info_geotiff[1]) - yoff = int((ylimits[1] - info_geotiff[3])/info_geotiff[5]) - xsize = int((xlimits[1] - xlimits[0])/info_geotiff[1]) + 1 - ysize = int((ylimits[0] - ylimits[1])/info_geotiff[5]) + 1 + xoff = int((xlimits[0] - info_geotiff[0]) / info_geotiff[1]) + yoff = int((ylimits[1] - info_geotiff[3]) / info_geotiff[5]) + xsize = int((xlimits[1] - xlimits[0]) / info_geotiff[1]) + 1 + ysize = int((ylimits[0] - ylimits[1]) / info_geotiff[5]) + 1 # read as grayscale image reducing to xlimit and ylimit - mosaic = np.ma.array(ds.ReadAsArray(xoff=xoff, yoff=yoff, - xsize=xsize, ysize=ysize)) + mosaic = np.ma.array( + ds.ReadAsArray(xoff=xoff, yoff=yoff, xsize=xsize, ysize=ysize) + ) # mask image mosaic if MASKED: # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # reduced x and y limits of image - xmin = info_geotiff[0] + xoff*info_geotiff[1] - xmax = info_geotiff[0] + xoff*info_geotiff[1] + (xsize-1)*info_geotiff[1] - ymax = info_geotiff[3] + yoff*info_geotiff[5] - ymin = info_geotiff[3] + yoff*info_geotiff[5] + (ysize-1)*info_geotiff[5] + xmin = info_geotiff[0] + xoff * info_geotiff[1] + xmax = ( + info_geotiff[0] + xoff * info_geotiff[1] + (xsize - 1) * info_geotiff[1] + ) + ymax = info_geotiff[3] + yoff * info_geotiff[5] + ymin = ( + info_geotiff[3] + yoff * info_geotiff[5] + (ysize - 1) * info_geotiff[5] + ) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.15*dx,Y-1.8*dy,Y+2.4*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.15 * dx, + Y - 1.8 * dy, + Y + 2.4 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # animate grid program -def animate_grid(base_dir, FILENAME, +def animate_grid( + base_dir, + FILENAME, REGION=None, DATAFORM=None, MASK=None, @@ -386,8 +484,8 @@ def animate_grid(base_dir, FILENAME, DRAW_SCALE=False, FIGURE_FILE=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -402,13 +500,14 @@ def animate_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -417,21 +516,21 @@ def animate_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file @@ -440,15 +539,25 @@ def animate_grid(base_dir, FILENAME, # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - dinput = gravtk.spatial().from_file(FILENAME, - format=DATAFORM, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + dinput = gravtk.spatial().from_file( + FILENAME, + format=DATAFORM, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) elif DATAFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,dataform = DATAFORM.split('-') - dinput = gravtk.spatial().from_index(FILENAME, - format=dataform, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + _, dataform = DATAFORM.split('-') + dinput = gravtk.spatial().from_index( + FILENAME, + format=dataform, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) # replace invalid with a new fill value dinput.replace_invalid(fill_value=FILL_VALUE) @@ -457,42 +566,51 @@ def animate_grid(base_dir, FILENAME, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # create movie writer objects FFMpegWriter = animation.writers['ffmpeg'] - metadata = dict(title=pathlib.Path(sys.argv[0]).name, artist='Matplotlib', - date_created=time.strftime('%Y-%m-%d',time.localtime())) + metadata = dict( + title=pathlib.Path(sys.argv[0]).name, + artist='Matplotlib', + date_created=time.strftime('%Y-%m-%d', time.localtime()), + ) # bitrate to be determined automatically by underlying utility - writer = FFMpegWriter(fps=8, metadata=metadata, bitrate=-1, - extra_args=['-vcodec','libx264']) + writer = FFMpegWriter( + fps=8, metadata=metadata, bitrate=-1, extra_args=['-vcodec', 'libx264'] + ) # setup stereographic map - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, figsize=region_figsize[REGION], - subplot_kw=dict(projection=projection)) + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # region x and y limits xlimits = region_xlimit[REGION] ylimits = region_ylimit[REGION] @@ -503,47 +621,58 @@ def animate_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # only plot grounded points if MASK is not None: - mask = gravtk.tools.mask_oceans(lonsin,latsin,order=order) + mask = gravtk.tools.mask_oceans(lonsin, latsin, order=order) # add place holder for figure image - im = ax1.imshow(np.zeros((my,mx)), interpolation='nearest', cmap=cmap, - norm=norm, extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - alpha=ALPHA, origin='lower', transform=projection, animated=True) + im = ax1.imshow( + np.zeros((my, mx)), + interpolation='nearest', + cmap=cmap, + norm=norm, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + alpha=ALPHA, + origin='lower', + transform=projection, + animated=True, + ) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2_subbasin'): + elif BASIN_TYPE == 'IMBIE-2_subbasin': plot_IMBIE2_subbasins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'Amundsen'): + elif BASIN_TYPE == 'Amundsen': plot_amundsen_basins(ax1, base_dir) start_indice = 0 else: @@ -553,11 +682,18 @@ def animate_grid(base_dir, FILENAME, if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -566,8 +702,9 @@ def animate_grid(base_dir, FILENAME, cbar_ax = fig.add_axes(region_cb_axis[REGION]) # extend = add extension triangles to upper and lower bounds # options: neither, both, min, max - cbar = fig.colorbar(im, cax=cbar_ax, extend=CBEXTEND, - extendfrac=0.0375, drawedges=False) + cbar = fig.colorbar( + im, cax=cbar_ax, extend=CBEXTEND, extendfrac=0.0375, drawedges=False + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -577,9 +714,13 @@ def animate_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, - length=region_cb_length[REGION], labelsize=region_fontsize[REGION], - direction='in') + cbar.ax.tick_params( + which='both', + width=1, + length=region_cb_length[REGION], + labelsize=region_fontsize[REGION], + direction='in', + ) # x and y limits, axis = equal ax1.set_xlim(xlimits) @@ -591,20 +732,29 @@ def animate_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), - fontsize=region_fontsize[REGION]) + ax1.set_title( + TITLE.replace('-', '\u2013'), fontsize=region_fontsize[REGION] + ) # Add figure label if LABEL is not None: if BASEMAP: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=region_labelsize[REGION], weight='bold')) - at.patch.set_boxstyle("Square,pad=0.25") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=region_labelsize[REGION], weight='bold'), + ) + at.patch.set_boxstyle('Square,pad=0.25') + at.patch.set_edgecolor('white') else: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=False, - prop=dict(size=region_labelsize[REGION], weight='bold')) + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=False, + prop=dict(size=region_labelsize[REGION], weight='bold'), + ) ax1.axes.add_artist(at) # draw map scale to corners @@ -615,9 +765,17 @@ def animate_grid(base_dir, FILENAME, # if plotting with a background mosaic: use a white time label # else: use a black time label text_color = 'w' if BASEMAP else 'k' - time_text = ax1.text(0.025, 0.025, '', transform=ax1.transAxes, - color=text_color, size=region_labelsize[REGION], - ha='left', va='baseline', usetex=True) + time_text = ax1.text( + 0.025, + 0.025, + '', + transform=ax1.transAxes, + color=text_color, + size=region_labelsize[REGION], + ha='left', + va='baseline', + usetex=True, + ) # stronger linewidth on frame ax1.spines['geo'].set_linewidth(2.0) @@ -632,23 +790,45 @@ def animate_grid(base_dir, FILENAME, # create image for each frame with writer.saving(fig, FIGURE_FILE, FIGURE_DPI): # for each input file - for t,gm in enumerate(dinput.month): + for t, gm in enumerate(dinput.month): # data for time t converted to a masked array data = dinput.subset(gm).to_masked_array() # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,data.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,data.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(data.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(data.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and ( + np.max(dinput.lon) > 180 + ): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, data.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, data.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + data.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + data.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # only plot grounded points @@ -663,28 +843,61 @@ def animate_grid(base_dir, FILENAME, contours = [] if CONTOURS and (np.sum(data**2) > 0): # plot line contours - contours.append(ax1.contour(lon, lat, data, reduce_clevs, - colors='0.2', linestyles='solid', - transform=ccrs.PlateCarree())) - contours.append(ax1.contour(lon, lat, data, 0, - colors='red', linestyles='solid', linewidths=1.5, - transform=ccrs.PlateCarree())) + contours.append( + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ) + contours.append( + ax1.contour( + lon, + lat, + data, + 0, + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,142 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -838,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, @@ -866,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 @@ -874,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 5e80b57f..1c44d2f8 100644 --- a/mapping/plot_GrIS_grid_3maps.py +++ b/mapping/plot_GrIS_grid_3maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_GrIS_grid_3maps.py Written by Tyler Sutterley (05/2023) Creates 3 GMT-like plots for the Greenland ice sheet @@ -49,6 +49,7 @@ Updated 09/2019: added parameter for specifying if netCDF4 or HDF5 Written 09/2019 """ + from __future__ import print_function import sys @@ -66,7 +67,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -74,52 +75,61 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Greenland ice divides # region directory, filename, title and data type -region_dir = ['masks','Rignot_GRE'] -region_title = ['CE','CW','NE','NO','NW','SE','SW'] +region_dir = ['masks', 'Rignot_GRE'] +region_title = ['CE', 'CW', 'NE', 'NO', 'NW', 'SE', 'SW'] # regional filenames region_filename = 'divide_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lon','lat'),'formats':('f','f')} +region_dtype = {'names': ('lon', 'lat'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','GRE_Basins_IMBIE2_v1.3','GRE_Basins_IMBIE2_v1.3.shp'] +IMBIE_basin_file = [ + 'masks', + 'GRE_Basins_IMBIE2_v1.3', + 'GRE_Basins_IMBIE2_v1.3.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('CW','NE','NO','NW','SE','SW') +IMBIE_title = ('CW', 'NE', 'NO', 'NW', 'SE', 'SW') # background image mosaics # MODIS mosaic of Greenland -image_file = ['MOG','mog500_2005_hp1_v1.1.tif'] +image_file = ['MOG', 'mog500_2005_hp1_v1.1.tif'] # Greenland grounded ice -coast_file = ['masks','GIMP','grn_ice_sheet_peripheral_glaciers.shp'] +coast_file = ['masks', 'GIMP', 'grn_ice_sheet_peripheral_glaciers.shp'] # Greenland bounds xlimits = np.array([-1530000, 1610000]) ylimits = np.array([-3600000, -280000]) # cartopy transform for NSIDC polar stereographic north try: - projection = ccrs.Stereographic(central_longitude=-45.0, - central_latitude=+90.0,true_scale_latitude=+70.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=-45.0, + central_latitude=+90.0, + true_scale_latitude=+70.0, + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -129,6 +139,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -138,9 +149,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Greenland drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -152,7 +165,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no GIC or islands - i = [i for i,a in enumerate(shape_attributes) if a[0] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[0] in IMBIE_title] # for each valid shape entity for indice in i: # extract lat/lon coordinates for record @@ -160,10 +173,15 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): + for p1, p2 in zip(parts[:-1], parts[1:]): # converting basin lat/lon into plot coordinates - ax.plot(points[p1:p2,0], points[p1:p2,1], color='k', - transform=ccrs.PlateCarree()) + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + color='k', + transform=ccrs.PlateCarree(), + ) + # PURPOSE: plot Greenland grounded ice delineation from GIMP def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): @@ -172,12 +190,18 @@ def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], c='k', lw=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + c='k', + lw=LINEWIDTH, + transform=projection, + ) + # PURPOSE: plot glaciated regions from Randolph Glacier Inventory def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): @@ -187,36 +211,43 @@ def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): RGI_files.append('06_rgi60_Iceland') RGI_files.append('07_rgi60_Svalbard') for f in RGI_files: - RGI_shapefile = base_dir.joinpath('RGI',f,f'{f}_plot.shp') + RGI_shapefile = base_dir.joinpath('RGI', f, f'{f}_plot.shp') logging.debug(str(RGI_shapefile)) shape_input = shapefile.Reader(str(RGI_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], color='k', linewidth=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + color='k', + linewidth=LINEWIDTH, + transform=projection, + ) + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, color='k', transform=ccrs.PlateCarree()) + # plot the MODIS Mosaic of Greenland as a background image def plot_image_mosaic(ax, base_dir, MASKED=True): # read MODIS mosaic of Greenland @@ -231,8 +262,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -240,38 +271,75 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # dataset range vmin, vmax = (0, 18770) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.15*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.15 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -303,12 +371,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -324,13 +391,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -339,21 +407,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -361,55 +429,68 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup polar stereographic maps - fig, (ax[0],ax[1],ax[2]) = plt.subplots(num=1, ncols=3, figsize=(10.25,3.5), - subplot_kw=dict(projection=projection)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, + ncols=3, + figsize=(10.25, 3.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # plot image of MODIS mosaic of Greenland as base layer if BASEMAP: # plot MODIS mosaic of Greenland plot_image_mosaic(ax1, base_dir) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -420,92 +501,136 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,165 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -740,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, @@ -770,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 @@ -778,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 9d804ab4..4b2a7cf3 100644 --- a/mapping/plot_GrIS_grid_5maps.py +++ b/mapping/plot_GrIS_grid_5maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_GrIS_grid_5maps.py Written by Tyler Sutterley (10/2023) Creates 5 GMT-like plots for the Greenland ice sheet @@ -36,6 +36,7 @@ UPDATE HISTORY: Written 10/2023 """ + from __future__ import print_function import sys @@ -53,7 +54,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -61,52 +62,61 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Greenland ice divides # region directory, filename, title and data type -region_dir = ['masks','Rignot_GRE'] -region_title = ['CE','CW','NE','NO','NW','SE','SW'] +region_dir = ['masks', 'Rignot_GRE'] +region_title = ['CE', 'CW', 'NE', 'NO', 'NW', 'SE', 'SW'] # regional filenames region_filename = 'divide_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lon','lat'),'formats':('f','f')} +region_dtype = {'names': ('lon', 'lat'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','GRE_Basins_IMBIE2_v1.3','GRE_Basins_IMBIE2_v1.3.shp'] +IMBIE_basin_file = [ + 'masks', + 'GRE_Basins_IMBIE2_v1.3', + 'GRE_Basins_IMBIE2_v1.3.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('CW','NE','NO','NW','SE','SW') +IMBIE_title = ('CW', 'NE', 'NO', 'NW', 'SE', 'SW') # background image mosaics # MODIS mosaic of Greenland -image_file = ['MOG','mog500_2005_hp1_v1.1.tif'] +image_file = ['MOG', 'mog500_2005_hp1_v1.1.tif'] # Greenland grounded ice -coast_file = ['masks','GIMP','grn_ice_sheet_peripheral_glaciers.shp'] +coast_file = ['masks', 'GIMP', 'grn_ice_sheet_peripheral_glaciers.shp'] # Greenland bounds (Bamber extended for GIMP) -xlimits = (-160.*5e3, 172.*5e3) -ylimits = (-680.*5e3,-131.*5e3) +xlimits = (-160.0 * 5e3, 172.0 * 5e3) +ylimits = (-680.0 * 5e3, -131.0 * 5e3) # cartopy transform for NSIDC polar stereographic north try: - projection = ccrs.Stereographic(central_longitude=-45.0, - central_latitude=+90.0,true_scale_latitude=+70.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=-45.0, + central_latitude=+90.0, + true_scale_latitude=+70.0, + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -116,6 +126,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -125,9 +136,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Greenland drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -139,7 +152,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no GIC or islands - i = [i for i,a in enumerate(shape_attributes) if a[0] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[0] in IMBIE_title] # for each valid shape entity for indice in i: # extract lat/lon coordinates for record @@ -147,10 +160,15 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): + for p1, p2 in zip(parts[:-1], parts[1:]): # converting basin lat/lon into plot coordinates - ax.plot(points[p1:p2,0], points[p1:p2,1], color='k', - transform=ccrs.PlateCarree()) + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + color='k', + transform=ccrs.PlateCarree(), + ) + # PURPOSE: plot Greenland grounded ice delineation from GIMP def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): @@ -159,12 +177,18 @@ def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], c='k', lw=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + c='k', + lw=LINEWIDTH, + transform=projection, + ) + # PURPOSE: plot glaciated regions from Randolph Glacier Inventory def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): @@ -174,36 +198,43 @@ def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): RGI_files.append('06_rgi60_Iceland') RGI_files.append('07_rgi60_Svalbard') for f in RGI_files: - RGI_shapefile = base_dir.joinpath('RGI',f,f'{f}_plot.shp') + RGI_shapefile = base_dir.joinpath('RGI', f, f'{f}_plot.shp') logging.debug(str(RGI_shapefile)) shape_input = shapefile.Reader(str(RGI_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], color='k', linewidth=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + color='k', + linewidth=LINEWIDTH, + transform=projection, + ) + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, color='k', transform=ccrs.PlateCarree()) + # plot the MODIS Mosaic of Greenland as a background image def plot_image_mosaic(ax, base_dir, MASKED=True): # read MODIS mosaic of Greenland @@ -218,8 +249,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -227,35 +258,63 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # dataset range vmin, vmax = (0, 18770) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.05*dx,X+1.05*dx,Y-0.5*dy,Y+4.5*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=1) - for i,c in enumerate([fc1,fc2,fc1,fc2,fc1]): - x1,x2,y1,y2 = [X+0.2*i*dx,X+0.2*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=4) - ax.plot([X,X,X+dx,X+dx], [Y+1.5*dy,Y,Y,Y+1.5*dy], fc2, zorder=4) - ax.text(X+0.5*dx, Y+1.3*dy, f'{dx/1e3:0.0f} km', - ha='center', va='bottom', fontsize=10, color=fc2) + x1, x2, y1, y2 = [ + X - 0.05 * dx, + X + 1.05 * dx, + Y - 0.5 * dy, + Y + 4.5 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=1) + for i, c in enumerate([fc1, fc2, fc1, fc2, fc1]): + x1, x2, y1, y2 = [X + 0.2 * i * dx, X + 0.2 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=4) + ax.plot( + [X, X, X + dx, X + dx], + [Y + 1.5 * dy, Y, Y, Y + 1.5 * dy], + fc2, + zorder=4, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + f'{dx / 1e3:0.0f} km', + ha='center', + va='bottom', + fontsize=10, + color=fc2, + ) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -287,11 +346,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -307,13 +366,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -322,21 +382,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -344,56 +404,68 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup polar stereographic maps - fig, (ax[0],ax[1],ax[2],ax[3],ax[4]) = plt.subplots( - num=1, ncols=5, figsize=(11.25,3.5), - subplot_kw=dict(projection=projection)) + fig, (ax[0], ax[1], ax[2], ax[3], ax[4]) = plt.subplots( + num=1, + ncols=5, + figsize=(11.25, 3.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # plot image of MODIS mosaic of Greenland as base layer if BASEMAP: # plot MODIS mosaic of Greenland plot_image_mosaic(ax1, base_dir) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -404,92 +476,136 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,165 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -724,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, @@ -754,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 @@ -762,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 d42373bc..729ff5a2 100644 --- a/mapping/plot_GrIS_grid_maps.py +++ b/mapping/plot_GrIS_grid_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_GrIS_grid_maps.py Written by Tyler Sutterley (05/2023) Creates GMT-like plots for the Greenland ice sheet @@ -62,6 +62,7 @@ Updated 06/2015: no bounding box Written 05/2015 """ + from __future__ import print_function import sys @@ -79,7 +80,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -87,52 +88,61 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Greenland ice divides # region directory, filename, title and data type -region_dir = ['masks','Rignot_GRE'] -region_title = ['CE','CW','NE','NO','NW','SE','SW'] +region_dir = ['masks', 'Rignot_GRE'] +region_title = ['CE', 'CW', 'NE', 'NO', 'NW', 'SE', 'SW'] # regional filenames region_filename = 'divide_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lon','lat'),'formats':('f','f')} +region_dtype = {'names': ('lon', 'lat'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','GRE_Basins_IMBIE2_v1.3','GRE_Basins_IMBIE2_v1.3.shp'] +IMBIE_basin_file = [ + 'masks', + 'GRE_Basins_IMBIE2_v1.3', + 'GRE_Basins_IMBIE2_v1.3.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('CW','NE','NO','NW','SE','SW') +IMBIE_title = ('CW', 'NE', 'NO', 'NW', 'SE', 'SW') # background image mosaics # MODIS mosaic of Greenland -image_file = ['MOG','mog500_2005_hp1_v1.1.tif'] +image_file = ['MOG', 'mog500_2005_hp1_v1.1.tif'] # Greenland grounded ice -coast_file = ['masks','GIMP','grn_ice_sheet_peripheral_glaciers.shp'] +coast_file = ['masks', 'GIMP', 'grn_ice_sheet_peripheral_glaciers.shp'] # Greenland bounds xlimits = np.array([-1530000, 1610000]) ylimits = np.array([-3600000, -280000]) # cartopy transform for NSIDC polar stereographic north try: - projection = ccrs.Stereographic(central_longitude=-45.0, - central_latitude=+90.0,true_scale_latitude=+70.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=-45.0, + central_latitude=+90.0, + true_scale_latitude=+70.0, + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -142,6 +152,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -151,9 +162,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Greenland drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -165,7 +178,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no GIC or islands - i = [i for i,a in enumerate(shape_attributes) if a[0] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[0] in IMBIE_title] # for each valid shape entity for indice in i: # extract lat/lon coordinates for record @@ -173,10 +186,15 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): + for p1, p2 in zip(parts[:-1], parts[1:]): # converting basin lat/lon into plot coordinates - ax.plot(points[p1:p2,0], points[p1:p2,1], color='k', - transform=ccrs.PlateCarree()) + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + color='k', + transform=ccrs.PlateCarree(), + ) + # PURPOSE: plot Greenland grounded ice delineation from GIMP def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): @@ -185,12 +203,18 @@ def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], c='k', lw=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + c='k', + lw=LINEWIDTH, + transform=projection, + ) + # PURPOSE: plot glaciated regions from Randolph Glacier Inventory def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): @@ -200,36 +224,43 @@ def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): RGI_files.append('06_rgi60_Iceland') RGI_files.append('07_rgi60_Svalbard') for f in RGI_files: - RGI_shapefile = base_dir.joinpath('RGI',f,f'{f}_plot.shp') + RGI_shapefile = base_dir.joinpath('RGI', f, f'{f}_plot.shp') logging.debug(str(RGI_shapefile)) shape_input = shapefile.Reader(str(RGI_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], color='k', linewidth=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + color='k', + linewidth=LINEWIDTH, + transform=projection, + ) + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, color='k', transform=ccrs.PlateCarree()) + # plot the MODIS Mosaic of Greenland as a background image def plot_image_mosaic(ax, base_dir, MASKED=True): # read MODIS mosaic of Greenland @@ -244,8 +275,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -253,38 +284,75 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # dataset range vmin, vmax = (0, 18770) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.15*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.15 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAME, +def plot_grid( + base_dir, + FILENAME, DATAFORM=None, VARIABLES=[], MASK=None, @@ -315,8 +383,8 @@ def plot_grid(base_dir, FILENAME, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -331,13 +399,14 @@ def plot_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -346,71 +415,85 @@ def plot_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAME, date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAME, + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAME, date=False, - field_mapping=field_mapping) - elif (DATAFORM == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAME, date=False, field_mapping=field_mapping + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAME, date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAME, date=False, field_mapping=field_mapping + ) # create masked array if missing values if MASK is not None: # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # setup stereographic map - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(9.875,9), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(9.875, 9), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # plot image of MODIS mosaic of Greenland as base layer if BASEMAP: @@ -418,78 +501,132 @@ def plot_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin, latsin, - data=img, order=order, iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2',linestyles='solid', - transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,184 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -727,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, @@ -757,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 @@ -765,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 b2ebc63e..3f13ee58 100644 --- a/mapping/plot_GrIS_grid_movie.py +++ b/mapping/plot_GrIS_grid_movie.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_GrIS_grid_movie.py Written by Tyler Sutterley (05/2023) Creates GMT-like animations for the Greenland Ice Sheet @@ -66,6 +66,7 @@ Updated 10/2015: updated for GIMP background mosaic Written 05/2015 """ + from __future__ import print_function import sys @@ -84,7 +85,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -93,59 +94,68 @@ import matplotlib.ticker as ticker import matplotlib.animation as animation import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # output file information suffix = ['txt', 'nc', 'H5'] # output units -unit_list = ['cmwe', 'mmGH', 'mmCU', u'\\u03BCGal', 'mbar'] +unit_list = ['cmwe', 'mmGH', 'mmCU', '\\u03BCGal', 'mbar'] # Greenland ice divides # region directory, filename, title and data type -region_dir = ['masks','Rignot_GRE'] -region_title = ['CE','CW','NE','NO','NW','SE','SW'] +region_dir = ['masks', 'Rignot_GRE'] +region_title = ['CE', 'CW', 'NE', 'NO', 'NW', 'SE', 'SW'] # regional filenames region_filename = 'divide_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lon','lat'),'formats':('f','f')} +region_dtype = {'names': ('lon', 'lat'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins IMBIE_basin_file = {} -IMBIE_basin_file=['masks','GRE_Basins_IMBIE2_v1.3','GRE_Basins_IMBIE2_v1.3.shp'] +IMBIE_basin_file = [ + 'masks', + 'GRE_Basins_IMBIE2_v1.3', + 'GRE_Basins_IMBIE2_v1.3.shp', +] # basin titles within shapefile to extract IMBIE_title = {} -IMBIE_title=('CW','NE','NO','NW','SE','SW') +IMBIE_title = ('CW', 'NE', 'NO', 'NW', 'SE', 'SW') # background image mosaics # MODIS mosaic of Greenland -image_file = ['MOG','mog500_2005_hp1_v1.1.tif'] +image_file = ['MOG', 'mog500_2005_hp1_v1.1.tif'] # Greenland grounded ice -coast_file=['masks','GIMP','grn_ice_sheet_peripheral_glaciers.shp'] +coast_file = ['masks', 'GIMP', 'grn_ice_sheet_peripheral_glaciers.shp'] # Greenland bounds xlimits = np.array([-1530000, 1610000]) ylimits = np.array([-3600000, -280000]) # cartopy transform for NSIDC polar stereographic north try: - projection = ccrs.Stereographic(central_longitude=-45.0, - central_latitude=+90.0,true_scale_latitude=+70.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=-45.0, + central_latitude=+90.0, + true_scale_latitude=+70.0, + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -155,6 +165,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -164,9 +175,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Greenland drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -178,7 +191,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no GIC or islands - i = [i for i,a in enumerate(shape_attributes) if a[0] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[0] in IMBIE_title] # for each valid shape entity for indice in i: # extract lat/lon coordinates for record @@ -186,10 +199,15 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): + for p1, p2 in zip(parts[:-1], parts[1:]): # converting basin lat/lon into plot coordinates - ax.plot(points[p1:p2,0], points[p1:p2,1], color='k', - transform=ccrs.PlateCarree()) + ax.plot( + points[p1:p2, 0], + points[p1:p2, 1], + color='k', + transform=ccrs.PlateCarree(), + ) + # PURPOSE: plot Greenland grounded ice delineation from GIMP def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): @@ -198,12 +216,18 @@ def plot_grounded_ice(ax, base_dir, START=1, END=300, LINEWIDTH=0.6): shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], c='k', lw=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + c='k', + lw=LINEWIDTH, + transform=projection, + ) + # PURPOSE: plot glaciated regions from Randolph Glacier Inventory def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): @@ -213,36 +237,43 @@ def plot_glacier_inventory(ax, base_dir, START=0, END=30, LINEWIDTH=0.6): RGI_files.append('06_rgi60_Iceland') RGI_files.append('07_rgi60_Svalbard') for f in RGI_files: - RGI_shapefile = base_dir.joinpath('RGI',f,f'{f}_plot.shp') + RGI_shapefile = base_dir.joinpath('RGI', f, f'{f}_plot.shp') logging.debug(str(RGI_shapefile)) shape_input = shapefile.Reader(str(RGI_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - for i in range(START,END): + for i in range(START, END): # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # converting Polar-Stereographic coordinates into plot coordinates - ax.plot(points[:,0], points[:,1], color='k', linewidth=LINEWIDTH, - transform=projection) + ax.plot( + points[:, 0], + points[:, 1], + color='k', + linewidth=LINEWIDTH, + transform=projection, + ) + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, color='k', transform=ccrs.PlateCarree()) + # plot the MODIS Mosaic of Greenland as a background image def plot_image_mosaic(ax, base_dir, MASKED=True): # read MODIS mosaic of Greenland @@ -257,8 +288,8 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # calculate image extents xmin = info_geotiff[0] ymax = info_geotiff[3] - xmax = xmin + (xsize-1)*info_geotiff[1] - ymin = ymax + (ysize-1)*info_geotiff[5] + xmax = xmin + (xsize - 1) * info_geotiff[1] + ymin = ymax + (ysize - 1) * info_geotiff[5] # read as grayscale image mosaic = np.ma.array(ds.ReadAsArray()) # mask image mosaic @@ -266,38 +297,75 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # dataset range vmin, vmax = (0, 18770) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.1*dx,X+1.15*dx,Y-2.5*dy,Y+3.2*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.1 * dx, + X + 1.15 * dx, + Y - 2.5 * dy, + Y + 3.2 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # animate grid program -def animate_grid(base_dir, FILENAME, +def animate_grid( + base_dir, + FILENAME, DATAFORM=None, MASK=None, INTERPOLATION=None, @@ -327,8 +395,8 @@ def animate_grid(base_dir, FILENAME, DRAW_SCALE=False, FIGURE_FILE=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -343,13 +411,14 @@ def animate_grid(base_dir, FILENAME, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -358,21 +427,21 @@ def animate_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file @@ -381,15 +450,25 @@ def animate_grid(base_dir, FILENAME, # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - dinput = gravtk.spatial().from_file(FILENAME, - format=DATAFORM, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + dinput = gravtk.spatial().from_file( + FILENAME, + format=DATAFORM, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) elif DATAFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,dataform = DATAFORM.split('-') - dinput = gravtk.spatial().from_index(FILENAME, - format=dataform, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + _, dataform = DATAFORM.split('-') + dinput = gravtk.spatial().from_index( + FILENAME, + format=dataform, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) # replace invalid with a new fill value dinput.replace_invalid(fill_value=FILL_VALUE) @@ -398,41 +477,51 @@ def animate_grid(base_dir, FILENAME, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # create movie writer objects FFMpegWriter = animation.writers['ffmpeg'] - metadata = dict(title=pathlib.Path(sys.argv[0]).name, artist='Matplotlib', - date_created=time.strftime('%Y-%m-%d',time.localtime())) + metadata = dict( + title=pathlib.Path(sys.argv[0]).name, + artist='Matplotlib', + date_created=time.strftime('%Y-%m-%d', time.localtime()), + ) # bitrate to be determined automatically by underlying utility - writer = FFMpegWriter(fps=8, metadata=metadata, bitrate=-1, - extra_args=['-vcodec','libx264']) + writer = FFMpegWriter( + fps=8, metadata=metadata, bitrate=-1, extra_args=['-vcodec', 'libx264'] + ) # setup stereographic map - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(9.875,9), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(9.875, 9), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # plot image of MODIS mosaic of Greenland as base layer if BASEMAP: @@ -440,43 +529,54 @@ def animate_grid(base_dir, FILENAME, plot_image_mosaic(ax1, base_dir) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # only plot grounded points if MASK is not None: - mask = gravtk.tools.mask_oceans(lonsin,latsin,order=order) + mask = gravtk.tools.mask_oceans(lonsin, latsin, order=order) # add place holder for figure image - im = ax1.imshow(np.zeros((my,mx)), interpolation='nearest', cmap=cmap, - norm=norm, extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - alpha=ALPHA, origin='lower', transform=projection, animated=True) + im = ax1.imshow( + np.zeros((my, mx)), + interpolation='nearest', + cmap=cmap, + norm=norm, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + alpha=ALPHA, + origin='lower', + transform=projection, + animated=True, + ) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # plot the coastline and grounded ice files plot_coastline(ax1, base_dir) # add basins based on BASIN_TYPE (Rignot 2012, IMBIE-2, IMBIE-2 subbasins) - if (BASIN_TYPE == 'Rignot'): + if BASIN_TYPE == 'Rignot': plot_rignot_basins(ax1, base_dir) start_indice = 1 - elif (BASIN_TYPE == 'IMBIE-2'): + elif BASIN_TYPE == 'IMBIE-2': plot_IMBIE2_basins(ax1, base_dir) start_indice = 1 else: @@ -488,11 +588,18 @@ def animate_grid(base_dir, FILENAME, if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=ccrs.PlateCarree(), + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -502,8 +609,16 @@ def animate_grid(base_dir, FILENAME, # options: neither, both, min, max # shrink = percent size of colorbar # aspect = lengthXwidth aspect of colorbar - cbar = plt.colorbar(im, ax=ax1, pad=0.025, extend=CBEXTEND, - extendfrac=0.0375, shrink=0.98, aspect=22.5, drawedges=False) + cbar = plt.colorbar( + im, + ax=ax1, + pad=0.025, + extend=CBEXTEND, + extendfrac=0.0375, + shrink=0.98, + aspect=22.5, + drawedges=False, + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -513,8 +628,9 @@ def animate_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=26, labelsize=24, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=26, labelsize=24, direction='in' + ) # x and y limits, axis = equal ax1.set_xlim(xlimits) @@ -526,38 +642,55 @@ def animate_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=24) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=24) # Add figure label if LABEL is not None: if BASEMAP: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=24,weight='bold')) - at.patch.set_boxstyle("Square,pad=0.25") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=24, weight='bold'), + ) + at.patch.set_boxstyle('Square,pad=0.25') + at.patch.set_edgecolor('white') else: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=False, - prop=dict(size=24,weight='bold')) + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=False, + prop=dict(size=24, weight='bold'), + ) ax1.axes.add_artist(at) # draw map scale to corners if DRAW_SCALE: - add_plot_scale(ax1,1110e3,-3460e3,400e3,55e3,False) + add_plot_scale(ax1, 1110e3, -3460e3, 400e3, 55e3, False) # add date label (year-calendar month e.g. 2002-01) # if plotting with a background mosaic: use a white time label # else: use a black time label text_color = 'w' if BASEMAP else 'k' - time_text = ax1.text(0.775, 0.025, '', transform=ax1.transAxes, - color=text_color, size=30, ha='left', va='baseline', usetex=True) + time_text = ax1.text( + 0.775, + 0.025, + '', + transform=ax1.transAxes, + color=text_color, + size=30, + ha='left', + va='baseline', + usetex=True, + ) # stronger linewidth on frame ax1.spines['geo'].set_linewidth(2.0) ax1.spines['geo'].set_zorder(10) ax1.spines['geo'].set_capstyle('projecting') # adjust subplot within figure - fig.subplots_adjust(left=0.02,right=0.99,bottom=0.02,top=0.96) + fig.subplots_adjust(left=0.02, right=0.99, bottom=0.02, top=0.96) # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) @@ -565,23 +698,45 @@ def animate_grid(base_dir, FILENAME, # create image for each frame with writer.saving(fig, FIGURE_FILE, FIGURE_DPI): # for each input file - for t,gm in enumerate(dinput.month): + for t, gm in enumerate(dinput.month): # data for time t converted to a masked array data = dinput.subset(gm).to_masked_array() # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,data.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,data.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(data.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(data.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and ( + np.max(dinput.lon) > 180 + ): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, data.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, data.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + data.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + data.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # only plot grounded points @@ -596,28 +751,61 @@ def animate_grid(base_dir, FILENAME, contours = [] if CONTOURS and (np.sum(data**2) > 0): # plot line contours - contours.append(ax1.contour(lon, lat, data, reduce_clevs, - colors='0.2', linestyles='solid', - transform=ccrs.PlateCarree())) - contours.append(ax1.contour(lon, lat, data, 0, - colors='red', linestyles='solid', linewidths=1.5, - transform=ccrs.PlateCarree())) + contours.append( + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ) + contours.append( + ax1.contour( + lon, + lat, + data, + 0, + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,141 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -770,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, @@ -798,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 @@ -806,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 ae50db05..25eaad3d 100644 --- a/mapping/plot_QML_grid_3maps.py +++ b/mapping/plot_QML_grid_3maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_AIS_grid_3maps.py Written by Tyler Sutterley (05/2023) Creates 3 GMT-like plots for Queen Maud Land (QML) in Antarctica @@ -51,6 +51,7 @@ Updated 12/2018: added parameter CBEXTEND for colorbar extension triangles Written 10/2018 """ + from __future__ import print_function import sys @@ -68,7 +69,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -76,55 +77,103 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import osgeo.gdal except ModuleNotFoundError: - warnings.warn("GDAL not available", ImportWarning) + warnings.warn('GDAL not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # Antarctic 2012 basins # region directory, filename, title and data type -region_dir = ['masks','Rignot_ANT'] -region_title = ['AAp','ApB','BC','CCp','CpD','DDp','DpE','EEp','EpFp', - 'FpG','GH','HHp','HpI','IIpp','IppJ','JJpp','JppK','KKp','KpA'] +region_dir = ['masks', 'Rignot_ANT'] +region_title = [ + 'AAp', + 'ApB', + 'BC', + 'CCp', + 'CpD', + 'DDp', + 'DpE', + 'EEp', + 'EpFp', + 'FpG', + 'GH', + 'HHp', + 'HpI', + 'IIpp', + 'IppJ', + 'JJpp', + 'JppK', + 'KKp', + 'KpA', +] # regional filenames region_filename = 'basin_{0}_index.ascii' # regional datatypes -region_dtype = {'names':('lat','lon'),'formats':('f','f')} +region_dtype = {'names': ('lat', 'lon'), 'formats': ('f', 'f')} # IMBIE-2 Drainage basins -IMBIE_basin_file = ['masks','ANT_Basins_IMBIE2_v1.6','ANT_Basins_IMBIE2_v1.6.shp'] +IMBIE_basin_file = [ + 'masks', + 'ANT_Basins_IMBIE2_v1.6', + 'ANT_Basins_IMBIE2_v1.6.shp', +] # basin titles within shapefile to extract -IMBIE_title = ('A-Ap','Ap-B','B-C','C-Cp','Cp-D','D-Dp','Dp-E','E-Ep','Ep-F', - 'F-Fp','F-G','G-H','H-Hp','Hp-I','I-Ipp','Ipp-J','J-Jpp','Jpp-K','K-A') +IMBIE_title = ( + 'A-Ap', + 'Ap-B', + 'B-C', + 'C-Cp', + 'Cp-D', + 'D-Dp', + 'Dp-E', + 'E-Ep', + 'Ep-F', + 'F-Fp', + 'F-G', + 'G-H', + 'H-Hp', + 'Hp-I', + 'I-Ipp', + 'Ipp-J', + 'J-Jpp', + 'Jpp-K', + 'K-A', +) # background image mosaics # MODIS mosaic of Antarctica -image_file = ['MOA','moa750_2004_hp1_v1.1.tif'] +image_file = ['MOA', 'moa750_2004_hp1_v1.1.tif'] # Coastlines for antarctica (islands) -coast_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] +coast_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', +] # Queen Maud Land bounds xlimits = np.array([-940000, 2400000]) ylimits = np.array([530000, 2300000]) # cartopy transform for polar stereographic south try: - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) -except (NameError,ValueError) as exc: + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -134,6 +183,7 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: plot Rignot 2012 drainage basin polylines def plot_rignot_basins(ax, base_dir): region_directory = base_dir.joinpath(*region_dir) @@ -143,9 +193,11 @@ def plot_rignot_basins(ax, base_dir): region_file = region_directory.joinpath(region_filename.format(reg)) region_ll = np.loadtxt(region_file, dtype=region_dtype) # converting region lat/lon into plot coordinates - points = projection.transform_points(ccrs.PlateCarree(), - region_ll['lon'], region_ll['lat']) - ax.plot(points[:,0], points[:,1], color='k', transform=projection) + points = projection.transform_points( + ccrs.PlateCarree(), region_ll['lon'], region_ll['lat'] + ) + ax.plot(points[:, 0], points[:, 1], color='k', transform=projection) + # PURPOSE: plot Antarctic drainage basins from IMBIE2 (Mouginot) def plot_IMBIE2_basins(ax, base_dir): @@ -157,7 +209,7 @@ def plot_IMBIE2_basins(ax, base_dir): shape_attributes = shape_input.records() # find record index for region by iterating through shape attributes # no islands or large regions - i=[i for i,a in enumerate(shape_attributes) if a[1] in IMBIE_title] + i = [i for i, a in enumerate(shape_attributes) if a[1] in IMBIE_title] # for each valid shape entity for indice in i: # extract Polar-Stereographic coordinates for record @@ -165,30 +217,34 @@ def plot_IMBIE2_basins(ax, base_dir): # IMBIE-2 basins can have multiple parts parts = shape_entities[indice].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic drainage sub-basins from IMBIE-2 (Mouginot) def plot_IMBIE2_subbasins(ax, base_dir): # read drainage basin polylines from shapefile (using splat operator) - IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7','Basins_v1.7.shp'] - basin_shapefile = base_dir.joinpath('masks',*IMBIE_subbasin_file) + IMBIE_subbasin_file = ['Basins_20Oct2016_v1.7', 'Basins_v1.7.shp'] + basin_shapefile = base_dir.joinpath('masks', *IMBIE_subbasin_file) logging.debug(str(basin_shapefile)) shape_input = shapefile.Reader(str(basin_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() # iterate through shape entities and attributes - indices = [i for i,a in enumerate(shape_attributes) if (a[1] != 'Islands')] + indices = [i for i, a in enumerate(shape_attributes) if (a[1] != 'Islands')] for i in indices: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[i].points) # IMBIE-2 basins can have multiple parts parts = shape_entities[i].parts parts.append(len(points)) - for p1,p2 in zip(parts[:-1],parts[1:]): - ax.plot(points[p1:p2,0], points[p1:p2,1], c='k', - transform=projection) + for p1, p2 in zip(parts[:-1], parts[1:]): + ax.plot( + points[p1:p2, 0], points[p1:p2, 1], c='k', transform=projection + ) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, START=1): @@ -197,12 +253,12 @@ def plot_grounded_ice(ax, base_dir, START=1): shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] for indice in i[START:]: # extract Polar-Stereographic coordinates for record points = np.array(shape_entities[indice].points) - ax.plot(points[:,0], points[:,1], c='k', - transform=projection) + ax.plot(points[:, 0], points[:, 1], c='k', transform=projection) + # PURPOSE: plot MODIS mosaic of Antarctica as background image def plot_image_mosaic(ax, base_dir, MASKED=True): @@ -214,55 +270,97 @@ def plot_image_mosaic(ax, base_dir, MASKED=True): info_geotiff = ds.GetGeoTransform() # reduce input image with GDAL # Specify offset and rows and columns to read - xoff = int((xlimits[0] - info_geotiff[0])/info_geotiff[1]) - yoff = int((ylimits[1] - info_geotiff[3])/info_geotiff[5]) - xsize = int((xlimits[1] - xlimits[0])/info_geotiff[1]) + 1 - ysize = int((ylimits[0] - ylimits[1])/info_geotiff[5]) + 1 + xoff = int((xlimits[0] - info_geotiff[0]) / info_geotiff[1]) + yoff = int((ylimits[1] - info_geotiff[3]) / info_geotiff[5]) + xsize = int((xlimits[1] - xlimits[0]) / info_geotiff[1]) + 1 + ysize = int((ylimits[0] - ylimits[1]) / info_geotiff[5]) + 1 # read as grayscale image reducing to xlimit and ylimit - mosaic = np.ma.array(ds.ReadAsArray(xoff=xoff, yoff=yoff, - xsize=xsize, ysize=ysize)) + mosaic = np.ma.array( + ds.ReadAsArray(xoff=xoff, yoff=yoff, xsize=xsize, ysize=ysize) + ) # mask image mosaic if MASKED: # mask invalid values mosaic.fill_value = 0 # create mask array for bad values - mosaic.mask = (mosaic.data == mosaic.fill_value) + mosaic.mask = mosaic.data == mosaic.fill_value # reduced x and y limits of image - xmin = info_geotiff[0] + xoff*info_geotiff[1] - xmax = info_geotiff[0] + xoff*info_geotiff[1] + (xsize-1)*info_geotiff[1] - ymax = info_geotiff[3] + yoff*info_geotiff[5] - ymin = info_geotiff[3] + yoff*info_geotiff[5] + (ysize-1)*info_geotiff[5] + xmin = info_geotiff[0] + xoff * info_geotiff[1] + xmax = ( + info_geotiff[0] + xoff * info_geotiff[1] + (xsize - 1) * info_geotiff[1] + ) + ymax = info_geotiff[3] + yoff * info_geotiff[5] + ymin = ( + info_geotiff[3] + yoff * info_geotiff[5] + (ysize - 1) * info_geotiff[5] + ) # dataset range vmin, vmax = (0, 16386) # create color map with transparent bad points image_cmap = copy.copy(cm.gist_gray) image_cmap.set_bad(alpha=0.0) # nearest to not interpolate image - im = ax.imshow(mosaic, interpolation='nearest', - cmap=image_cmap, vmin=vmin, vmax=vmax, origin='upper', - extent=(xmin, xmax, ymin, ymax), transform=projection) + im = ax.imshow( + mosaic, + interpolation='nearest', + cmap=image_cmap, + vmin=vmin, + vmax=vmax, + origin='upper', + extent=(xmin, xmax, ymin, ymax), + transform=projection, + ) im.set_rasterized(True) # close the dataset ds = None + # PURPOSE: add a plot scale -def add_plot_scale(ax,X,Y,dx,dy,masked,fc1='w',fc2='k'): +def add_plot_scale(ax, X, Y, dx, dy, masked, fc1='w', fc2='k'): if masked: - x1,x2,y1,y2 = [X-0.3*dx,X+1.2*dx,Y-3.5*dy,Y+3.4*dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], fc1, zorder=4) - for i,c in enumerate([fc1,fc2,fc1,fc2]): - x1,x2,y1,y2 = [X+0.25*i*dx,X+0.25*(i+1)*dx,Y,Y+dy] - ax.fill([x1,x2,x2,x1,x1], [y1,y1,y2,y2,y1], c, zorder=5) - ax.plot([X,X+dx,X+dx,X,X], [Y,Y,Y+dy,Y+dy,Y], fc2, zorder=6) + x1, x2, y1, y2 = [ + X - 0.3 * dx, + X + 1.2 * dx, + Y - 3.5 * dy, + Y + 3.4 * dy, + ] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], fc1, zorder=4) + for i, c in enumerate([fc1, fc2, fc1, fc2]): + x1, x2, y1, y2 = [X + 0.25 * i * dx, X + 0.25 * (i + 1) * dx, Y, Y + dy] + ax.fill([x1, x2, x2, x1, x1], [y1, y1, y2, y2, y1], c, zorder=5) + ax.plot([X, X + dx, X + dx, X, X], [Y, Y, Y + dy, Y + dy, Y], fc2, zorder=6) for i in range(3): - ax.plot([X+0.5*i*dx,X+0.5*i*dx], [Y,Y-0.5*dy], fc2, zorder=6) - ax.text(X+0.5*i*dx, Y-0.9*dy, '{0:0.0f}'.format(0.5*i*dx/1e3), - ha='center', va='top', fontsize=12, color=fc2, zorder=6) - ax.text(X+0.5*dx, Y+1.3*dy, 'km', ha='center', va='bottom', - fontsize=12, color=fc2, zorder=6) + ax.plot( + [X + 0.5 * i * dx, X + 0.5 * i * dx], + [Y, Y - 0.5 * dy], + fc2, + zorder=6, + ) + ax.text( + X + 0.5 * i * dx, + Y - 0.9 * dy, + '{0:0.0f}'.format(0.5 * i * dx / 1e3), + ha='center', + va='top', + fontsize=12, + color=fc2, + zorder=6, + ) + ax.text( + X + 0.5 * dx, + Y + 1.3 * dy, + 'km', + ha='center', + va='bottom', + fontsize=12, + color=fc2, + zorder=6, + ) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -293,11 +391,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -313,13 +411,14 @@ def plot_grid(base_dir, FILENAMES, cmap.set_bad(alpha=0.0) else: # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -328,21 +427,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -350,55 +449,68 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup polar stereographic maps - fig, (ax[0],ax[1],ax[2]) = plt.subplots(num=1, nrows=3, figsize=(6.5,9.0), - subplot_kw=dict(projection=projection)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, + nrows=3, + figsize=(6.5, 9.0), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # plot image of MODIS mosaic of Antarctica as base layer if BASEMAP: # plot MODIS mosaic of Antarctica plot_image_mosaic(ax1, base_dir) # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -409,93 +521,137 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - mx = np.int64((xlimits[1]-xlimits[0])/1000.)+1 - my = np.int64((ylimits[1]-ylimits[0])/1000.)+1 - X = np.linspace(xlimits[0],xlimits[1],mx) - Y = np.linspace(ylimits[0],ylimits[1],my) - gridx,gridy = np.meshgrid(X,Y) + mx = np.int64((xlimits[1] - xlimits[0]) / 1000.0) + 1 + my = np.int64((ylimits[1] - ylimits[0]) / 1000.0) + 1 + X = np.linspace(xlimits[0], xlimits[1], mx) + Y = np.linspace(ylimits[0], ylimits[1], my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = ccrs.PlateCarree().transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = ccrs.PlateCarree().transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', cmap=cmap, - extent=(xlimits[0],xlimits[1],ylimits[0],ylimits[1]), - norm=norm, alpha=ALPHA, origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + cmap=cmap, + extent=(xlimits[0], xlimits[1], ylimits[0], ylimits[1]), + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) data = dinput.to_masked_array() # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(lon,lat,data,reduce_clevs,colors='0.2', - linestyles='solid',transform=ccrs.PlateCarree()) - ax1.contour(lon,lat,data,[0],colors='red',linestyles='solid', - linewidths=1.5,transform=ccrs.PlateCarree()) + ax1.contour( + lon, + lat, + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + transform=ccrs.PlateCarree(), + ) + ax1.contour( + lon, + lat, + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + transform=ccrs.PlateCarree(), + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(data.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,162 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -725,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, @@ -754,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 @@ -762,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 d8736213..53110858 100644 --- a/mapping/plot_global_grid_3maps.py +++ b/mapping/plot_global_grid_3maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_3maps.py Written by Tyler Sutterley (05/2023) Creates 3 GMT-like plots in a Plate Carree (Equirectangular) projection @@ -46,6 +46,7 @@ Updated 11/2017: can plot a contour of the global average with MEAN Written 08/2017 """ + from __future__ import print_function import sys @@ -65,7 +66,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -73,23 +74,25 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -99,44 +102,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -164,11 +176,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -178,13 +190,14 @@ def plot_grid(base_dir, FILENAMES, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -193,21 +206,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -215,50 +228,63 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup Plate Carree projection - fig, (ax[0],ax[1],ax[2]) = plt.subplots(num=1, nrows=3, figsize=(6.75,9.0), - subplot_kw=dict(projection=projection)) + fig, (ax[0], ax[1], ax[2]) = plt.subplots( + num=1, + nrows=3, + figsize=(6.75, 9.0), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -269,92 +295,132 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom(np.invert(img.mask), 5, order=1, output=bool) data.mask = np.invert(mask) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(data,reduce_clevs,colors='0.2',linestyles='solid', - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) - ax1.contour(data,[0],colors='red',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(dinput.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,153 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -568,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, @@ -594,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 @@ -602,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 96fcf4c4..5fb99dd6 100644 --- a/mapping/plot_global_grid_4maps.py +++ b/mapping/plot_global_grid_4maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_4maps.py Written by Tyler Sutterley (05/2023) Creates 4 GMT-like plots in a Plate Carree (Equirectangular) projection @@ -46,6 +46,7 @@ Updated 11/2017: can plot a contour of the global average with MEAN Written 08/2017 """ + from __future__ import print_function import sys @@ -65,7 +66,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -73,23 +74,25 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -99,44 +102,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -164,11 +176,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -178,13 +190,14 @@ def plot_grid(base_dir, FILENAMES, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -193,21 +206,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -215,50 +228,64 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup Plate Carree projection - fig, ((ax[0],ax[1]),(ax[2],ax[3])) = plt.subplots(num=1, nrows=2, ncols=2, - figsize=(10.375,7.0), subplot_kw=dict(projection=projection)) + fig, ((ax[0], ax[1]), (ax[2], ax[3])) = plt.subplots( + num=1, + nrows=2, + ncols=2, + figsize=(10.375, 7.0), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -269,92 +296,132 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom(np.invert(img.mask), 5, order=1, output=bool) data.mask = np.invert(mask) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(data,reduce_clevs,colors='0.2',linestyles='solid', - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) - ax1.contour(data,[0],colors='red',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(dinput.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,154 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -570,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, @@ -596,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 @@ -604,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 5deef916..9e770b64 100644 --- a/mapping/plot_global_grid_5maps.py +++ b/mapping/plot_global_grid_5maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_5maps.py Written by Tyler Sutterley (05/2023) Creates 5 GMT-like plots in a Plate Carree (Equirectangular) projection @@ -46,6 +46,7 @@ Updated 11/2017: can plot a contour of the global average with MEAN Written 08/2017 """ + from __future__ import print_function import sys @@ -65,7 +66,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -74,23 +75,25 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -100,44 +103,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -165,11 +177,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -179,13 +191,14 @@ def plot_grid(base_dir, FILENAMES, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -194,21 +207,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -216,55 +229,64 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup Plate Carree projection - fig = plt.figure(figsize=(10.375,12.5)) - gs = gridspec.GridSpec(3, 2, height_ratios=[2,1,1]) - ax[0] = plt.subplot(gs[0,:], projection=projection) - ax[1] = plt.subplot(gs[1,0], projection=projection) - ax[2] = plt.subplot(gs[1,1], projection=projection) - ax[3] = plt.subplot(gs[2,0], projection=projection) - ax[4] = plt.subplot(gs[2,1], projection=projection) + fig = plt.figure(figsize=(10.375, 12.5)) + gs = gridspec.GridSpec(3, 2, height_ratios=[2, 1, 1]) + ax[0] = plt.subplot(gs[0, :], projection=projection) + ax[1] = plt.subplot(gs[1, 0], projection=projection) + ax[2] = plt.subplot(gs[1, 1], projection=projection) + ax[3] = plt.subplot(gs[2, 0], projection=projection) + ax[4] = plt.subplot(gs[2, 1], projection=projection) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -275,92 +297,132 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom(np.invert(img.mask), 5, order=1, output=bool) data.mask = np.invert(mask) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(data,reduce_clevs,colors='0.2',linestyles='solid', - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) - ax1.contour(data,[0],colors='red',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(dinput.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,154 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -576,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, @@ -602,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 @@ -610,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 4406f4ce..771ffcec 100644 --- a/mapping/plot_global_grid_9maps.py +++ b/mapping/plot_global_grid_9maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_9maps.py Written by Tyler Sutterley (05/2023) Creates 9 GMT-like plots in a Plate Carree (Equirectangular) projection @@ -46,6 +46,7 @@ Updated 11/2017: can plot a contour of the global average with MEAN Written 08/2017 """ + from __future__ import print_function import sys @@ -65,7 +66,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -73,23 +74,25 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -99,44 +102,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # plot grid program -def plot_grid(base_dir, FILENAMES, +def plot_grid( + base_dir, + FILENAMES, DATAFORM=None, VARIABLES=[], MASK=None, @@ -164,11 +176,11 @@ def plot_grid(base_dir, FILENAMES, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # extend list if a single format was entered for all files if len(DATAFORM) < len(FILENAMES): - DATAFORM = DATAFORM*len(FILENAMES) + DATAFORM = DATAFORM * len(FILENAMES) # read CPT or use color map if CPT_FILE is not None: @@ -178,13 +190,14 @@ def plot_grid(base_dir, FILENAMES, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -193,21 +206,21 @@ def plot_grid(base_dir, FILENAMES, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # create masked array if missing values @@ -215,51 +228,67 @@ def plot_grid(base_dir, FILENAMES, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # remove a spatial field from each input map if REMOVE_FILE is not None: - REMOVE = gravtk.spatial().from_netCDF4(REMOVE_FILE, - date=False).data[:,:] + REMOVE = ( + gravtk.spatial().from_netCDF4(REMOVE_FILE, date=False).data[:, :] + ) else: REMOVE = 0.0 # image extents ax = {} # setup Plate Carree projection - fig, ((ax[0],ax[1],ax[2]),(ax[3],ax[4],ax[5]),(ax[6],ax[7],ax[8])) = \ - plt.subplots(num=1, nrows=3, ncols=3, figsize=(10.375,7.125), - subplot_kw=dict(projection=projection)) + ( + fig, + ((ax[0], ax[1], ax[2]), (ax[3], ax[4], ax[5]), (ax[6], ax[7], ax[8])), + ) = plt.subplots( + num=1, + nrows=3, + ncols=3, + figsize=(10.375, 7.125), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) - - for i,ax1 in ax.items(): + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) + for i, ax1 in ax.items(): # input ascii/netCDF4/HDF5 file - if (DATAFORM[i] == 'ascii'): + if DATAFORM[i] == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAMES[i], date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM[i] == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAMES[i], + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM[i] == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAMES[i], date=False, - field_mapping=field_mapping) - elif (DATAFORM[i] == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAMES[i], date=False, field_mapping=field_mapping + ) + elif DATAFORM[i] == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAMES[i], date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAMES[i], date=False, field_mapping=field_mapping + ) # remove offset and scale to units if (REMOVE != 0.0) or (SCALE_FACTOR != 1.0): @@ -270,92 +299,132 @@ def plot_grid(base_dir, FILENAMES, dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, - gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180, - dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180, - dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon, - dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data, - lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask, - lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data, - dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask, - dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin,latsin, - data=img,order=order,iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom(np.invert(img.mask), 5, order=1, output=bool) data.mask = np.invert(mask) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(data,reduce_clevs,colors='0.2',linestyles='solid', - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) - ax1.contour(data,[0],colors='red',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(dinput.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,154 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -571,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, @@ -597,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 @@ -605,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 761abd59..8fe8f966 100644 --- a/mapping/plot_global_grid_maps.py +++ b/mapping/plot_global_grid_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_maps.py Written by Tyler Sutterley (05/2023) Creates GMT-like plots in a Plate Carree (Equirectangular) projection @@ -47,6 +47,7 @@ Updated 02/2017: direction="in" for matplotlib2.0 color bar ticks Written 03/2016 """ + from __future__ import print_function import sys @@ -66,7 +67,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -74,23 +75,25 @@ import matplotlib.colors as colors import matplotlib.ticker as ticker import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -100,44 +103,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # plot grid program -def plot_grid(base_dir, FILENAME, +def plot_grid( + base_dir, + FILENAME, DATAFORM=None, VARIABLES=[], MASK=None, @@ -166,8 +178,8 @@ def plot_grid(base_dir, FILENAME, FIGURE_FILE=None, FIGURE_FORMAT=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -176,13 +188,14 @@ def plot_grid(base_dir, FILENAME, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -191,144 +204,209 @@ def plot_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - dinput = gravtk.spatial().from_ascii(FILENAME, date=False, - columns=VARIABLES, spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): + dinput = gravtk.spatial().from_ascii( + FILENAME, + date=False, + columns=VARIABLES, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) + elif DATAFORM == 'netCDF4': # netCDF4 (.nc) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_netCDF4(FILENAME, date=False, - field_mapping=field_mapping) - elif (DATAFORM == 'HDF5'): + dinput = gravtk.spatial().from_netCDF4( + FILENAME, date=False, field_mapping=field_mapping + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) field_mapping = gravtk.spatial().default_field_mapping(VARIABLES) - dinput = gravtk.spatial().from_HDF5(FILENAME, date=False, - field_mapping=field_mapping) + dinput = gravtk.spatial().from_HDF5( + FILENAME, date=False, field_mapping=field_mapping + ) # create masked array if missing values if MASK is not None: # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # setup Plate Carree projection - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(5.5,3.5), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(5.5, 3.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,dinput.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(dinput.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(dinput.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and (np.max(dinput.lon) > 180): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, dinput.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + dinput.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + dinput.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # plot only grounded points if MASK is not None: - img = gravtk.tools.mask_oceans(lonsin, latsin, - data=img, order=order, iceshelves=False) + img = gravtk.tools.mask_oceans( + lonsin, latsin, data=img, order=order, iceshelves=False + ) # plot image with transparency using normalization - im = ax1.imshow(img, interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + img, + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom(np.invert(img.mask), 5, order=1, output=bool) data.mask = np.invert(mask) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # plot contours - ax1.contour(data,reduce_clevs,colors='0.2',linestyles='solid', - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) - ax1.contour(data,[0],colors='red',linestyles='solid',linewidths=1.5, - extent=(xmin,xmax,ymin,ymax),origin='lower', - transform=projection) + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(dinput.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,154 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -560,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, @@ -588,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 @@ -596,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 eff08e2e..0b212bd0 100644 --- a/mapping/plot_global_grid_movie.py +++ b/mapping/plot_global_grid_movie.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" plot_global_grid_maps.py Written by Tyler Sutterley (05/2023) Creates GMT-like animations in a Plate Carree (Equirectangular) projection @@ -47,6 +47,7 @@ Updated 02/2017: direction="in" for matplotlib2.0 color bar ticks Written 12/2015 """ + from __future__ import print_function import sys @@ -67,7 +68,7 @@ try: import cartopy.crs as ccrs except ModuleNotFoundError: - warnings.warn("cartopy not available", ImportWarning) + warnings.warn('cartopy not available', ImportWarning) try: import matplotlib import matplotlib.pyplot as plt @@ -76,23 +77,25 @@ import matplotlib.ticker as ticker import matplotlib.animation as animation import matplotlib.offsetbox as offsetbox + matplotlib.rcParams['axes.linewidth'] = 2.0 matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = ['Helvetica'] matplotlib.rcParams['mathtext.default'] = 'regular' except ModuleNotFoundError: - warnings.warn("matplotlib not available", ImportWarning) + warnings.warn('matplotlib not available', ImportWarning) try: import shapefile except ModuleNotFoundError: - warnings.warn("shapefile not available", ImportWarning) + warnings.warn('shapefile not available', ImportWarning) # cartopy transform for Equirectangular Projection try: projection = ccrs.PlateCarree() -except (NameError,ValueError) as exc: +except (NameError, ValueError) as exc: pass + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -102,44 +105,53 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE plot coastlines and islands (GSHHS with G250 Greenland) def plot_coastline(ax, base_dir, LINEWIDTH=0.5): # read the coastline shape file - coastline_dir = base_dir.joinpath('masks','G250') + coastline_dir = base_dir.joinpath('masks', 'G250') coastline_shape_files = [] coastline_shape_files.append('GSHHS_i_L1_no_greenland.shp') coastline_shape_files.append('greenland_coastline_islands.shp') - for fi,S in zip(coastline_shape_files,[1000,200]): + for fi, S in zip(coastline_shape_files, [1000, 200]): coast_shapefile = coastline_dir.joinpath(fi) logging.debug(str(coast_shapefile)) shape_input = shapefile.Reader(str(coast_shapefile)) shape_entities = shape_input.shapes() # for each entity within the shapefile - for c,ent in enumerate(shape_entities[:S]): + for c, ent in enumerate(shape_entities[:S]): # extract coordinates and plot - lon,lat = np.transpose(ent.points) + lon, lat = np.transpose(ent.points) ax.plot(lon, lat, c='k', lw=LINEWIDTH, transform=projection) + # PURPOSE: plot Antarctic grounded ice delineation def plot_grounded_ice(ax, base_dir, LINEWIDTH=0.5): - grounded_ice_file = ['masks','IceBoundaries_Antarctica_v02', - 'ant_ice_sheet_islands_v2.shp'] + grounded_ice_file = [ + 'masks', + 'IceBoundaries_Antarctica_v02', + 'ant_ice_sheet_islands_v2.shp', + ] grounded_ice_shapefile = base_dir.joinpath(*grounded_ice_file) logging.debug(str(grounded_ice_shapefile)) shape_input = shapefile.Reader(str(grounded_ice_shapefile)) shape_entities = shape_input.shapes() shape_attributes = shape_input.records() - i = [i for i,e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] + i = [i for i, e in enumerate(shape_entities) if (np.ndim(e.points) > 1)] # cartopy transform for NSIDC polar stereographic south - projection = ccrs.Stereographic(central_longitude=0.0, - central_latitude=-90.0,true_scale_latitude=-71.0) + projection = ccrs.Stereographic( + central_longitude=0.0, central_latitude=-90.0, true_scale_latitude=-71.0 + ) for indice in i: # extract Polar-Stereographic coordinates for record pts = np.array(shape_entities[indice].points) - ax.plot(pts[:,0], pts[:,1], c='k', lw=LINEWIDTH, transform=projection) + ax.plot(pts[:, 0], pts[:, 1], c='k', lw=LINEWIDTH, transform=projection) + # animate grid program -def animate_grid(base_dir, FILENAME, +def animate_grid( + base_dir, + FILENAME, DATAFORM=None, MASK=None, INTERPOLATION=None, @@ -165,8 +177,8 @@ def animate_grid(base_dir, FILENAME, GRID=None, FIGURE_FILE=None, FIGURE_DPI=None, - MODE=0o775): - + MODE=0o775, +): # read CPT or use color map if CPT_FILE is not None: # cpt file @@ -175,13 +187,14 @@ def animate_grid(base_dir, FILENAME, # colormap cmap = copy.copy(cm.get_cmap(COLOR_MAP)) # grey color map for bad values - cmap.set_bad('lightgray',1.0) + cmap.set_bad('lightgray', 1.0) # set transparency ALPHA if BOUNDARY is None: # contours - levels = np.arange(PLOT_RANGE[0], PLOT_RANGE[1]+PLOT_RANGE[2], - PLOT_RANGE[2]) + levels = np.arange( + PLOT_RANGE[0], PLOT_RANGE[1] + PLOT_RANGE[2], PLOT_RANGE[2] + ) norm = colors.Normalize(vmin=PLOT_RANGE[0], vmax=PLOT_RANGE[1]) else: # boundary between contours @@ -190,21 +203,21 @@ def animate_grid(base_dir, FILENAME, # convert degree spacing and interval parameters # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # interpolation method for image background using transform_scalar - if (INTERPOLATION == 'nearest'): + if INTERPOLATION == 'nearest': order = 0 - elif (INTERPOLATION == 'bilinear'): + elif INTERPOLATION == 'bilinear': order = 1 - elif (INTERPOLATION == 'cubic'): + elif INTERPOLATION == 'cubic': order = 3 # input ascii/netCDF4/HDF5 file @@ -213,15 +226,25 @@ def animate_grid(base_dir, FILENAME, # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - dinput = gravtk.spatial().from_file(FILENAME, - format=DATAFORM, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + dinput = gravtk.spatial().from_file( + FILENAME, + format=DATAFORM, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) elif DATAFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,dataform = DATAFORM.split('-') - dinput = gravtk.spatial().from_index(FILENAME, - format=dataform, date=True, spacing=[dlon, dlat], - nlat=nlat, nlon=nlon) + _, dataform = DATAFORM.split('-') + dinput = gravtk.spatial().from_index( + FILENAME, + format=dataform, + date=True, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + ) # replace invalid with a new fill value dinput.replace_invalid(fill_value=FILL_VALUE) @@ -230,73 +253,93 @@ def animate_grid(base_dir, FILENAME, # Read Land-Sea Mask of specified input file # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(MASK, - date=False, varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + MASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape - mask = np.zeros((nth,nphi),dtype=bool) + nth, nphi = landsea.shape + mask = np.zeros((nth, nphi), dtype=bool) # combine land and island levels for land function - indx,indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) - mask[indx,indy] = True + indx, indy = np.nonzero((landsea.data >= 1) & (landsea.data <= 3)) + mask[indx, indy] = True # update mask dinput.replace_invalid(fill_value=dinput.fill_value, mask=mask) # scale input dataset - if (SCALE_FACTOR != 1.0): + if SCALE_FACTOR != 1.0: dinput = dinput.scale(SCALE_FACTOR) # if dlat is negative - if (np.sign(dlat) == -1): + if np.sign(dlat) == -1: dinput = dinput.flip(axis=0) # create movie writer objects FFMpegWriter = animation.writers['ffmpeg'] - metadata = dict(title=pathlib.Path(sys.argv[0]).name, artist='Matplotlib', - date_created=time.strftime('%Y-%m-%d',time.localtime())) + metadata = dict( + title=pathlib.Path(sys.argv[0]).name, + artist='Matplotlib', + date_created=time.strftime('%Y-%m-%d', time.localtime()), + ) # bitrate to be determined automatically by underlying utility - writer = FFMpegWriter(fps=8, metadata=metadata, bitrate=-1, - extra_args=['-vcodec','libx264']) + writer = FFMpegWriter( + fps=8, metadata=metadata, bitrate=-1, extra_args=['-vcodec', 'libx264'] + ) # setup Plate Carree projection - fig, ax1 = plt.subplots(num=1, nrows=1, ncols=1, figsize=(5.5,3.5), - subplot_kw=dict(projection=projection)) + fig, ax1 = plt.subplots( + num=1, + nrows=1, + ncols=1, + figsize=(5.5, 3.5), + subplot_kw=dict(projection=projection), + ) # WGS84 Ellipsoid parameters - a_axis = 6378137.0# [m] semimajor axis of the ellipsoid - flat = 1.0/298.257223563# flattening of the ellipsoid + a_axis = 6378137.0 # [m] semimajor axis of the ellipsoid + flat = 1.0 / 298.257223563 # flattening of the ellipsoid # (4pi/3)R^3 = (4pi/3)(a^2)b = (4pi/3)(a^3)(1 -f) - rad_e = a_axis*(1.0 - flat)**(1.0/3.0) + rad_e = a_axis * (1.0 - flat) ** (1.0 / 3.0) # calculate image coordinates - xmin,xmax,ymin,ymax = ax1.get_extent() - mx = np.int64((xmax-xmin)/0.5)+1 - my = np.int64((ymax-ymin)/0.5)+1 - X = np.linspace(xmin,xmax,mx) - Y = np.linspace(ymin,ymax,my) - gridx,gridy = np.meshgrid(X,Y) + xmin, xmax, ymin, ymax = ax1.get_extent() + mx = np.int64((xmax - xmin) / 0.5) + 1 + my = np.int64((ymax - ymin) / 0.5) + 1 + X = np.linspace(xmin, xmax, mx) + Y = np.linspace(ymin, ymax, my) + gridx, gridy = np.meshgrid(X, Y) # create mesh lon/lat - points = projection.transform_points(projection, gridx.flatten(), gridy.flatten()) - lonsin = points[:,0].reshape(my,mx) - latsin = points[:,1].reshape(my,mx) + points = projection.transform_points( + projection, gridx.flatten(), gridy.flatten() + ) + lonsin = points[:, 0].reshape(my, mx) + latsin = points[:, 1].reshape(my, mx) # only plot grounded points if MASK is not None: - mask = gravtk.tools.mask_oceans(lonsin,latsin,order=order) + mask = gravtk.tools.mask_oceans(lonsin, latsin, order=order) # add place holder for figure image - im = ax1.imshow(np.zeros((my,mx)), interpolation='nearest', - extent=(xmin,xmax,ymin,ymax), - cmap=cmap, norm=norm, alpha=ALPHA, - origin='lower', transform=projection) + im = ax1.imshow( + np.zeros((my, mx)), + interpolation='nearest', + extent=(xmin, xmax, ymin, ymax), + cmap=cmap, + norm=norm, + alpha=ALPHA, + origin='lower', + transform=projection, + ) # plot line contours if CONTOURS: - clevs = np.arange(CONTOUR_RANGE[0], + clevs = np.arange( + CONTOUR_RANGE[0], CONTOUR_RANGE[1] + CONTOUR_RANGE[2], - CONTOUR_RANGE[2]) + CONTOUR_RANGE[2], + ) # remove 0 (will plot in red) reduce_clevs = clevs[np.nonzero(clevs)] # create mesh lon/lat - lon, lat = np.meshgrid(dinput.lon,dinput.lat) + lon, lat = np.meshgrid(dinput.lon, dinput.lat) # draw coastlines plot_coastline(ax1, base_dir) @@ -306,11 +349,18 @@ def animate_grid(base_dir, FILENAME, # draw lat/lon grid lines if DRAW_GRID_LINES: # meridian and parallel grid spacing - llx,lly = (GRID[0],GRID[0]) if (len(GRID) == 1) else (GRID[0],GRID[1]) + llx, lly = ( + (GRID[0], GRID[0]) if (len(GRID) == 1) else (GRID[0], GRID[1]) + ) grid_meridians = np.arange(-180, 180 + llx, llx) grid_parallels = np.arange(-90, 90 + lly, lly) - gl = ax1.gridlines(crs=projection, draw_labels=False, - linewidth=0.1, color='0.25', linestyle='-') + gl = ax1.gridlines( + crs=projection, + draw_labels=False, + linewidth=0.1, + color='0.25', + linestyle='-', + ) gl.xlocator = ticker.FixedLocator(grid_meridians) gl.ylocator = ticker.FixedLocator(grid_parallels) @@ -320,9 +370,17 @@ def animate_grid(base_dir, FILENAME, # options: neither, both, min, max # shrink = percent size of colorbar # aspect = lengthXwidth aspect of colorbar - cbar = plt.colorbar(im, ax=ax1, extend=CBEXTEND, - extendfrac=0.0375, orientation='horizontal', pad=0.025, - shrink=0.90, aspect=22, drawedges=False) + cbar = plt.colorbar( + im, + ax=ax1, + extend=CBEXTEND, + extendfrac=0.0375, + orientation='horizontal', + pad=0.025, + shrink=0.90, + aspect=22, + drawedges=False, + ) # rasterized colorbar to remove lines cbar.solids.set_rasterized(True) # Add label to the colorbar @@ -333,8 +391,9 @@ def animate_grid(base_dir, FILENAME, cbar.set_ticks(levels) cbar.set_ticklabels([CBFORMAT.format(ct) for ct in levels]) # ticks lines all the way across - cbar.ax.tick_params(which='both', width=1, length=15, labelsize=13, - direction='in') + cbar.ax.tick_params( + which='both', width=1, length=15, labelsize=13, direction='in' + ) # axis = equal ax1.set_aspect('equal', adjustable='box') @@ -344,20 +403,33 @@ def animate_grid(base_dir, FILENAME, # add main title if TITLE is not None: - ax1.set_title(TITLE.replace('-',u'\u2013'), fontsize=13) + ax1.set_title(TITLE.replace('-', '\u2013'), fontsize=13) ax1.title.set_y(1.01) # Add figure label if LABEL is not None: - at = offsetbox.AnchoredText(LABEL, - loc=2, pad=0, frameon=True, - prop=dict(size=13,weight='bold',color='k')) - at.patch.set_boxstyle("Square,pad=0.2") - at.patch.set_edgecolor("white") + at = offsetbox.AnchoredText( + LABEL, + loc=2, + pad=0, + frameon=True, + prop=dict(size=13, weight='bold', color='k'), + ) + at.patch.set_boxstyle('Square,pad=0.2') + at.patch.set_edgecolor('white') ax1.axes.add_artist(at) # add date label (year-calendar month e.g. 2002-01) - time_text = ax1.text(0.02, 0.015, '', transform=fig.transFigure, - color='k', size=18, ha='left', va='baseline', usetex=True) + time_text = ax1.text( + 0.02, + 0.015, + '', + transform=fig.transFigure, + color='k', + size=18, + ha='left', + va='baseline', + usetex=True, + ) # stronger linewidth on frame ax1.spines['geo'].set_linewidth(2.0) @@ -365,32 +437,54 @@ def animate_grid(base_dir, FILENAME, ax1.spines['geo'].set_capstyle('projecting') # adjust subplot within figure - fig.subplots_adjust(left=0.04,right=0.96,bottom=0.05,top=0.96) + fig.subplots_adjust(left=0.04, right=0.96, bottom=0.05, top=0.96) - # create output directory if non-existent + # create output directory if non-existent FIGURE_FILE.parent.mkdir(mode=MODE, parents=True, exist_ok=True) # replace data and contours to create movie frames # create image for each frame with writer.saving(fig, FIGURE_FILE, FIGURE_DPI): # for each input file - for t,gm in enumerate(dinput.month): + for t, gm in enumerate(dinput.month): # data for time t converted to a masked array subset = dinput.subset(gm) data = subset.to_masked_array() # interpolate to image coordinates - if (INTERVAL == 1) and (np.max(dinput.lon) > 180):# (0:360, 90:-90) - shift_data,lon180 = gravtk.tools.shift_grid(180.0,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,data.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,data.lat,lonsin,latsin,order) - elif (INTERVAL == 2) and (np.max(dinput.lon) > 180):# DDEG/2 - shift_data,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.data,dinput.lon) - shift_mask,lon180 = gravtk.tools.shift_grid(180.0+dlon,data.mask,dinput.lon) - img = gravtk.tools.interp_grid(shift_data,lon180,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(shift_mask,lon180,dinput.lat,lonsin,latsin,order) - else:# -180:180 or modification of there of - img = gravtk.tools.interp_grid(data.data,dinput.lon,dinput.lat,lonsin,latsin,order) - msk = gravtk.tools.interp_grid(data.mask,dinput.lon,dinput.lat,lonsin,latsin,order) + if (INTERVAL == 1) and ( + np.max(dinput.lon) > 180 + ): # (0:360, 90:-90) + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, data.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, data.lat, lonsin, latsin, order + ) + elif (INTERVAL == 2) and (np.max(dinput.lon) > 180): # DDEG/2 + shift_data, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.data, dinput.lon + ) + shift_mask, lon180 = gravtk.tools.shift_grid( + 180.0 + dlon, data.mask, dinput.lon + ) + img = gravtk.tools.interp_grid( + shift_data, lon180, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + shift_mask, lon180, dinput.lat, lonsin, latsin, order + ) + else: # -180:180 or modification of there of + img = gravtk.tools.interp_grid( + data.data, dinput.lon, dinput.lat, lonsin, latsin, order + ) + msk = gravtk.tools.interp_grid( + data.mask, dinput.lon, dinput.lat, lonsin, latsin, order + ) # create masked array of image img = np.ma.array(img, mask=msk.astype(bool)) # only plot grounded points @@ -402,38 +496,70 @@ def animate_grid(base_dir, FILENAME, # set data to image with transparency using normalization im.set_data(img) # recalculate data at zoomed coordinates - data = np.ma.array(scipy.ndimage.zoom(img.data,5,order=1)) - mask = scipy.ndimage.zoom(np.invert(img.mask),5,order=1,output=bool) + data = np.ma.array(scipy.ndimage.zoom(img.data, 5, order=1)) + mask = scipy.ndimage.zoom( + np.invert(img.mask), 5, order=1, output=bool + ) data.mask = np.invert(mask) # plot line contours contours = [] if CONTOURS and (np.sum(data**2) > 0): # plot line contours - contours.append(ax1.contour(data, reduce_clevs, - colors='0.2', linestyles='solid', - extent=(xmin,xmax,ymin,ymax), origin='lower', - transform=projection)) - contours.append(ax1.contour(data, [0], - colors='red', linestyles='solid', linewidths=1.5, - extent=(xmin,xmax,ymin,ymax), origin='lower', - transform=projection)) + contours.append( + ax1.contour( + data, + reduce_clevs, + colors='0.2', + linestyles='solid', + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ) + contours.append( + ax1.contour( + data, + [0], + colors='red', + linestyles='solid', + linewidths=1.5, + extent=(xmin, xmax, ymin, ymax), + origin='lower', + transform=projection, + ) + ) # plot line contour for global average if MEAN_CONTOUR and CONTOURS: # calculate areas of each grid cell - dphi,dth = (dlon*np.pi/180.0,dlat*np.pi/180.0) - indy,indx = np.nonzero(np.logical_not(subset.mask)) - area = (rad_e**2)*dth*dphi*np.cos(lat[indy,indx]*np.pi/180.0) + 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,129 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -574,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, @@ -598,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 @@ -606,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/pixi.lock b/pixi.lock index ca312704..d80f0d49 100644 --- a/pixi.lock +++ b/pixi.lock @@ -16,11 +16,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h7033f15_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.25.0-py313h08cd8bf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf01b4d8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cftime-1.6.4-py313h29aa505_2.conda @@ -40,18 +40,18 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py313h78bf25f_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.9-py313hc8edb43_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-ha97dd6f_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.4-h3f801dc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.1-gpl_h7be2006_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-36_h4a7cf45_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hb03c661_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-36_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda @@ -65,21 +65,21 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.3.0-h4c17acf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.4.0-h10be129_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.1-h6cb5226_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.2-h174a0a3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libkml-1.3.0-hf539b9f_1021.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-36_h47877c9_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.3-nompi_h11f7409_103.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/librttopo-1.1.0-h96cd706_19.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.22-h280c20c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-gpl_hcb59c51_118.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda @@ -94,7 +94,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-devel-2.15.0-h26afc86_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.0.2-py313h4a16004_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h280c20c_1002.conda @@ -106,7 +106,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.7.2-nompi_py313hfae5b86_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.3-py313hf6604e3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.4-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-11.3.0-py313ha492abd_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/proj-9.7.0-hb72c0af_0.conda @@ -130,8 +130,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h387f397_9.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda @@ -146,7 +146,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 @@ -269,7 +269,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 @@ -379,11 +379,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py313h6535dbc_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py313hb4b7877_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cartopy-0.25.0-py313h7d16b84_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py313h89bd988_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cftime-1.6.4-py313h2732efb_2.conda @@ -402,17 +402,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/json-c-0.18-he4178ee_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py313h8f79df9_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.4.9-py313hf88c9ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-hfd3d5f3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.17-h7eeda09_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.4-h51d1e36_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarchive-3.8.1-gpl_h46575ef_101.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-36_h51639a9_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.1.0-h6caf38d_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-36_hb0561ab_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.14.1-h73640d1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.2-hf598326_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda @@ -424,20 +424,20 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgdal-core-3.11.4-hf39520b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-hfcf01ff_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-h742603c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.3.0-h48b13b8_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.4.0-ha332bbd_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.1-h7274d02_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.2-h934fa54_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libkml-1.3.0-he250239_1021.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-36_hd9741b5_openblas.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnetcdf-4.9.3-nompi_h80c4520_103.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_h60d53f8_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librttopo-1.1.0-hf7cb3ef_19.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.22-h1a92334_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libspatialite-5.1.0-gpl_he9e465b_118.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda @@ -449,7 +449,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-devel-2.15.0-h9329255_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzip-1.11.2-h1336266_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.2-h4a912ad_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.0.2-py313he6cafaa_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda @@ -462,7 +462,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/netcdf4-1.7.2-nompi_py313hb28a5cb_104.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.3-py313h9771d21_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.4-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-11.3.0-py313he4c6d0d_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/proj-9.7.0-hf83150c_0.conda @@ -488,8 +488,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-h5505292_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h888dc83_9.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h10816f8_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py313h9734d34_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda - pypi: . @@ -506,7 +506,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 @@ -617,9 +617,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py313h5ea7bf4_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py313hfe59770_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cartopy-0.25.0-py313hc90dcd4_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py313h5ea7bf4_0.conda @@ -637,17 +637,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py313hfa70ccb_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.9-py313h1a38498_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.17-hbcf6048_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.4-h20038f6_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.1-gpl_h26aea39_101.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-35_h5709861_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.1.0-hfd05255_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-35_h2a3cdd5_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.14.1-h88aaa65_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h8206538_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda @@ -657,10 +657,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libgdal-core-3.11.4-he493212_4.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h1383e82_5.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h64bd3f2_1002.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.3.0-ha71e874_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.4.0-h172a326_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.1-hb7713f0_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.2-h932607e_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libkml-1.3.0-h538826c_1021.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-35_hf9ab0e9_mkl.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda @@ -668,7 +668,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libnetcdf-4.9.3-nompi_h7d90bef_103.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h7351971_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/librttopo-1.1.0-h5ff11c1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libspatialite-5.1.0-gpl_h3bf7137_118.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda @@ -681,7 +681,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-devel-2.15.0-ha29bfb0_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzip-1.11.2-h3135430_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.2-hfa2b4ca_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.0.2-py313h1af1686_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda @@ -694,7 +694,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/netcdf4-1.7.2-nompi_py313hbe59507_104.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.3-py313hce7ae62_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h24db6dd_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.4-h725018a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.46-h3402e2f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-11.3.0-py313hf455b62_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/proj-9.7.0-h9080b7b_0.conda @@ -725,12 +725,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-h0e40799_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-h0e40799_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h5bddc39_9.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h3a581c9_11.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py313h5fd188c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda - pypi: . - default: + aws: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: @@ -739,277 +739,2184 @@ environments: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.14.1-py313hd6074c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.3-hdc83ae9_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h6e18c7b_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.14.1-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.2-haaf8e00_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.7.1-h31346e9_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.11.0-h434eaf2_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.26.3-ha0b0e19_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.16.0-h4976820_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.12.6-hd767fb6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.7-haaf8e00_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.10-haaf8e00_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.40.1-h085388a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.833-h799c9a6_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.3-h206d751_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-h71f81a8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.18.0-h74b55db_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.14.0-hf596fc9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.16.0-h1f05bef_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h7033f15_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.25.0-py313h08cd8bf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf01b4d8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cftime-1.6.4-py313h29aa505_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py313h7037e92_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py313h07c4f96_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.17-py313h5d5ffb9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.60.1-py313h3dea7bd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freexl-2.0.0-h9dce30a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.8.0-py313h6b9daa2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdal-3.11.4-py313h60f829d_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/geos-3.14.0-h480dda7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.2-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/google-crc32c-1.8.0-py313h74173ec_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.14.0-nompi_py313h253c126_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h2a13503_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h6e4c0c1_103.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/json-c-0.18-h6688a6e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py313h78bf25f_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.9-py313hc8edb43_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-ha97dd6f_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260526.0-cxx17_h7b12aa8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.4-h3f801dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.1-gpl_h7be2006_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-24.0.0-hcdbc531_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-24.0.0-h635bf11_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-24.0.0-h53684a4_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-24.0.0-h635bf11_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-24.0.0-h66fbfdd_10_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-36_h4a7cf45_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hb03c661_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-36_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgdal-core-3.11.4-habfead2_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-3.6.0-hbc29df5_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-3.6.0-hdbdcf42_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.82.1-h792040b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.4.0-h10be129_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.2-h174a0a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libkml-1.3.0-hf539b9f_1021.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-36_h47877c9_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.3-nompi_h11f7409_103.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.27.0-h3133023_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.27.0-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-24.0.0-h7376487_10_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-7.35.1-h2840a7c_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h60473fc_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/librttopo-1.1.0-h96cd706_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.22-h280c20c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-gpl_hcb59c51_118.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h7d032f7_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h8261f1e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.0-ha9997c6_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.0-h26afc86_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-devel-2.15.0-h26afc86_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.0.2-py313h4a16004_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-4.4.5-py313h28739b2_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h280c20c_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.10.6-py313h683a580_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/minizip-4.0.10-h05a5f5f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.2.1-py313hc8edb43_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.7.1-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/muparser-2.3.5-h5888daf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.7.2-nompi_py313hfae5b86_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numcodecs-0.16.5-py313h08cd8bf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.3-py313hf6604e3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/obstore-0.11.0-py313h5c7d99a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.4-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h443056b_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.3-py313hbfd7664_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-11.3.0-py313ha492abd_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/proj-9.7.0-hb72c0af_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.5.2-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.0-py313h07c4f96_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-24.0.0-py313h78bf25f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-24.0.0-py313h98bfbea_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.7.2-py313h77f6078_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hfb55c3c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h94463f1_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.27.1-py313h843e2db_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.5-h7e3ee7f_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.2-py313h11c21cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/shapely-2.1.2-py313hfc84eb1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.50.4-hbc0de68_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.2-py313h07c4f96_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/uriparser-0.9.8-hac33072_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-1.17.3-py313h07c4f96_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xerces-c-3.2.5-h988505b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.24.2-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.7.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.13.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.9.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh707e725_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-2026.7.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.7.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/deprecated-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.7.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/donfig-0.8.1.post1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geoid-toolkit-1.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh82676e8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.6.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.25.1-he01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.23.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.23.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyshp-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2026.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250822-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-3.2.1-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - pypi: . osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.7.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.13.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.9.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh707e725_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-2026.7.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.7.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/deprecated-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.7.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/donfig-0.8.1.post1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geoid-toolkit-1.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh92f572d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.6.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.25.1-he01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.23.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.23.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyshp-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2026.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh31c8845_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250822-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-3.2.1-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aiohttp-3.14.1-py313h53c0e3e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py313h6535dbc_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.10.3-hc187ab6_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.9.14-h3623c0d_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.14.1-h84a0fba_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.3.2-h35327cf_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.7.1-h1e7fc79_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.11.0-he9547b6_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.26.3-h63484d5_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.16.0-h73b2f1b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.12.6-hc78c590_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.2.7-h35327cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.2.10-h35327cf_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.40.1-h9ead58b_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.833-h918993c_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.16.3-he5ae378_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.13.3-h05177fb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.18.0-h409340b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.14.0-h7cba3ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.16.0-h16bb3af_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py313hb4b7877_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cartopy-0.25.0-py313h7d16b84_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py313h89bd988_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cftime-1.6.4-py313h2732efb_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.3.3-py313hc50a443_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cytoolz-1.1.0-py313h0997733_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.17-py313hc37fe24_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fonttools-4.60.1-py313h7d74516_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freexl-2.0.0-h3ab3353_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozenlist-1.8.0-py313h750ce70_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdal-3.11.4-py313h976e3e5_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/geos-3.14.0-h4bcf65f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/giflib-5.2.2-h93a5062_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/google-crc32c-1.8.0-py313h11ab6f4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h5py-3.14.0-nompi_py313ha10fd41_101.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf4-4.2.15-h2ee6834_7.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf5-1.14.6-nompi_he65715a_103.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/json-c-0.18-he4178ee_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py313h8f79df9_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.4.9-py313hf88c9ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-hfd3d5f3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.17-h7eeda09_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260526.0-cxx17_h2062a1b_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.4-h51d1e36_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarchive-3.8.1-gpl_h46575ef_101.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-24.0.0-ha20fcc7_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-24.0.0-heaff1e6_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-compute-24.0.0-h93a2d87_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-24.0.0-heaff1e6_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-24.0.0-h2208e57_10_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-36_h51639a9_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.1.0-h6caf38d_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-36_hb0561ab_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.14.1-h73640d1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.2-hf598326_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.6-h1da3d7d_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgdal-core-3.11.4-hf39520b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-hfcf01ff_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-h742603c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-3.6.0-ha595bda_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-3.6.0-hc8aed40_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.82.1-hf3e839d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.4.0-ha332bbd_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.2-h934fa54_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libkml-1.3.0-he250239_1021.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-36_hd9741b5_openblas.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnetcdf-4.9.3-nompi_h80c4520_103.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_h60d53f8_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-1.27.0-h46c1d2a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-headers-1.27.0-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-24.0.0-h0de02aa_10_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-7.35.1-h8daa630_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2025.11.05-h5d15848_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librttopo-1.1.0-hf7cb3ef_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.22-h1a92334_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libspatialite-5.1.0-gpl_he9e465b_118.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.22.0-h1fb9c8a_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h7dc4979_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.11.3-h2431656_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.0-h0ff4647_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.0-h9329255_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-devel-2.15.0-h9329255_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzip-1.11.2-h1336266_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.2-h4a912ad_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.0.2-py313he6cafaa_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-4.4.5-py313hd065f0a_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lzo-2.10-h925e9cb_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py313h7d74516_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-base-3.10.6-py313h58042b9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/minizip-4.0.10-hff1a8ea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgpack-python-1.2.1-py313h2af2deb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.7.1-py313haf6918d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/muparser-2.3.5-h11e0b38_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/netcdf4-1.7.2-nompi_py313hb28a5cb_104.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.12.0-h784d473_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numcodecs-0.16.5-py313h7d16b84_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.3-py313h9771d21_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/obstore-0.11.0-py313h7d00576_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.4-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.3.0-hef2a647_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-3.0.3-py313h1188861_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-11.3.0-py313he4c6d0d_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/proj-9.7.0-hf83150c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/prometheus-cpp-1.3.0-h0967b3e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/propcache-0.5.2-py313h65a2061_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.0-py313h6535dbc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-24.0.0-py313h39782a4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-24.0.0-py313h23b330d_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-11.1-py313had225c5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-11.1-py313h4e140e3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproj-3.7.2-py313h698103d_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.7-h5c937ed_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py313h7d74516_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312hd65ceae_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/re2-2025.11.05-h5d60da5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.27.1-py313h80e0809_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.16.2-py313h0d10b07_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/shapely-2.1.2-py313hfb5a6ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hd121638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlite-3.50.4-hb5dd463_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.2-py313hcdf3177_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uriparser-0.9.8-h00cdb27_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.17.3-py313hcdf3177_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xerces-c-3.2.5-h92fc2f4_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.24.2-py313h65a2061_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h10816f8_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py313h9734d34_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: . + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.7.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.13.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.9.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh7428d3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-2026.7.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.7.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/deprecated-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.7.0-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/donfig-0.8.1.post1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geoid-toolkit-1.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh3521513_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.6.0-pyh6be1c34_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.25.1-he01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.23.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.23.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyshp-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2026.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh5737063_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250822-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zarr-3.2.1-pyhc364b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.14.1-py313h51e1470_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py313h5ea7bf4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.10.3-h0a1c2aa_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.9.14-h92e57be_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.14.1-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.3.2-h4c63076_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.7.1-h3c50fbe_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.11.0-h2c07012_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.26.3-hf70332f_6.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.16.0-h2dc7e49_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.12.6-hcb9225d_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.2.7-h4c63076_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.2.10-h4c63076_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.40.1-h5dbc166_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.833-h583dddf_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/azure-core-cpp-1.16.3-h49e36cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/azure-identity-cpp-1.13.3-hf9fdf8e_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/azure-storage-blobs-cpp-12.18.0-h4fb3251_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/azure-storage-common-cpp-12.14.0-hf9fdf8e_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/azure-storage-files-datalake-cpp-12.16.0-heb2e695_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/c-ares-1.34.6-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cartopy-0.25.0-py313hc90dcd4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cftime-1.6.4-py313h0591002_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/contourpy-1.3.3-py313hf069bd2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cytoolz-1.1.0-py313h5ea7bf4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.17-py313h927ade5_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fonttools-4.60.1-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.1-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freexl-2.0.0-hf297d47_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/frozenlist-1.8.0-py313h0c48a3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gdal-3.11.4-py313hf168f70_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/geos-3.14.0-hdade9fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/google-crc32c-1.8.0-py313h5327936_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/h5py-3.14.0-nompi_py313hf7f959b_101.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/hdf4-4.2.15-h5557f11_7.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_he30205f_103.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py313hfa70ccb_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.9-py313h1a38498_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.17-hbcf6048_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20260526.0-cxx17_h0eb2380_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.4-h20038f6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.1-gpl_h26aea39_101.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-24.0.0-h11fefc6_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-acero-24.0.0-h7d8d6a5_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-compute-24.0.0-h081cd8e_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-dataset-24.0.0-h7d8d6a5_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-substrait-24.0.0-hffb5221_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-35_h5709861_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-35_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcrc32c-1.1.2-h0e60522_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h8206538_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libevent-2.1.12-h3671451_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.1-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.1-hdbac1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h1383e82_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgdal-core-3.11.4-he493212_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h1383e82_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-3.6.0-hfe3f7e9_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-storage-3.6.0-he04ea4c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgrpc-1.82.1-he6cc664_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h64bd3f2_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.4.0-h172a326_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.2-h932607e_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libkml-1.3.0-h538826c_1021.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-35_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libnetcdf-4.9.3-nompi_h7d90bef_103.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopentelemetry-cpp-1.27.0-h4cd8edf_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libopentelemetry-cpp-headers-1.27.0-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libparquet-24.0.0-h7051d1f_10_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h7351971_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-7.35.1-h1a71995_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libre2-11-2025.11.05-h45f70d9_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/librttopo-1.1.0-h5ff11c1_19.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libspatialite-5.1.0-gpl_h3bf7137_118.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libthrift-0.22.0-h2e43b2f_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.1-h550210a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libutf8proc-2.11.3-hb980946_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxcb-1.17.0-h0e4246c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.0-h06f855e_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.0-ha29bfb0_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-devel-2.15.0-ha29bfb0_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzip-1.11.2-h3135430_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.2-hfa2b4ca_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.0.2-py313h1af1686_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-4.4.5-py313h4bbca4b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lzo-2.10-h6a83c73_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.10.6-py313he1ded55_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/minizip-4.0.10-h9fa1bad_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2024.2.2-h57928b3_16.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msgpack-python-1.2.1-py313h1a38498_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.7.1-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/muparser-2.3.5-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/netcdf4-1.7.2-nompi_py313hbe59507_104.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nlohmann_json-3.12.0-h5112557_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numcodecs-0.16.5-py313hc90dcd4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.3-py313hce7ae62_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/obstore-0.11.0-py313hf61f64f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h24db6dd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/orc-2.3.0-hf7eca67_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pandas-3.0.3-py313h26f5e95_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.46-h3402e2f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-11.3.0-py313hf455b62_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/proj-9.7.0-h9080b7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/prometheus-cpp-1.3.0-hcea2f5d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/propcache-0.5.2-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.0-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-24.0.0-py313hfa70ccb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-core-24.0.0-py313hc76bb52_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyproj-3.7.2-py313h24787ba_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.7-hdf00ec1_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py313h40c08fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py313h5813708_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312hbb5da91_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/qhull-2020.2-hc790b64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/re2-2025.11.05-haef9524_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.27.1-py313hfbe8231_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/scipy-1.16.2-py313h62a08ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py313hae85795_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.50.4-hdb435a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h18a62a1_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.2-py313h5ea7bf4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/uriparser-0.9.8-h5a68840_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.44.35208-h38c0c73_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/wrapt-1.17.3-py313h5ea7bf4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xerces-c-3.2.5-he0c23c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yarl-1.24.2-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h3a581c9_11.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py313h5fd188c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda + - pypi: . + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf01b4d8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cftime-1.6.4-py313h29aa505_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py313h7037e92_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.60.1-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h2a13503_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h6e4c0c1_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.9-py313hc8edb43_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-ha97dd6f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.4-h3f801dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-36_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-36_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-36_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.3-nompi_h11f7409_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h8261f1e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.0-ha9997c6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.0-h26afc86_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.0.2-py313h4a16004_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.10.6-py313h683a580_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.7.2-nompi_py313hfae5b86_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.3-py313hf6604e3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-11.3.0-py313ha492abd_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.2-py313h11c21cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - pypi: . + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py313h89bd988_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cftime-1.6.4-py313h2732efb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.3.3-py313hc50a443_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fonttools-4.60.1-py313h7d74516_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf4-4.2.15-h2ee6834_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf5-1.14.6-nompi_he65715a_103.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.4.9-py313hf88c9ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-hfd3d5f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.17-h7eeda09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.4-h51d1e36_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-36_h51639a9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-36_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.2-hf598326_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.6-h1da3d7d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-hfcf01ff_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-h742603c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-36_hd9741b5_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnetcdf-4.9.3-nompi_h80c4520_103.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_h60d53f8_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h7dc4979_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.0-h0ff4647_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.0-h9329255_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzip-1.11.2-h1336266_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.2-h4a912ad_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.0.2-py313he6cafaa_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-base-3.10.6-py313h58042b9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/netcdf4-1.7.2-nompi_py313hb28a5cb_104.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.3-py313h9771d21_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-11.3.0-py313he4c6d0d_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.7-h5c937ed_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py313h7d74516_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.16.2-py313h0d10b07_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hd121638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py313h9734d34_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: . + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cftime-1.6.4-py313h0591002_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/contourpy-1.3.3-py313hf069bd2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fonttools-4.60.1-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.1-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/hdf4-4.2.15-h5557f11_7.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_he30205f_103.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.9-py313h1a38498_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.17-hbcf6048_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.4-h20038f6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-35_h5709861_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-35_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h8206538_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.1-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.1-hdbac1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h1383e82_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h1383e82_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h64bd3f2_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-35_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libnetcdf-4.9.3-nompi_h7d90bef_103.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h7351971_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.1-h550210a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxcb-1.17.0-h0e4246c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.0-h06f855e_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.0-ha29bfb0_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzip-1.11.2-h3135430_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.2-hfa2b4ca_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.0.2-py313h1af1686_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.10.6-py313he1ded55_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2024.2.2-h57928b3_16.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/netcdf4-1.7.2-nompi_py313hbe59507_104.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.3-py313hce7ae62_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h24db6dd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-11.3.0-py313hf455b62_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.7-hdf00ec1_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/qhull-2020.2-hc790b64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/scipy-1.16.2-py313h62a08ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h18a62a1_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py313h5fd188c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda + - pypi: . + dev: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.25.0-py313h08cd8bf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf01b4d8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cftime-1.6.4-py313h29aa505_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py313h7037e92_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.10.7-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.17-py313h5d5ffb9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-hb03c661_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.60.1-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freexl-2.0.0-h9dce30a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdal-3.11.4-py313h60f829d_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.3-h2b0a6b4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/geos-3.14.0-h480dda7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.2-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.86.0-hf516916_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-13.1.2-h87b6fe6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.2.4-py313h7033f15_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h021d004_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.14.0-nompi_py313h253c126_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-12.1.0-h15599e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h2a13503_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h6e4c0c1_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/json-c-0.18-h6688a6e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py313h78bf25f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.9-py313hc8edb43_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-ha97dd6f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.4-h3f801dc_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.1-gpl_h7be2006_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-36_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-36_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgdal-core-3.11.4-habfead2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-devel-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.0-h1fed272_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.4.0-h10be129_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.2-h174a0a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libkml-1.3.0-hf539b9f_1021.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-36_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.3-nompi_h11f7409_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.60.0-h61e6d4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/librttopo-1.1.0-h96cd706_19.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.22-h280c20c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-gpl_hcb59c51_118.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h8f9b012_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-h4852527_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h8261f1e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-hca5e8e5_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.0-ha9997c6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.0-h26afc86_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-devel-2.15.0-h26afc86_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.0.2-py313h4a16004_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h280c20c_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.10.6-py313h683a580_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/minizip-4.0.10-h05a5f5f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/muparser-2.3.5-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.7.2-nompi_py313hfae5b86_104.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.3-py313hf6604e3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-11.3.0-py313ha492abd_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/proj-9.7.0-hb72c0af_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.0-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pybtex-docutils-1.0.3-py313h78bf25f_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.7.2-py313h77f6078_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hfb55c3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.27.1-py313h843e2db_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.2-py313h11c21cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/shapely-2.1.2-py313hfc84eb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.43-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.50.4-hbc0de68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.2-py313h07c4f96_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/uriparser-0.9.8-hac33072_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xerces-c-3.2.5-h988505b_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.46-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-xorgproto-2024.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh707e725_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/commonmark-0.9.1-py_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-7.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geoid-toolkit-1.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh82676e8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.6.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.25.1-he01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-cache-1.0.1-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-nb-1.3.0-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.9.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.23.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pybtex-0.25.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyflakes-3.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyshp-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-argparse-0.5.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-design-0.6.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx_rtd_theme-3.0.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-bibtex-2.6.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jquery-4.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250822-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - pypi: . + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh707e725_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/commonmark-0.9.1-py_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-7.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geoid-toolkit-1.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh92f572d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.6.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.25.1-he01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-cache-1.0.1-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-nb-1.3.0-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.9.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.23.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pybtex-0.25.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyflakes-3.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyshp-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh31c8845_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-argparse-0.5.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-design-0.6.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx_rtd_theme-3.0.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-bibtex-2.6.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jquery-4.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250822-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py313h6535dbc_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cartopy-0.25.0-py313h7d16b84_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py313h89bd988_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cftime-1.6.4-py313h2732efb_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.3.3-py313hc50a443_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.10.7-py313h7d74516_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.17-py313hc37fe24_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-hc919400_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fonttools-4.60.1-py313h7d74516_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freexl-2.0.0-h3ab3353_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdal-3.11.4-py313h976e3e5_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.3-h7542897_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/geos-3.14.0-h4bcf65f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/giflib-5.2.2-h93a5062_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.86.0-hb9d6e3a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-13.1.2-hcd33d8b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/greenlet-3.2.4-py313hb4b7877_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.43-h07173f4_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h5py-3.14.0-nompi_py313ha10fd41_101.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-11.5.1-haf38c7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf4-4.2.15-h2ee6834_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf5-1.14.6-nompi_he65715a_103.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/json-c-0.18-he4178ee_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py313h8f79df9_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.4.9-py313hf88c9ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-hfd3d5f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.17-h7eeda09_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.4-h51d1e36_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarchive-3.8.1-gpl_h46575ef_101.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-36_h51639a9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-36_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.2-hf598326_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.6-h1da3d7d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgd-2.3.3-hb2c3a21_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgdal-core-3.11.4-hf39520b_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-hfcf01ff_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-h742603c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.86.0-h1bb475b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.4.0-ha332bbd_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.2-h934fa54_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libkml-1.3.0-he250239_1021.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-36_hd9741b5_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnetcdf-4.9.3-nompi_h80c4520_103.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_h60d53f8_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.60.0-h5c55ec3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librttopo-1.1.0-hf7cb3ef_19.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.22-h1a92334_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libspatialite-5.1.0-gpl_he9e465b_118.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h7dc4979_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.0-h0ff4647_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.0-h9329255_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-devel-2.15.0-h9329255_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzip-1.11.2-h1336266_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.2-h4a912ad_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.0.2-py313he6cafaa_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lzo-2.10-h925e9cb_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py313h7d74516_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-base-3.10.6-py313h58042b9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/minizip-4.0.10-hff1a8ea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/muparser-2.3.5-h11e0b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/netcdf4-1.7.2-nompi_py313hb28a5cb_104.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.3-py313h9771d21_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-11.3.0-py313he4c6d0d_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/proj-9.7.0-hf83150c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.0-py313h6535dbc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pybtex-docutils-1.0.3-py313h8f79df9_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-11.1-py313had225c5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-11.1-py313h4e140e3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproj-3.7.2-py313h698103d_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.7-h5c937ed_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py313h7d74516_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312hd65ceae_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.27.1-py313h80e0809_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.16.2-py313h0d10b07_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/shapely-2.1.2-py313hfb5a6ed_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hd121638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlalchemy-2.0.43-py313hcdf3177_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlite-3.50.4-hb5dd463_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.2-py313hcdf3177_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uriparser-0.9.8-h00cdb27_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xerces-c-3.2.5-h92fc2f4_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-h5505292_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h10816f8_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py313h9734d34_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda - pypi: . + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.5-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh7428d3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/commonmark-0.9.1-py_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-7.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geoid-toolkit-1.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh3521513_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.6.0-pyh6be1c34_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jmespath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/json5-0.12.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.25.1-he01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-cache-1.0.1-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-nb-1.3.0-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.9.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.23.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pybtex-0.25.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyflakes-3.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyshp-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh5737063_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-argparse-0.5.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-design-0.6.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx_rtd_theme-3.0.2-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-bibtex-2.6.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jquery-4.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.9.0.20250822-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.15.0-h396c80c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.14-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-24.11.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.8.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.14-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py313h5ea7bf4_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py313hfe59770_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cartopy-0.25.0-py313hc90dcd4_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py313h5ea7bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cftime-1.6.4-py313h0591002_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/contourpy-1.3.3-py313hf069bd2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.10.7-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.17-py313h927ade5_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/fonttools-4.60.1-py313hd650c13_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.1-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freexl-2.0.0-hf297d47_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gdal-3.11.4-py313hf168f70_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/geos-3.14.0-hdade9fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/getopt-win32-0.1-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-hac47afa_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/graphviz-13.1.2-ha5e8f4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/greenlet-3.2.4-py313hfe59770_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gts-0.7.6-h6b5321d_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/h5py-3.14.0-nompi_py313hf7f959b_101.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-12.1.0-h5f2951f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/hdf4-4.2.15-h5557f11_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_he30205f_103.conda - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py313hfa70ccb_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.9-py313h1a38498_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.17-hbcf6048_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.4-h20038f6_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.1-gpl_h26aea39_101.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-35_h5709861_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.1.0-hfd05255_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-35_h2a3cdd5_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.14.1-h88aaa65_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h8206538_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.1-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.1-hdbac1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h1383e82_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgd-2.3.3-h7208af6_11.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgdal-core-3.11.4-he493212_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.86.0-h5f26cbf_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h1383e82_5.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h64bd3f2_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.4.0-h172a326_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.2-h932607e_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libkml-1.3.0-h538826c_1021.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-35_hf9ab0e9_mkl.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-h2466b09_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libnetcdf-4.9.3-nompi_h7d90bef_103.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h7351971_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/librttopo-1.1.0-h5ff11c1_19.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libspatialite-5.1.0-gpl_h3bf7137_118.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.1-h550210a_0.conda @@ -1018,39 +2925,70 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libxcb-1.17.0-h0e4246c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.0-h06f855e_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.0-ha29bfb0_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-devel-2.15.0-ha29bfb0_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzip-1.11.2-h3135430_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.2-hfa2b4ca_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.0.2-py313h1af1686_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lzo-2.10-h6a83c73_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py313hd650c13_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.10.6-py313he1ded55_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/minizip-4.0.10-h9fa1bad_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2024.2.2-h57928b3_16.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/muparser-2.3.5-he0c23c2_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/netcdf4-1.7.2-nompi_py313hbe59507_104.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.3-py313hce7ae62_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h24db6dd_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.4-h725018a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h03d888a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.46-h3402e2f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-11.3.0-py313hf455b62_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/proj-9.7.0-h9080b7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.0-py313h5ea7bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pybtex-docutils-1.0.3-py313hfa70ccb_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyproj-3.7.2-py313h24787ba_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.7-hdf00ec1_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py313h40c08fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywinpty-2.0.15-py313h5813708_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py313hd650c13_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312hbb5da91_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/qhull-2020.2-hc790b64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.27.1-py313hfbe8231_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/scipy-1.16.2-py313h62a08ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py313hae85795_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sqlalchemy-2.0.43-py313h5ea7bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.50.4-hdb435a2_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h18a62a1_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.2-py313h5ea7bf4_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/uriparser-0.9.8-h5a68840_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_31.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_31.conda - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_31.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/xerces-c-3.2.5-he0c23c2_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libice-1.1.2-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libsm-1.2.6-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libx11-1.8.12-hf48077a_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-h0e40799_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxext-1.3.6-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxpm-3.5.17-h0e40799_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxt-1.3.1-h0e40799_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h3a581c9_11.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py313h5fd188c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda - pypi: . - dev: + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + doc: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: @@ -1065,17 +3003,16 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h7033f15_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.25.0-py313h08cd8bf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf01b4d8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cftime-1.6.4-py313h29aa505_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py313h7037e92_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.10.7-py313h3dea7bd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.17-py313h5d5ffb9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-hb03c661_2.conda @@ -1104,19 +3041,19 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py313h78bf25f_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.9-py313hc8edb43_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-ha97dd6f_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.4-h3f801dc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.1-gpl_h7be2006_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-36_h4a7cf45_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hb03c661_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-36_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda @@ -1140,23 +3077,23 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.3.0-h4c17acf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.4.0-h10be129_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.1-h6cb5226_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.2-h174a0a3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libkml-1.3.0-hf539b9f_1021.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-36_h47877c9_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.3-nompi_h11f7409_103.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.60.0-h61e6d4b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/librttopo-1.1.0-h96cd706_19.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.22-h280c20c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-gpl_hcb59c51_118.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda @@ -1172,7 +3109,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-devel-2.15.0-h26afc86_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.0.2-py313h4a16004_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h280c20c_1002.conda @@ -1184,7 +3121,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.7.2-nompi_py313hfae5b86_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.3-py313hf6604e3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.4-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-11.3.0-py313ha492abd_3.conda @@ -1229,8 +3166,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.6-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-xorgproto-2024.1-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h387f397_9.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda @@ -1247,7 +3184,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 @@ -1263,9 +3200,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-7.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 @@ -1284,7 +3219,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh82676e8_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.6.0-pyhfa0c392_0.conda @@ -1313,7 +3247,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda @@ -1334,22 +3267,16 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.23.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pybtex-0.25.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyflakes-3.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyshp-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda @@ -1418,7 +3345,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 @@ -1434,9 +3361,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-7.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 @@ -1455,7 +3380,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh92f572d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.6.0-pyhfa0c392_0.conda @@ -1484,7 +3408,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda @@ -1505,22 +3428,16 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.23.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pybtex-0.25.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyflakes-3.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyshp-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda @@ -1574,17 +3491,16 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py313h6535dbc_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py313hb4b7877_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cartopy-0.25.0-py313h7d16b84_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py313h89bd988_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cftime-1.6.4-py313h2732efb_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.3.3-py313hc50a443_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coverage-7.10.7-py313h7d74516_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.17-py313hc37fe24_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-hc919400_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda @@ -1611,17 +3527,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/json-c-0.18-he4178ee_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py313h8f79df9_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.4.9-py313hf88c9ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-hfd3d5f3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.17-h7eeda09_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.4-h51d1e36_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarchive-3.8.1-gpl_h46575ef_101.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-36_h51639a9_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.1.0-h6caf38d_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-36_hb0561ab_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.14.1-h73640d1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.2-hf598326_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda @@ -1635,22 +3551,22 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-hfcf01ff_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-h742603c_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.86.0-h1bb475b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.3.0-h48b13b8_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.4.0-ha332bbd_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.1-h7274d02_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.2-h934fa54_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libkml-1.3.0-he250239_1021.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-36_hd9741b5_openblas.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnetcdf-4.9.3-nompi_h80c4520_103.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_h60d53f8_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.60.0-h5c55ec3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librttopo-1.1.0-hf7cb3ef_19.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.22-h1a92334_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libspatialite-5.1.0-gpl_he9e465b_118.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda @@ -1662,7 +3578,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-devel-2.15.0-h9329255_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzip-1.11.2-h1336266_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.2-h4a912ad_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.0.2-py313he6cafaa_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda @@ -1675,7 +3591,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/netcdf4-1.7.2-nompi_py313hb28a5cb_104.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.3-py313h9771d21_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.4-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-11.3.0-py313he4c6d0d_3.conda @@ -1705,8 +3621,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-h5505292_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h888dc83_9.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h10816f8_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py313h9734d34_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda - pypi: . @@ -1725,7 +3641,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 @@ -1741,9 +3657,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/flake8-7.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 @@ -1762,7 +3676,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh3521513_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.6.0-pyh6be1c34_0.conda @@ -1791,7 +3704,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mccabe-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda @@ -1811,21 +3723,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.4.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.23.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pybtex-0.25.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pycodestyle-2.14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pyflakes-3.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyshp-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.2-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.2-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.7-h4df99d1_100.conda @@ -1880,16 +3786,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py313h5ea7bf4_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py313hfe59770_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cartopy-0.25.0-py313hc90dcd4_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py313h5ea7bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cftime-1.6.4-py313h0591002_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/contourpy-1.3.3-py313hf069bd2_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/coverage-7.10.7-py313hd650c13_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.17-py313h927ade5_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/fonttools-4.60.1-py313hd650c13_0.conda @@ -1910,17 +3815,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py313hfa70ccb_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.9-py313h1a38498_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.17-hbcf6048_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.4-h20038f6_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.1-gpl_h26aea39_101.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-35_h5709861_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.1.0-hfd05255_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-35_h2a3cdd5_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.14.1-h88aaa65_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h8206538_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda @@ -1932,11 +3837,11 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.86.0-h5f26cbf_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h1383e82_5.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h64bd3f2_1002.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.3.0-ha71e874_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.4.0-h172a326_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.1-hb7713f0_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.2-h932607e_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libkml-1.3.0-h538826c_1021.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-35_hf9ab0e9_mkl.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda @@ -1944,7 +3849,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libnetcdf-4.9.3-nompi_h7d90bef_103.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h7351971_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/librttopo-1.1.0-h5ff11c1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libspatialite-5.1.0-gpl_h3bf7137_118.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda @@ -1957,7 +3862,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-devel-2.15.0-ha29bfb0_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzip-1.11.2-h3135430_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.2-hfa2b4ca_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.0.2-py313h1af1686_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda @@ -1970,7 +3875,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/netcdf4-1.7.2-nompi_py313hbe59507_104.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.3-py313hce7ae62_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h24db6dd_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.4-h725018a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h03d888a_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.46-h3402e2f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-11.3.0-py313hf455b62_3.conda @@ -2011,13 +3916,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxpm-3.5.17-h0e40799_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxt-1.3.1-h0e40799_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h5bddc39_9.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h3a581c9_11.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py313h5fd188c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda - pypi: . - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl - doc: + map: channels: - url: https://conda.anaconda.org/conda-forge/ indexes: @@ -2027,67 +3932,46 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/attr-2.5.2-h39aace5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h7033f15_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.25.0-py313h08cd8bf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf01b4d8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cftime-1.6.4-py313h29aa505_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py313h7037e92_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.17-py313h5d5ffb9_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-hb03c661_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.60.1-py313h3dea7bd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/freexl-2.0.0-h9dce30a_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gdal-3.11.4-py313h60f829d_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.3-h2b0a6b4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/geos-3.14.0-h480dda7_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.2-hd590300_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.86.0-hf516916_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-13.1.2-h87b6fe6_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/greenlet-3.2.4-py313h7033f15_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h021d004_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.14.0-nompi_py313h253c126_101.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-12.1.0-h15599e2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h2a13503_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.14.6-nompi_h6e4c0c1_103.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/json-c-0.18-h6688a6e_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py313h78bf25f_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.9-py313hc8edb43_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-ha97dd6f_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.4-h3f801dc_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.1-gpl_h7be2006_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-36_h4a7cf45_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb03c661_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hb03c661_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-36_h0358290_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.125-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-1.7.0-ha4b6fd6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libegl-devel-1.7.0-ha4b6fd6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda @@ -2095,34 +3979,25 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-h767d61c_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgdal-core-3.11.4-habfead2_4.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-hcd61629_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-1.7.0-ha4b6fd6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgl-devel-1.7.0-ha4b6fd6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.0-h1fed272_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libglvnd-1.7.0-ha4b6fd6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-1.7.0-ha4b6fd6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libglx-devel-1.7.0-ha4b6fd6_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-h767d61c_5.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.3.0-h4c17acf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.4.0-h10be129_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.1-h6cb5226_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.2-h174a0a3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libkml-1.3.0-hf539b9f_1021.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-36_h47877c9_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.3-nompi_h11f7409_103.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.60.0-h61e6d4b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/librttopo-1.1.0-h96cd706_19.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.22-h280c20c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-gpl_hcb59c51_118.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda @@ -2132,13 +4007,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-hca5e8e5_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.0-ha9997c6_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.0-h26afc86_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-devel-2.15.0-h26afc86_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxslt-1.1.43-h711ed8c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzip-1.11.2-h6991a6a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.0.2-py313h4a16004_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h280c20c_1002.conda @@ -2150,15 +4024,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.7.2-nompi_py313hfae5b86_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.3-py313hf6604e3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.4-h26f9b46_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-11.3.0-py313ha492abd_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/proj-9.7.0-hb72c0af_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.0-py313h07c4f96_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pybtex-docutils-1.0.3-py313h78bf25f_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.7.2-py313h77f6078_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_0.conda @@ -2169,39 +4040,19 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.2-py313h11c21cd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/shapely-2.1.2-py313hfc84eb1_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlalchemy-2.0.43-py313h07c4f96_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.50.4-hbc0de68_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.2-py313h07c4f96_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/uriparser-0.9.8-hac33072_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xerces-c-3.2.5-h988505b_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.46-hb03c661_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.2-hb03c661_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxxf86vm-1.1.6-hb9d3cd8_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-xorgproto-2024.1-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h387f397_9.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda @@ -2213,29 +4064,19 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh707e725_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/commonmark-0.9.1-py_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/geoid-toolkit-1.1.2-pyhd8ed1ab_0.conda @@ -2246,7 +4087,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh82676e8_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda @@ -2261,7 +4101,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.25.1-he01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-cache-1.0.1-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda @@ -2273,22 +4112,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.15-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-nb-1.3.0-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.9.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 @@ -2300,7 +4132,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pybtex-0.25.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda @@ -2317,28 +4148,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh0d859eb_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-argparse-0.5.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-design-0.6.1-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx_rtd_theme-3.0.2-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-bibtex-2.6.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jquery-4.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda @@ -2357,11 +4173,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.14-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - pypi: . - - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda @@ -2374,29 +4187,19 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh707e725_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/commonmark-0.9.1-py_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/geoid-toolkit-1.1.2-pyhd8ed1ab_0.conda @@ -2407,7 +4210,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh92f572d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda @@ -2422,7 +4224,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.25.1-he01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-cache-1.0.1-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda @@ -2434,22 +4235,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.15-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-nb-1.3.0-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.9.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 @@ -2461,7 +4255,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pybtex-0.25.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda @@ -2478,28 +4271,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh31c8845_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-argparse-0.5.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-design-0.6.1-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx_rtd_theme-3.0.2-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-bibtex-2.6.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jquery-4.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda @@ -2518,55 +4296,41 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.14-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py313h6535dbc_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py313hb4b7877_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cartopy-0.25.0-py313h7d16b84_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-2.0.0-py313h89bd988_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cftime-1.6.4-py313h2732efb_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.3.3-py313hc50a443_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.17-py313hc37fe24_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-hc919400_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fonttools-4.60.1-py313h7d74516_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freexl-2.0.0-h3ab3353_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdal-3.11.4-py313h976e3e5_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.3-h7542897_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/geos-3.14.0-h4bcf65f_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/giflib-5.2.2-h93a5062_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.86.0-hb9d6e3a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-13.1.2-hcd33d8b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/greenlet-3.2.4-py313hb4b7877_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.43-h07173f4_5.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h5py-3.14.0-nompi_py313ha10fd41_101.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-11.5.1-haf38c7b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf4-4.2.15-h2ee6834_7.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hdf5-1.14.6-nompi_he65715a_103.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_2.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/json-c-0.18-he4178ee_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py313h8f79df9_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.4.9-py313hf88c9ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-hfd3d5f3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.17-h7eeda09_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.4-h51d1e36_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarchive-3.8.1-gpl_h46575ef_101.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-36_h51639a9_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.1.0-h6caf38d_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.1.0-h6caf38d_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-36_hb0561ab_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.14.1-h73640d1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.2-hf598326_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda @@ -2575,27 +4339,23 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.6-h1da3d7d_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgd-2.3.3-hb2c3a21_11.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgdal-core-3.11.4-hf39520b_4.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-hfcf01ff_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-h742603c_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.86.0-h1bb475b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.3.0-h48b13b8_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.4.0-ha332bbd_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.1-h7274d02_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.2-h934fa54_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libkml-1.3.0-he250239_1021.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-36_hd9741b5_openblas.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnetcdf-4.9.3-nompi_h80c4520_103.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_h60d53f8_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.60.0-h5c55ec3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librttopo-1.1.0-hf7cb3ef_19.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.22-h1a92334_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libspatialite-5.1.0-gpl_he9e465b_118.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda @@ -2607,7 +4367,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-devel-2.15.0-h9329255_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxslt-1.1.43-hb2570ba_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzip-1.11.2-h1336266_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.2-h4a912ad_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lxml-6.0.2-py313he6cafaa_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda @@ -2620,15 +4380,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/netcdf4-1.7.2-nompi_py313hb28a5cb_104.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.3-py313h9771d21_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.4-h5503f6c_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-11.3.0-py313he4c6d0d_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/proj-9.7.0-hf83150c_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.0-py313h6535dbc_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pybtex-docutils-1.0.3-py313h8f79df9_3.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-core-11.1-py313had225c5_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyobjc-framework-cocoa-11.1-py313h4e140e3_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproj-3.7.2-py313h698103d_2.conda @@ -2641,7 +4398,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.16.2-py313h0d10b07_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/shapely-2.1.2-py313hfb5a6ed_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hd121638_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlalchemy-2.0.43-py313hcdf3177_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlite-3.50.4-hb5dd463_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.2-py313hcdf3177_1.conda @@ -2650,15 +4406,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-h5505292_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hd74edd7_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h888dc83_9.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h10816f8_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py313h9734d34_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda - pypi: . - - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.11.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_1.conda @@ -2670,29 +4424,20 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.2.0-pyh29332c3_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.2.0-h82add2a_4.conda - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.10.5-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.0-pyh7428d3b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/commonmark-0.9.1-py_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.7-py313hd8ed1ab_100.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/geoid-toolkit-1.1.2-pyhd8ed1ab_0.conda @@ -2703,7 +4448,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.30.1-pyh3521513_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.7-pyhcf101f3_2.conda @@ -2718,7 +4462,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.25.1-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.25.1-he01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-cache-1.0.1-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda @@ -2730,22 +4473,15 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.15-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdit-py-plugins-0.5.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.1.4-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-nb-1.3.0-pyhe01879c_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/myst-parser-4.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.16.6-pyh29332c3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/numpydoc-1.9.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 @@ -2755,7 +4491,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.23.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pybtex-0.25.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.5-pyhcf101f3_0.conda @@ -2772,28 +4507,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/roman-numerals-py-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.3-pyh5737063_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-8.2.3-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-argparse-0.5.2-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx-design-0.6.1-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinx_rtd_theme-3.0.2-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-bibtex-2.6.5-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.1.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jquery-4.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-2.0.0-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda @@ -2815,62 +4535,50 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py313h5ea7bf4_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py313hfe59770_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cartopy-0.25.0-py313hc90dcd4_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-2.0.0-py313h5ea7bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/cftime-1.6.4-py313h0591002_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/contourpy-1.3.3-py313hf069bd2_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.17-py313h927ade5_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/fonttools-4.60.1-py313hd650c13_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.1-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/freexl-2.0.0-hf297d47_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/gdal-3.11.4-py313hf168f70_4.conda - conda: https://conda.anaconda.org/conda-forge/win-64/geos-3.14.0-hdade9fe_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/getopt-win32-0.1-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-hac47afa_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/graphviz-13.1.2-ha5e8f4b_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/greenlet-3.2.4-py313hfe59770_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/gts-0.7.6-h6b5321d_4.conda - conda: https://conda.anaconda.org/conda-forge/win-64/h5py-3.14.0-nompi_py313hf7f959b_101.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-12.1.0-h5f2951f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/hdf4-4.2.15-h5557f11_7.conda - conda: https://conda.anaconda.org/conda-forge/win-64/hdf5-1.14.6-nompi_he30205f_103.conda - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py313hfa70ccb_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.4.9-py313h1a38498_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.17-hbcf6048_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.4-h20038f6_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.1-gpl_h26aea39_101.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-35_h5709861_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.1.0-hfd05255_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.1.0-hfd05255_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-35_h2a3cdd5_mkl.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.14.1-h88aaa65_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h8206538_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.1-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.1-hdbac1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.2.0-h1383e82_5.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libgd-2.3.3-h7208af6_11.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgdal-core-3.11.4-he493212_4.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.86.0-h5f26cbf_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.2.0-h1383e82_5.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h64bd3f2_1002.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.3.0-ha71e874_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.4.0-h172a326_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.1-hb7713f0_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.2-h932607e_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libkml-1.3.0-h538826c_1021.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-35_hf9ab0e9_mkl.conda - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda @@ -2878,7 +4586,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libnetcdf-4.9.3-nompi_h7d90bef_103.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h7351971_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/librttopo-1.1.0-h5ff11c1_19.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libspatialite-5.1.0-gpl_h3bf7137_118.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.4-hf5d6505_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda @@ -2891,7 +4599,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-devel-2.15.0-ha29bfb0_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libxslt-1.1.43-h0fbe4c1_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libzip-1.11.2-h3135430_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.2-hfa2b4ca_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lxml-6.0.2-py313h1af1686_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda @@ -2904,15 +4612,12 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/netcdf4-1.7.2-nompi_py313hbe59507_104.conda - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.3-py313hce7ae62_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h24db6dd_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.4-h725018a_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h03d888a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.46-h3402e2f_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-11.3.0-py313hf455b62_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/proj-9.7.0-h9080b7b_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.0-py313h5ea7bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/pybtex-docutils-1.0.3-py313hfa70ccb_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyproj-3.7.2-py313h24787ba_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.7-hdf00ec1_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py313h40c08fc_1.conda @@ -2924,7 +4629,6 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/scipy-1.16.2-py313h62a08ca_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py313hae85795_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/sqlalchemy-2.0.43-py313h5ea7bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.50.4-hdb435a2_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h18a62a1_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda @@ -2936,21 +4640,14 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_31.conda - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/xerces-c-3.2.5-he0c23c2_2.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libice-1.1.2-h0e40799_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libsm-1.2.6-h0e40799_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libx11-1.8.12-hf48077a_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-h0e40799_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-h0e40799_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxext-1.3.6-h0e40799_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxpm-3.5.17-h0e40799_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxt-1.3.1-h0e40799_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h5bddc39_9.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h3a581c9_11.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py313h5fd188c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda - pypi: . - - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 @@ -2973,6 +4670,29 @@ packages: purls: [] size: 23621 timestamp: 1650670423406 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.14.1-py313hd6074c6_0.conda + sha256: f374f40167f46eabe4cd5142201a35eca9c7a82a14499b95b7c1348b09509d2a + md5: cfc88e2202ea33635e6d69960fddc588 + depends: + - __glibc >=2.17,<3.0.a0 + - aiohappyeyeballs >=2.5.0 + - aiosignal >=1.4.0 + - attrs >=17.3.0 + - frozenlist >=1.1.1 + - libgcc >=14 + - multidict >=4.5,<7.0 + - propcache >=0.2.0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - typing_extensions >=4.4 + - yarl >=1.17.0,<2.0 + license: MIT AND Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/aiohttp?source=hash-mapping + run_exports: {} + size: 1083744 + timestamp: 1780914191006 - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_1.conda sha256: 49761e3f489914ee783173f26b278943d9169275f81e56785b57738e22ea1958 md5: 0d56c6a245ef9528644a33947a4dca4f @@ -3042,6 +4762,319 @@ packages: purls: [] size: 68072 timestamp: 1756738968573 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.10.3-hdc83ae9_4.conda + sha256: d7746767f17d3bf12cef8e69b7d6fa1ba975a25d4a1c3250bc7031020f0eb94d + md5: 00840a04f8ec0033ed380f150fcd1c93 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-sdkutils >=0.2.7,<0.2.8.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-c-auth >=0.10.3,<0.10.4.0a0 + size: 134896 + timestamp: 1783461441420 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.14-h6e18c7b_3.conda + sha256: 594d8c27ea57997863499a8e55ac8fb6587cdd62639445da91e1a88756488a88 + md5: d1b68989381f8cbf582ba7a5e3bb5b02 + depends: + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - libgcc >=14 + - openssl >=3.5.7,<4.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - aws-c-cal >=0.9.14,<0.9.15.0a0 + size: 56631 + timestamp: 1783165003471 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.14.1-hb03c661_0.conda + sha256: a83c1a18754fe0f466d3a230106d6a52847c915c625b0be6b9f4677f46024a6c + md5: 4faa1e8d0cbc9137e8b0109c78931234 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - aws-c-common >=0.14.1,<0.14.2.0a0 + size: 244798 + timestamp: 1782343578152 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.2-haaf8e00_3.conda + sha256: 5f40faa0c3661a231e1a01e4abf287621509ae60fb95c92fdafdf7a2dd3d0da7 + md5: 953cf7d978432cc1703395b3e97b2776 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-compression >=0.3.2,<0.3.3.0a0 + size: 22288 + timestamp: 1783166720432 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.7.1-h31346e9_3.conda + sha256: 557075a0f86f5aabde49be012bd6bba3af2c4c567a8ed3c1e5d5dd571eb41eca + md5: 52e4dca958c1ea325a8a839a42fb2538 + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-checksums >=0.2.10,<0.2.11.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + size: 59588 + timestamp: 1783192975741 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.11.0-h434eaf2_3.conda + sha256: 8a21457233210ca69bae462d932d0513ccf6329a0914b2f454b42aa936ddf819 + md5: 5d0c1e7ef42363ed72969ed488703644 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-compression >=0.3.2,<0.3.3.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-http >=0.11.0,<0.11.1.0a0 + size: 231263 + timestamp: 1783192866041 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.26.3-ha0b0e19_6.conda + sha256: 5406c95965698b7998f9288a05a5aa138d7cc6af82e4a823d3afaca09170e06c + md5: bec59731db9cc0965f270a6e33a8db73 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - s2n >=1.7.5,<1.7.6.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-io >=0.26.3,<0.26.4.0a0 + size: 182092 + timestamp: 1783180025608 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.16.0-h4976820_1.conda + sha256: fa938630cc8ff9d1d29496f05bfe1afdfedb0c1c8ad895b009dc384e2e877c1a + md5: ee72798a2101d2849eb3e2e6f2a5410c + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-mqtt >=0.16.0,<0.16.1.0a0 + size: 224932 + timestamp: 1783205864451 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.12.6-hd767fb6_1.conda + sha256: 81abe7f421c630b168454f40d4daf58d73b09aab78f8d99b5d6c8e2d132bcfc7 + md5: aa4756841efe742938984663ef2397ef + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - openssl >=3.5.7,<4.0a0 + - aws-c-auth >=0.10.3,<0.10.4.0a0 + - aws-checksums >=0.2.10,<0.2.11.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-c-s3 >=0.12.6,<0.12.7.0a0 + size: 154344 + timestamp: 1783471978484 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.7-haaf8e00_1.conda + sha256: e2653b11b1b222c9189cdf0362be5e2d347589d9ad5dfad25509860852e3ba85 + md5: 2eb431d738f8e5ee90aff8ac5d255456 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-sdkutils >=0.2.7,<0.2.8.0a0 + size: 68604 + timestamp: 1783172608400 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.10-haaf8e00_3.conda + sha256: 90c4d073871f309cc35bc66da55eb169c9ac27790c51670963fd0a36a6e46f0d + md5: 394a24d9d451020541ba28c27fc6ef16 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-checksums >=0.2.10,<0.2.11.0a0 + size: 101925 + timestamp: 1783166893505 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.40.1-h085388a_2.conda + sha256: aef7545f1b8b9913be6500b093d9c48f2f213847ebad650241eb5f3ae457cda8 + md5: b486f1ffd023a845fd8966cd828eefd3 + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + - aws-c-s3 >=0.12.6,<0.12.7.0a0 + - aws-c-auth >=0.10.3,<0.10.4.0a0 + - aws-c-sdkutils >=0.2.7,<0.2.8.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-mqtt >=0.16.0,<0.16.1.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + size: 416431 + timestamp: 1783483029313 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.833-h799c9a6_8.conda + sha256: 2608dbc63fdb18feecff50dbdbde1f2331ef9bd4058c80a7bade5f48eca980de + md5: a2102f9d40aad9453580dec56e481c1b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + - libzlib >=1.3.2,<2.0a0 + - libcurl >=8.21.0,<9.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-sdk-cpp >=1.11.833,<1.11.834.0a0 + size: 3394406 + timestamp: 1783536782931 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.3-h206d751_0.conda + sha256: fffc66e9be8806a92b314e27129c42b1298ea7065028c9e5d175dacb07829261 + md5: 0cabc152dc8896c3a4c4aebf2b308627 + depends: + - __glibc >=2.17,<3.0.a0 + - libcurl >=8.19.0,<9.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + size: 349147 + timestamp: 1775238757304 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-h71f81a8_2.conda + sha256: ebca774f1ebaa24c150730603b418b33fc7862811a16d097716b1a29f34798c5 + md5: f4fc754ec17176046988c46a54962a8c + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-identity-cpp >=1.13.3,<1.13.4.0a0 + size: 250737 + timestamp: 1781268163278 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.18.0-h74b55db_1.conda + sha256: 04bb27dbf1d426b4447b28c3dc92ec01c807fa784eea86ffa1f8c2bd9b9e8076 + md5: 43151a3b0a8e037747d79a82dff9f967 + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + size: 589716 + timestamp: 1781283775801 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.14.0-hf596fc9_1.conda + sha256: 8a806f9852d280926d7613df548889b49e2a1c5d836385bcb2cedb24e7b08c64 + md5: 7896f4a6ee78538e2d0261e3b36dfa69 + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libxml2 + - libxml2-16 >=2.14.6 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + size: 159394 + timestamp: 1781268171097 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.16.0-h1f05bef_1.conda + sha256: 13e2e6eb942f65bf81e9089bf6b5926534d9245c781288c5270beb3a25c9156b + md5: 70972c9cb0893d6499dd4118415a966b + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-storage-files-datalake-cpp >=12.16.0,<12.16.1.0a0 + size: 308699 + timestamp: 1781538835962 - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda sha256: e7af5d1183b06a206192ff440e08db1c4e8b2ca1f8376ee45fb2f3a85d4ee45d md5: 2c2fae981fd2afd00812c92ac47d023d @@ -3058,36 +5091,42 @@ packages: purls: [] size: 48427 timestamp: 1733513201413 -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.1.0-hb03c661_4.conda - sha256: 294526a54fa13635341729f250d0b1cf8f82cad1e6b83130304cbf3b6d8b74cc - md5: eaf3fbd2aa97c212336de38a51fe404e +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + sha256: e511644d691f05eb12ebe1e971fd6dc3ae55a4df5c253b4e1788b789bdf2dfa6 + md5: 8ccf913aaba749a5496c17629d859ed1 depends: - __glibc >=2.17,<3.0.a0 - - brotli-bin 1.1.0 hb03c661_4 - - libbrotlidec 1.1.0 hb03c661_4 - - libbrotlienc 1.1.0 hb03c661_4 + - brotli-bin 1.2.0 hb03c661_1 + - libbrotlidec 1.2.0 hb03c661_1 + - libbrotlienc 1.2.0 hb03c661_1 - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 19883 - timestamp: 1756599394934 -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hb03c661_4.conda - sha256: 444903c6e5c553175721a16b7c7de590ef754a15c28c99afbc8a963b35269517 - md5: ca4ed8015764937c81b830f7f5b68543 + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 20103 + timestamp: 1764017231353 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + sha256: 64b137f30b83b1dd61db6c946ae7511657eead59fdf74e84ef0ded219605aa94 + md5: af39b9a8711d4a8d437b52c1d78eb6a1 depends: - __glibc >=2.17,<3.0.a0 - - libbrotlidec 1.1.0 hb03c661_4 - - libbrotlienc 1.1.0 hb03c661_4 + - libbrotlidec 1.2.0 hb03c661_1 + - libbrotlienc 1.2.0 hb03c661_1 - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 19615 - timestamp: 1756599385418 -- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py313h7033f15_4.conda - sha256: b1941426e564d326097ded7af8b525540be219be7a88ca961d58a8d4fc116db2 - md5: bc8624c405856b1d047dd0a81829b08c + run_exports: {} + size: 21021 + timestamp: 1764017221344 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + sha256: dadec2879492adede0a9af0191203f9b023f788c18efd45ecac676d424c458ae + md5: 6c4d3597cf43f3439a51b2b13e29a4ba depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 @@ -3095,13 +5134,14 @@ packages: - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 constrains: - - libbrotlicommon 1.1.0 hb03c661_4 + - libbrotlicommon 1.2.0 hb03c661_1 license: MIT license_family: MIT purls: - - pkg:pypi/brotli?source=compressed-mapping - size: 353639 - timestamp: 1756599425945 + - pkg:pypi/brotli?source=hash-mapping + run_exports: {} + size: 367721 + timestamp: 1764017371123 - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 md5: 51a19bba1b8ebfb60df25cde030b7ebc @@ -3113,17 +5153,20 @@ packages: purls: [] size: 260341 timestamp: 1757437258798 -- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda - sha256: f8003bef369f57396593ccd03d08a8e21966157269426f71e943f96e4b579aeb - md5: f7f0d6cc2dc986d42ac2689ec88192be +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + sha256: cc9accf72fa028d31c2a038460787751127317dcfa991f8d1f1babf216bb454e + md5: 920bb03579f15389b9e512095ad995b7 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 + - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 206884 - timestamp: 1744127994291 + run_exports: + weak: + - c-ares >=1.34.6,<2.0a0 + size: 207882 + timestamp: 1765214722852 - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda sha256: 3bd6a391ad60e471de76c0e9db34986c4b5058587fbf2efa5a7f54645e28c2c7 md5: 09262e66b19567aff4f592fb53b28760 @@ -3233,6 +5276,22 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 390586 timestamp: 1758501129226 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py313h07c4f96_2.conda + sha256: fd33f531288fb08afef72a65414d29caefbba31cb398533534794475af35b1b3 + md5: 7e7e3c5a8a28c6b8eb430183e0554adf + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - toolz >=0.10.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cytoolz?source=hash-mapping + run_exports: {} + size: 620281 + timestamp: 1771855841837 - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda sha256: 3b988146a50e165f0fa4e839545c679af88e4782ec284cc7b6d07dd226d6a068 md5: 679616eb5ad4e521c83da4650860aba7 @@ -3353,6 +5412,22 @@ packages: purls: [] size: 61244 timestamp: 1757438574066 +- conda: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.8.0-py313h6b9daa2_0.conda + sha256: 8b228f80d05f6ea9d1ed5051fb343b1623777949d2e2d377e30eb348f412c6f2 + md5: a51a4bc5d02deee4354781783c132b7f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/frozenlist?source=hash-mapping + run_exports: {} + size: 54166 + timestamp: 1779999854010 - conda: https://conda.anaconda.org/conda-forge/linux-64/gdal-3.11.4-py313h60f829d_4.conda sha256: 7bf977f1d5e36be8a1aaa2679669aaadad21f239e3b560bf202ef08d4e34033e md5: 256aa979eef0da305e38ea92e40e157e @@ -3397,6 +5472,21 @@ packages: purls: [] size: 1977241 timestamp: 1755851798617 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + sha256: 6c33bf0c4d8f418546ba9c250db4e4221040936aef8956353bc764d4877bc39a + md5: d411fc29e338efb48c5fd4576d71d881 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - gflags >=2.2.2,<2.3.0a0 + size: 119654 + timestamp: 1726600001928 - conda: https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.2-hd590300_0.conda sha256: aac402a8298f0c0cc528664249170372ef6b37ac39fdc92b40601a6aed1e32ff md5: 3bf7b9fd5a7136126e0234db4b87c8b6 @@ -3418,6 +5508,37 @@ packages: purls: [] size: 117284 timestamp: 1757403341964 +- conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda + sha256: dc824dc1d0aa358e28da2ecbbb9f03d932d976c8dca11214aa1dcdfcbd054ba2 + md5: ff862eebdfeb2fd048ae9dc92510baca + depends: + - gflags >=2.2.2,<2.3.0a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - glog >=0.7.1,<0.8.0a0 + size: 143452 + timestamp: 1718284177264 +- conda: https://conda.anaconda.org/conda-forge/linux-64/google-crc32c-1.8.0-py313h74173ec_1.conda + sha256: c0e39ebce8b9d8bc4e734bb5d2538e60f7c8c85ec04f9b0690fc6e1cb9656869 + md5: d358850e37a98739224fdc265d7d8eb7 + depends: + - __glibc >=2.17,<3.0.a0 + - libcrc32c >=1.1.2,<1.2.0a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/google-crc32c?source=hash-mapping + run_exports: {} + size: 25326 + timestamp: 1768549200259 - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda sha256: 25ba37da5c39697a77fce2c9a15e48cf0a84f1464ad2aafbe53d8357a9f6cc8c md5: 2cd94587f3a401ae05e03a6caf09539d @@ -3659,21 +5780,25 @@ packages: - pkg:pypi/kiwisolver?source=hash-mapping size: 77227 timestamp: 1756467528380 -- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238 - md5: 3f43953b7d3fb3aaa1d0d0723d91e368 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda + sha256: 9b07046870772f28740e3f6149f09ff222843733087a33c5540b169c6289652d + md5: 54157a1c8c0bb70f62dd0b17fba7e7f2 depends: - - keyutils >=1.6.1,<2.0a0 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - libgcc-ng >=12 - - libstdcxx-ng >=12 - - openssl >=3.3.1,<4.0a0 + - __glibc >=2.17,<3.0.a0 + - keyutils >=1.6.3,<2.0a0 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.7,<4.0a0 license: MIT license_family: MIT purls: [] - size: 1370023 - timestamp: 1719463201255 + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 1388990 + timestamp: 1781859420533 - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.17-h717163a_0.conda sha256: d6a61830a354da022eae93fa896d0991385a875c6bba53c82263a289deda9db8 md5: 000e85703f0fd9594c81710dd5066471 @@ -3707,42 +5832,182 @@ packages: - libgcc >=13 - libstdcxx >=13 license: Apache-2.0 - license_family: Apache + license_family: Apache + purls: [] + size: 264243 + timestamp: 1745264221534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260526.0-cxx17_h7b12aa8_1.conda + sha256: 32933de2d4fa6e6ffd949052815b49cb65a0649ad70007155c533ab97ea8cefd + md5: c4393db381bffa0a83a8d9e47b238106 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + constrains: + - abseil-cpp =20260526.0 + - libabseil-static =20260526.0=cxx17* + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libabseil >=20260526.0,<20260527.0a0 + - libabseil =*=cxx17* + size: 1437712 + timestamp: 1780524559298 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.4-h3f801dc_0.conda + sha256: 410ab78fe89bc869d435de04c9ffa189598ac15bb0fe1ea8ace8fb1b860a2aa3 + md5: 01ba04e414e47f95c03d6ddd81fd37be + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 36825 + timestamp: 1749993532943 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.1-gpl_h7be2006_101.conda + sha256: 6a1a1a660a5911d0c8bf423a2ceddedd578c8685737afc9fb1b150c17df32484 + md5: e6f55365ce179b10b48de5a1677ae013 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - libgcc >=14 + - liblzma >=5.8.1,<6.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - lzo >=2.10,<3.0a0 + - openssl >=3.5.2,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 896229 + timestamp: 1757963333811 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-24.0.0-hcdbc531_10_cpu.conda + build_number: 10 + sha256: 0569ad9533fb7308babd16bf0b4248ebf8333eb7a2af89f879c33792f3afa7bd + md5: 627bb5c0594fdccb1bea8542ac64a026 + depends: + - __glibc >=2.17,<3.0.a0 + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + - aws-sdk-cpp >=1.11.833,<1.11.834.0a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-identity-cpp >=1.13.3,<1.13.4.0a0 + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + - azure-storage-files-datalake-cpp >=12.16.0,<12.16.1.0a0 + - bzip2 >=1.0.8,<2.0a0 + - glog >=0.7.1,<0.8.0a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libgcc >=14 + - libgoogle-cloud >=3.6.0,<3.7.0a0 + - libgoogle-cloud-storage >=3.6.0,<3.7.0a0 + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - orc >=2.3.0,<2.3.1.0a0 + - snappy >=1.2.2,<1.3.0a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - parquet-cpp <0.0a0 + - arrow-cpp <0.0a0 + - apache-arrow-proc =*=cpu + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow >=24.0.0,<24.1.0a0 + size: 6501233 + timestamp: 1783212718608 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-24.0.0-h635bf11_10_cpu.conda + build_number: 10 + sha256: 2fbd1f480fe901ee6d12f50bbc9ac0eed9e0418f795391aa8d7351aa9e0d6852 + md5: 07f90881b97ae46fdd4af7aae42d19ce + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 24.0.0 hcdbc531_10_cpu + - libarrow-compute 24.0.0 h53684a4_10_cpu + - libgcc >=14 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow-acero >=24.0.0,<24.1.0a0 + size: 591246 + timestamp: 1783212906064 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-24.0.0-h53684a4_10_cpu.conda + build_number: 10 + sha256: 2b564ed69fe192884aa5965acfbada753997fdf31cd6ea931e2fa60a4cae95f1 + md5: be945f60e50334ae726acb7a679b7a5f + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 24.0.0 hcdbc531_10_cpu + - libgcc >=14 + - libre2-11 >=2025.11.5 + - libstdcxx >=14 + - libutf8proc >=2.11.3,<2.12.0a0 + - re2 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 264243 - timestamp: 1745264221534 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libaec-1.1.4-h3f801dc_0.conda - sha256: 410ab78fe89bc869d435de04c9ffa189598ac15bb0fe1ea8ace8fb1b860a2aa3 - md5: 01ba04e414e47f95c03d6ddd81fd37be + run_exports: + weak: + - libarrow-compute >=24.0.0,<24.1.0a0 + size: 2990464 + timestamp: 1783212783919 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-24.0.0-h635bf11_10_cpu.conda + build_number: 10 + sha256: 49b46a7bdcfa2b5f7e7edc70c5755fd1e86529a2e2556a476349dc5dbd31699a + md5: fe97a7da671ea6611cd19a20a6720ef1 depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libstdcxx >=13 - license: BSD-2-Clause - license_family: BSD + - libarrow 24.0.0 hcdbc531_10_cpu + - libarrow-acero 24.0.0 h635bf11_10_cpu + - libarrow-compute 24.0.0 h53684a4_10_cpu + - libgcc >=14 + - libparquet 24.0.0 h7376487_10_cpu + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 36825 - timestamp: 1749993532943 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.1-gpl_h7be2006_101.conda - sha256: 6a1a1a660a5911d0c8bf423a2ceddedd578c8685737afc9fb1b150c17df32484 - md5: e6f55365ce179b10b48de5a1677ae013 + run_exports: + weak: + - libarrow-dataset >=24.0.0,<24.1.0a0 + size: 590994 + timestamp: 1783212990303 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-24.0.0-h66fbfdd_10_cpu.conda + build_number: 10 + sha256: 57f357ac5a0fb56b692aa9aad9fc7b74aad1c66652968a261bf06624e6383d14 + md5: 97325d79f36aae1bf062f44805098d75 depends: - __glibc >=2.17,<3.0.a0 - - bzip2 >=1.0.8,<2.0a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libarrow 24.0.0 hcdbc531_10_cpu + - libarrow-acero 24.0.0 h635bf11_10_cpu + - libarrow-dataset 24.0.0 h635bf11_10_cpu - libgcc >=14 - - liblzma >=5.8.1,<6.0a0 - - libxml2 - - libxml2-16 >=2.14.6 - - libzlib >=1.3.1,<2.0a0 - - lz4-c >=1.10.0,<1.11.0a0 - - lzo >=2.10,<3.0a0 - - openssl >=3.5.2,<4.0a0 - - zstd >=1.5.7,<1.6.0a0 - license: BSD-2-Clause - license_family: BSD + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 896229 - timestamp: 1757963333811 + run_exports: + weak: + - libarrow-substrait >=24.0.0,<24.1.0a0 + size: 500452 + timestamp: 1783213020688 - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-36_h4a7cf45_openblas.conda build_number: 36 sha256: a1670eb8c9293f37a245e313bd9d72a301c79e8668a6a5d418c90335719fbaff @@ -3761,41 +6026,50 @@ packages: purls: [] size: 17421 timestamp: 1758396490057 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.1.0-hb03c661_4.conda - sha256: 2338a92d1de71f10c8cf70f7bb9775b0144a306d75c4812276749f54925612b6 - md5: 1d29d2e33fe59954af82ef54a8af3fe1 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + sha256: 318f36bd49ca8ad85e6478bd8506c88d82454cc008c1ac1c6bf00a3c42fa610e + md5: 72c8fd1af66bd67bf580645b426513ed depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 69333 - timestamp: 1756599354727 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hb03c661_4.conda - sha256: fcec0d26f67741b122f0d5eff32f0393d7ebd3ee6bb866ae2f17f3425a850936 - md5: 5cb5a1c9a94a78f5b23684bcb845338d + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + size: 79965 + timestamp: 1764017188531 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + sha256: 12fff21d38f98bc446d82baa890e01fd82e3b750378fedc720ff93522ffb752b + md5: 366b40a69f0ad6072561c1d09301c886 depends: - __glibc >=2.17,<3.0.a0 - - libbrotlicommon 1.1.0 hb03c661_4 + - libbrotlicommon 1.2.0 hb03c661_1 - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 33406 - timestamp: 1756599364386 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hb03c661_4.conda - sha256: d42c7f0afce21d5279a0d54ee9e64a2279d35a07a90e0c9545caae57d6d7dc57 - md5: 2e55011fa483edb8bfe3fd92e860cd79 + run_exports: + weak: + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 34632 + timestamp: 1764017199083 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + sha256: a0c15c79997820bbd3fbc8ecf146f4fe0eca36cc60b62b63ac6cf78857f1dd0d + md5: 4ffbb341c8b616aa2494b6afb26a0c5f depends: - __glibc >=2.17,<3.0.a0 - - libbrotlicommon 1.1.0 hb03c661_4 + - libbrotlicommon 1.2.0 hb03c661_1 - libgcc >=14 license: MIT license_family: MIT purls: [] - size: 289680 - timestamp: 1756599375485 + run_exports: + weak: + - libbrotlienc >=1.2.0,<1.3.0a0 + size: 298378 + timestamp: 1764017210931 - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-36_h0358290_openblas.conda build_number: 36 sha256: 45110023d1661062288168c6ee01510bcb472ba2f5184492acdcdd3d1af9b58d @@ -3811,37 +6085,57 @@ packages: purls: [] size: 17401 timestamp: 1758396499759 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda - sha256: cb83980c57e311783ee831832eb2c20ecb41e7dee6e86e8b70b8cef0e43eab55 - md5: d4a250da4737ee127fb1fa6452a9002e +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 + sha256: fd1d153962764433fe6233f34a72cdeed5dcf8a883a85769e8295ce940b5b0c5 + md5: c965a5aa0d5c1c37ffc62dff36e28400 + depends: + - libgcc-ng >=9.4.0 + - libstdcxx-ng >=9.4.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libcrc32c >=1.1.2,<1.2.0a0 + size: 20440 + timestamp: 1633683576494 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-h7a8fb5f_6.conda + sha256: 205c4f19550f3647832ec44e35e6d93c8c206782bdd620c1d7cf66237580ff9c + md5: 49c553b47ff679a6a1e9fc80b9c5a2d4 depends: - __glibc >=2.17,<3.0.a0 - - krb5 >=1.21.3,<1.22.0a0 - - libgcc >=13 - - libstdcxx >=13 + - krb5 >=1.22.2,<1.23.0a0 + - libgcc >=14 + - libstdcxx >=14 - libzlib >=1.3.1,<2.0a0 license: Apache-2.0 license_family: Apache purls: [] - size: 4523621 - timestamp: 1749905341688 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda - sha256: b6c5cf340a4f80d70d64b3a29a7d9885a5918d16a5cb952022820e6d3e79dc8b - md5: 45f6713cb00f124af300342512219182 + run_exports: + weak: + - libcups >=2.3.3,<2.4.0a0 + size: 4518030 + timestamp: 1770902209173 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.21.0-hcf29cc6_1.conda + sha256: 7d243f45a48f62cb8e3b871dd336137fe0fb90094a191756fb553508837f6338 + md5: 2c1e55d695b11525c760b486ac0be517 depends: - __glibc >=2.17,<3.0.a0 - - krb5 >=1.21.3,<1.22.0a0 - - libgcc >=13 - - libnghttp2 >=1.64.0,<2.0a0 + - krb5 >=1.22.2,<1.23.0a0 + - libgcc >=14 + - libnghttp2 >=1.68.1,<2.0a0 - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 - zstd >=1.5.7,<1.6.0a0 license: curl license_family: MIT purls: [] - size: 449910 - timestamp: 1749033146806 + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 478914 + timestamp: 1782802520033 - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda sha256: 8420748ea1cc5f18ecc5068b4f24c7a023cc9b20971c99c824ba10641fb95ddf md5: 64f0c503da58ec25ebd359e4d990afa8 @@ -3910,6 +6204,20 @@ packages: purls: [] size: 112766 timestamp: 1702146165126 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda + sha256: 2e14399d81fb348e9d231a82ca4d816bf855206923759b69ad006ba482764131 + md5: a1cfcc585f0c42bf8d5546bb1dfb668d + depends: + - libgcc-ng >=12 + - openssl >=3.1.1,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libevent >=2.1.12,<2.1.13.0a0 + size: 427426 + timestamp: 1685725977222 - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda sha256: da2080da8f0288b95dd86765c801c6e166c4619b910b11f9a8446fb852438dc2 md5: 4211416ecba1866fab0c6470986c22d6 @@ -4149,17 +6457,90 @@ packages: purls: [] size: 448982 timestamp: 1759682511761 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.3.0-h4c17acf_1.conda - sha256: 2bdd1cdd677b119abc5e83069bec2e28fe6bfb21ebaea3cd07acee67f38ea274 - md5: c2a0c1d0120520e979685034e0b79859 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-3.6.0-hbc29df5_1.conda + sha256: b4d7be073a7ee3773284c79a6d4864157b0f6dd6b1fa34d76acecbd9de9068ed + md5: b29293dba25b40ed6aac45d271ce9208 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcurl >=8.21.0,<9.0a0 + - libgcc >=14 + - libgrpc >=1.82.0,<1.83.0a0 + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libstdcxx >=14 + - openssl >=3.5.7,<4.0a0 + constrains: + - libgoogle-cloud 3.6.0 *_1 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libgoogle-cloud >=3.6.0,<3.7.0a0 + size: 2692420 + timestamp: 1783158998339 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-3.6.0-hdbdcf42_1.conda + sha256: d3b6ca880388033cca73e52d1ed44784debc30a5625ce8cb1bccdf4e69d410fe + md5: 98471c9293325926adc267a878f17ece + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl + - libgcc >=14 + - libgoogle-cloud 3.6.0 hbc29df5_1 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + - openssl + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libgoogle-cloud-storage >=3.6.0,<3.7.0a0 + size: 784898 + timestamp: 1783159120502 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.82.1-h792040b_0.conda + sha256: 1e657c1b802c111178bc1845fe06488db5f69a85e34e970ce0989810aa562d45 + md5: aa4571fc3bcf18ad12370429aa991223 + depends: + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.34.6,<2.0a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libgcc >=14 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libre2-11 >=2025.11.5 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 + - re2 + constrains: + - grpc-cpp =1.82.1 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libgrpc >=1.82.1,<1.83.0a0 + size: 6957755 + timestamp: 1783588701960 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.4.0-h10be129_0.conda + sha256: 8b70955d5e9a49d08945d4f8e2eab855b2efa5fce9cb9bc5e75d86764e6f2f38 + md5: 3a9428b74c403c71048104d38437b48c depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 - libstdcxx >=14 license: Apache-2.0 OR BSD-3-Clause purls: [] - size: 1448617 - timestamp: 1758894401402 + run_exports: + weak: + - libhwy >=1.4.0,<1.5.0a0 + size: 1435782 + timestamp: 1776989559668 - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f md5: 915f5995e94f60e9a4826e0b0920ee88 @@ -4182,21 +6563,24 @@ packages: purls: [] size: 628947 timestamp: 1745268527144 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.1-h6cb5226_4.conda - sha256: b9d924d69fc84cd3c660a181985748d9c2df34cd7c7bb03b92d8f70efa7753d9 - md5: f2840d9c2afb19e303e126c9d3a04b36 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.2-h174a0a3_1.conda + sha256: 0c8a78c6a42a6e4c6de3a5e82d692f60400d43f4cc80591745f28b37daad9c70 + md5: 850f48943d6b4589800a303f0de6a816 depends: - __glibc >=2.17,<3.0.a0 - - libbrotlidec >=1.1.0,<1.2.0a0 - - libbrotlienc >=1.1.0,<1.2.0a0 - libgcc >=14 - - libhwy >=1.3.0,<1.4.0a0 - libstdcxx >=14 + - libhwy >=1.4.0,<1.5.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 1740823 - timestamp: 1757583994233 + run_exports: + weak: + - libjxl >=0.11,<1.0a0 + size: 1846962 + timestamp: 1777065125966 - conda: https://conda.anaconda.org/conda-forge/linux-64/libkml-1.3.0-hf539b9f_1021.conda sha256: 721c3916d41e052ffd8b60e77f2da6ee47ff0d18babfca48ccf93606f1e0656a md5: e8c7620cc49de0c6a2349b6dd6e39beb @@ -4276,23 +6660,26 @@ packages: purls: [] size: 871447 timestamp: 1757977084313 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda - sha256: a4a7dab8db4dc81c736e9a9b42bdfd97b087816e029e221380511960ac46c690 - md5: b499ce4b026493a13774bcf0f4c33849 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda + sha256: 663444d77a42f2265f54fb8b48c5450bfff4388d9c0f8253dd7855f0d993153f + md5: 2a45e7f8af083626f009645a6481f12d depends: - __glibc >=2.17,<3.0.a0 - - c-ares >=1.34.5,<2.0a0 + - c-ares >=1.34.6,<2.0a0 - libev >=4.33,<4.34.0a0 - libev >=4.33,<5.0a0 - libgcc >=14 - libstdcxx >=14 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.2,<4.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 666600 - timestamp: 1756834976695 + run_exports: + weak: + - libnghttp2 >=1.68.1,<2.0a0 + size: 663344 + timestamp: 1773854035739 - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 md5: d864d34357c3b65a4b731f78c0801dc4 @@ -4319,6 +6706,57 @@ packages: purls: [] size: 5938936 timestamp: 1755474342204 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.27.0-h3133023_1.conda + sha256: 8f9281133322e9eb0bd4a5b11330db1c14f3b40c409639dd3805251151ca52e2 + md5: f749f223bede1bac7724e49d686ac598 + depends: + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcurl >=8.21.0,<9.0a0 + - libgrpc >=1.82.0,<1.83.0a0 + - libopentelemetry-cpp-headers 1.27.0 ha770c72_1 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libzlib >=1.3.2,<2.0a0 + - nlohmann_json + - prometheus-cpp >=1.3.0,<1.4.0a0 + constrains: + - cpp-opentelemetry-sdk =1.27.0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + size: 948783 + timestamp: 1783133058845 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.27.0-ha770c72_1.conda + sha256: 31349543e3c59c64f17e24494939a22b0e1d611bdb19d98d115775dba423cbf7 + md5: 6cc68cbbe88746be8beaf027d6c64ad3 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 394726 + timestamp: 1783133038109 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-24.0.0-h7376487_10_cpu.conda + build_number: 10 + sha256: 9d8c988d42540d5d7282b75d30cf615dabf70f3ca2eb41d732f0c63893ff1609 + md5: ad6da7642b43c15e7545657050db17c2 + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 24.0.0 hcdbc531_10_cpu + - libgcc >=14 + - libstdcxx >=14 + - libthrift >=0.22.0,<0.22.1.0a0 + - openssl >=3.5.7,<4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libparquet >=24.0.0,<24.1.0a0 + size: 1427119 + timestamp: 1783212876990 - conda: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hb9d3cd8_0.conda sha256: 0bd91de9b447a2991e666f284ae8c722ffb1d84acb594dbd0c031bd656fa32b2 md5: 70e3400cbbfa03e96dcde7fc13e38c7b @@ -4341,6 +6779,43 @@ packages: purls: [] size: 317390 timestamp: 1753879899951 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-7.35.1-h2840a7c_2.conda + sha256: b5ac3938186516c1091b96414c34f90255da2a3bbd736a0712b22bbf4b5ebf02 + md5: 729db8acaadb9a5e5bb2dc3d9ac84ae4 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libprotobuf >=7.35.1,<7.35.2.0a0 + size: 3774596 + timestamp: 1783168720126 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h60473fc_2.conda + sha256: 43132eb45a569b1f63199b0e7d5874d94227e9be950b327a323e7dcc1f8cd58c + md5: ee5c400d37b79db5d32a69ed1a79fc0b + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libgcc >=14 + - libstdcxx >=14 + constrains: + - re2 2025.11.05.* + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libre2-11 >=2025.11.5 + size: 210598 + timestamp: 1781312565737 - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.60.0-h61e6d4b_0.conda sha256: 960b137673b2b8293e2a12d194add72967b3bf12fcdf691e7ad8bd5c8318cec3 md5: 91e6d4d684e237fba31b9815c4b40edf @@ -4371,15 +6846,19 @@ packages: purls: [] size: 232294 timestamp: 1755880773417 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - sha256: 0105bd108f19ea8e6a78d2d994a6d4a8db16d19a41212070d2d1d48a63c34161 - md5: a587892d3c13b6621a6091be690dbca2 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.22-h280c20c_1.conda + sha256: b677bbf1c339d894757c3dcfbb2f88649e499e4991d70ae09a1466da9a6c92d6 + md5: 965e4d531b588b2e42f66fd8e48b056c depends: - - libgcc-ng >=12 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 license: ISC purls: [] - size: 205978 - timestamp: 1716828628198 + run_exports: + weak: + - libsodium >=1.0.22,<1.0.23.0a0 + size: 269272 + timestamp: 1779163468406 - conda: https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-gpl_hcb59c51_118.conda sha256: 309e16bb34f98cd383fe4c84c5b8b9a19fcbc33dd0fe5dda04a59f9d39eb6977 md5: ce07b32efdf860ed996fdedcff3ea96e @@ -4449,6 +6928,24 @@ packages: purls: [] size: 29339 timestamp: 1759682673925 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h7d032f7_2.conda + sha256: af6025aa4a4fc3f4e71334000d2739d927e2f678607b109ec630cc17d716918a + md5: b6e326fbe1e3948da50ec29cee0380db + depends: + - __glibc >=2.17,<3.0.a0 + - libevent >=2.1.12,<2.1.13.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libthrift >=0.22.0,<0.22.1.0a0 + size: 423861 + timestamp: 1777018957474 - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h8261f1e_0.conda sha256: ddda0d7ee67e71e904a452010c73e32da416806f5cb9145fb62c322f97e717fb md5: 72b531694ebe4e8aa6f5745d1015c1b4 @@ -4467,6 +6964,20 @@ packages: purls: [] size: 437211 timestamp: 1758278398952 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda + sha256: ecbf4b7520296ed580498dc66a72508b8a79da5126e1d6dc650a7087171288f9 + md5: 1247168fe4a0b8912e3336bccdbf98a5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libutf8proc >=2.11.3,<2.12.0a0 + size: 85969 + timestamp: 1768735071295 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.2-he9a06e4_0.conda sha256: e5ec6d2ad7eef538ddcb9ea62ad4346fde70a4736342c4ad87bd713641eb9808 md5: 80c07c68d2f6870250959dcc95b209d1 @@ -4599,19 +7110,21 @@ packages: purls: [] size: 109043 timestamp: 1730442108429 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 - md5: edb0dca6bc32e4f4789199455a1dbeb8 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 + md5: d87ff7921124eccd67248aa483c23fec depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 constrains: - - zlib 1.3.1 *_2 + - zlib 1.3.2 *_2 license: Zlib license_family: Other purls: [] - size: 60963 - timestamp: 1727963148474 + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 63629 + timestamp: 1774072609062 - conda: https://conda.anaconda.org/conda-forge/linux-64/lxml-6.0.2-py313h4a16004_1.conda sha256: 5451ff14e6fc0088ae093b35957f5665aa2838c7d4591c46112f5a9c6c674045 md5: 9640530427d53c076a1aadb00d8fcbf4 @@ -4629,6 +7142,23 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1608549 timestamp: 1759294892555 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-4.4.5-py313h28739b2_1.conda + sha256: cbc82f4fa7587376c038d2f0471a73efa7ade4439857b04a0cc839262f1de6e5 + md5: e69ad33075938ba81e43311da86b809c + depends: + - python + - lz4-c + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + - lz4-c >=1.10.0,<1.11.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/lz4?source=hash-mapping + run_exports: {} + size: 44861 + timestamp: 1765026393230 - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda sha256: 47326f811392a5fd3055f0f773036c392d26fdb32e4d8e7a8197eed951489346 md5: 9de5350a85c4a20c685259b889aa6393 @@ -4716,6 +7246,37 @@ packages: purls: [] size: 93471 timestamp: 1746450475308 +- conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.2.1-py313hc8edb43_1.conda + sha256: 6bc2690c67ba7354affa0498a68998fad8f66759c287706e7ff42c2242e14ccb + md5: edd7c461f72756bd3829f836e107c488 + depends: + - python + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/msgpack?source=compressed-mapping + run_exports: {} + size: 112798 + timestamp: 1782460772873 +- conda: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.7.1-py313h3dea7bd_0.conda + sha256: 3d277c0a9e237dc4c64f0b6414f3cf3e95806b2f5d03dec9c50f0ad0db5b7df1 + md5: 4f3e7bf5a9fc60a7d39047ba9e84c84c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/multidict?source=hash-mapping + run_exports: {} + size: 99374 + timestamp: 1771610936898 - conda: https://conda.anaconda.org/conda-forge/linux-64/muparser-2.3.5-h5888daf_0.conda sha256: 320dfc59a94cb9e3635bda71b9e62278b34aa2fdaea0caa6832ddb9b37e9ccd5 md5: ab3e3db511033340e75e7002e80ce8c0 @@ -4758,6 +7319,38 @@ packages: - pkg:pypi/netcdf4?source=hash-mapping size: 1117361 timestamp: 1756898546245 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda + sha256: fd2cbd8dfc006c72f45843672664a8e4b99b2f8137654eaae8c3d46dca776f63 + md5: 16c2a0e9c4a166e53632cfca4f68d020 + constrains: + - nlohmann_json-abi ==3.12.0 + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 136216 + timestamp: 1758194284857 +- conda: https://conda.anaconda.org/conda-forge/linux-64/numcodecs-0.16.5-py313h08cd8bf_0.conda + sha256: 7c35d46639f8638849535a22cb7ae1b7210121be0c7b053d8e2ab7ed485e6bff + md5: 0f394ef25fb81d1dec8ff4fa716f00bd + depends: + - __glibc >=2.17,<3.0.a0 + - deprecated + - libgcc >=14 + - libstdcxx >=14 + - msgpack-python + - numpy >=1.23,<3 + - numpy >=1.24 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - typing_extensions + license: MIT + license_family: MIT + purls: + - pkg:pypi/numcodecs?source=hash-mapping + run_exports: {} + size: 808201 + timestamp: 1764782369322 - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.3-py313hf6604e3_0.conda sha256: 88d45c6dbedabbc8ebb19555bb3d04b5e2846ae8a7dfc2c0204b54f5f6efaef7 md5: 3122d20dc438287e125fb5acff1df170 @@ -4779,6 +7372,23 @@ packages: - pkg:pypi/numpy?source=hash-mapping size: 8888776 timestamp: 1757505485589 +- conda: https://conda.anaconda.org/conda-forge/linux-64/obstore-0.11.0-py313h5c7d99a_0.conda + sha256: 32749ec2d55933ad5a3e767eb5b17f11581438a45e6df79bb7f7cb21882ed628 + md5: b0895bcc353f6268f5895421116eac3a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/obstore?source=hash-mapping + run_exports: {} + size: 4175718 + timestamp: 1782408521332 - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda sha256: 3900f9f2dbbf4129cf3ad6acf4e4b6f7101390b53843591c53b00f034343bc4d md5: 11b3379b191f63139e29c0d19dee24cd @@ -4794,9 +7404,9 @@ packages: purls: [] size: 355400 timestamp: 1758489294972 -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.4-h26f9b46_0.conda - sha256: e807f3bad09bdf4075dbb4168619e14b0c0360bacb2e12ef18641a834c8c5549 - md5: 14edad12b59ccbfa3910d42c72adc2a0 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda + sha256: d48f5c22b9897c01e4dff3680f1f57ceb02711ab9c62f74339b080419dfad34b + md5: 79dd2074b5cd5c5c6b2930514a11e22d depends: - __glibc >=2.17,<3.0.a0 - ca-certificates @@ -4804,8 +7414,92 @@ packages: license: Apache-2.0 license_family: Apache purls: [] - size: 3119624 - timestamp: 1759324353651 + run_exports: + weak: + - openssl >=3.6.3,<4.0a0 + size: 3159683 + timestamp: 1781069855778 +- conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h443056b_2.conda + sha256: 1f60ac42b217f05e0b5bf1b05f2ccac6d431911d842ce2f85b11d636fe4efbdf + md5: 4e185f3ae8e0b69c597b6a9a91a65328 + depends: + - tzdata + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libabseil >=20260526.0,<20260527.0a0 + - libabseil * cxx17* + - snappy >=1.2.2,<1.3.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libzlib >=1.3.2,<2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - orc >=2.3.0,<2.3.1.0a0 + size: 1453470 + timestamp: 1783199534282 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.3-py313hbfd7664_0.conda + sha256: a02d58327a57eb2f149c040b31c758fc6acea7c6aa5cb09b40b146eb6ed637d9 + md5: 70d4dd67877354f6912af31177cb1117 + depends: + - python + - numpy >=1.26.0 + - python-dateutil >=2.8.2 + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + - numpy >=1.23,<3 + constrains: + - adbc-driver-postgresql >=1.2.0 + - adbc-driver-sqlite >=1.2.0 + - beautifulsoup4 >=4.12.3 + - blosc >=1.21.3 + - bottleneck >=1.4.2 + - fastparquet >=2024.11.0 + - fsspec >=2024.10.0 + - gcsfs >=2024.10.0 + - html5lib >=1.1 + - hypothesis >=6.116.0 + - jinja2 >=3.1.5 + - lxml >=5.3.0 + - matplotlib >=3.9.3 + - numba >=0.60.0 + - numexpr >=2.10.2 + - odfpy >=1.4.1 + - openpyxl >=3.1.5 + - psycopg2 >=2.9.10 + - pyarrow >=13.0.0 + - pyiceberg >=0.8.1 + - pymysql >=1.1.1 + - pyqt5 >=5.15.9 + - pyreadstat >=1.2.8 + - pytables >=3.10.1 + - pytest >=8.3.4 + - pytest-xdist >=3.6.1 + - python-calamine >=0.3.0 + - pytz >=2024.2 + - pyxlsb >=1.0.10 + - qtpy >=2.4.2 + - scipy >=1.14.1 + - s3fs >=2024.10.0 + - sqlalchemy >=2.0.36 + - tabulate >=0.9.0 + - xarray >=2024.10.0 + - xlrd >=2.0.1 + - xlsxwriter >=3.2.0 + - zstandard >=0.23.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandas?source=compressed-mapping + run_exports: {} + size: 15001668 + timestamp: 1778602610159 - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda sha256: 3613774ad27e48503a3a6a9d72017087ea70f1426f6e5541dbdb59a3b626eaaf md5: 79f71230c069a287efe3a8614069ddf1 @@ -4894,6 +7588,39 @@ packages: purls: [] size: 3259440 timestamp: 1757929968903 +- conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda + sha256: 013669433eb447548f21c3c6b16b2ed64356f726b5f77c1b39d5ba17a8a4b8bc + md5: a83f6a2fdc079e643237887a37460668 + depends: + - __glibc >=2.17,<3.0.a0 + - libcurl >=8.10.1,<9.0a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + - zlib + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - prometheus-cpp >=1.3.0,<1.4.0a0 + size: 199544 + timestamp: 1730769112346 +- conda: https://conda.anaconda.org/conda-forge/linux-64/propcache-0.5.2-py313h3dea7bd_0.conda + sha256: 1fc8367d7ab0ffe70c3d9b96cf1cc953da1d8100899042337f554138a3d2fa69 + md5: 9f8899a9482c1737bf402d8218780df1 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/propcache?source=hash-mapping + run_exports: {} + size: 50526 + timestamp: 1780037863138 - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.1.0-py313h07c4f96_0.conda sha256: 6058abf3d6b8ca24fbbe16b56f5a333f7ef55475d3e59ce3ad6f20e46ca49102 md5: f25cdf145885936fe458452d73d991dc @@ -4919,6 +7646,45 @@ packages: purls: [] size: 8252 timestamp: 1726802366959 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-24.0.0-py313h78bf25f_0.conda + sha256: 096c8948fc4a0436dd1e32294f57be135819909a112978833bc749d43b14dd33 + md5: c93f1ad82cb397a98d6cadb4a07572af + depends: + - libarrow-acero 24.0.0.* + - libarrow-dataset 24.0.0.* + - libarrow-substrait 24.0.0.* + - libparquet 24.0.0.* + - pyarrow-core 24.0.0 *_0_* + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 26789 + timestamp: 1776927995964 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-24.0.0-py313h98bfbea_0_cpu.conda + sha256: feeb4ae83dffa8af4ee131279925e22e6969958f14342015f94d75cc196ef689 + md5: 4c7cc18a6d6d18557c18069af469a96c + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 24.0.0.* *cpu + - libarrow-compute 24.0.0.* *cpu + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.2,<2.0a0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + constrains: + - numpy >=1.23,<3 + - apache-arrow-proc * cpu + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/pyarrow?source=hash-mapping + run_exports: {} + size: 5967733 + timestamp: 1776927971776 - conda: https://conda.anaconda.org/conda-forge/linux-64/pybtex-docutils-1.0.3-py313h78bf25f_3.conda sha256: 5ff7c1cd2bdf00dbedaf27edd5011901776c10eeb642ca68ff9153ea33ea40dd md5: 9b1fe6ef70bec3ed76ef009a577fda60 @@ -5021,6 +7787,20 @@ packages: purls: [] size: 552937 timestamp: 1720813982144 +- conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h94463f1_2.conda + sha256: 7279908454f0c87b84ebc4aec6924f02f41a105d88420fb35dfaf5ecd976e0f5 + md5: d83f2ee212d217a6d745a00e5be84671 + depends: + - libre2-11 2025.11.05 h60473fc_2 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libre2-11 >=2025.11.5 + - re2 + size: 27397 + timestamp: 1781312576962 - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c md5: 283b96675859b20a825f8fa30f311446 @@ -5048,6 +7828,21 @@ packages: - pkg:pypi/rpds-py?source=hash-mapping size: 389189 timestamp: 1756737629819 +- conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.7.5-h7e3ee7f_1.conda + sha256: 7369ac21f3561e2203f5b1600ef0671270057380783ca4b9e15f679ba25db575 + md5: fa1c00d999e83ec20173c9775f71a1f1 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - openssl >=3.5.7,<4.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - s2n >=1.7.5,<1.7.6.0a0 + size: 392607 + timestamp: 1783164766248 - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.16.2-py313h11c21cd_0.conda sha256: 3c26c268a4db6ff62103f6b245f650d3cd7478240335405c07e05bae85af4d36 md5: 85a80978a04be9c290b8fe6d9bccff1c @@ -5181,6 +7976,21 @@ packages: purls: [] size: 330474 timestamp: 1751817998141 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-1.17.3-py313h07c4f96_1.conda + sha256: 3688598866224e3fbeed8a74f12fd0a3c19dadcb931ce778bdc6cc2e04621b3b + md5: c2662497e9a9ff2153753682f53989c9 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/wrapt?source=hash-mapping + run_exports: {} + size: 64865 + timestamp: 1756851811052 - conda: https://conda.anaconda.org/conda-forge/linux-64/xerces-c-3.2.5-h988505b_2.conda sha256: 339ab0ff05170a295e59133cd0fa9a9c4ba32b6941c8a2a73484cc13f81e248a md5: 9dda9667feba914e0e80b95b82f7402b @@ -5433,33 +8243,55 @@ packages: purls: [] size: 85189 timestamp: 1753484064210 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h387f397_9.conda - sha256: 47cfe31255b91b4a6fa0e9dbaf26baa60ac97e033402dbc8b90ba5fee5ffe184 - md5: 8035e5b54c08429354d5d64027041cad +- conda: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.24.2-py313h3dea7bd_0.conda + sha256: 5241c3132540458b69bc633fcb209887cc656190f176138c3391bea17e366910 + md5: 48b599c76bdb7e0c367a86b11c24dd70 + depends: + - __glibc >=2.17,<3.0.a0 + - idna >=2.0 + - libgcc >=14 + - multidict >=4.0 + - propcache >=0.2.1 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/yarl?source=compressed-mapping + run_exports: {} + size: 155105 + timestamp: 1779246217985 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h09e67af_11.conda + sha256: dc9f28dedcb5f35a127fad2d847674d2833369dd616d294e423b8997df31d8a8 + md5: 96b08867e21d4694fa5c2c226e6581b0 depends: - - libstdcxx >=14 - libgcc >=14 - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - libsodium >=1.0.20,<1.0.21.0a0 - - krb5 >=1.21.3,<1.22.0a0 + - libstdcxx >=14 + - krb5 >=1.22.2,<1.23.0a0 + - libsodium >=1.0.22,<1.0.23.0a0 license: MPL-2.0 license_family: MOZILLA purls: [] - size: 310648 - timestamp: 1757370847287 -- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda - sha256: 5d7c0e5f0005f74112a34a7425179f4eb6e73c92f5d109e6af4ddeca407c92ab - md5: c9f075ab2f33b3bbee9e62d4ad0a6cd8 + run_exports: + weak: + - zeromq >=4.3.5,<4.4.0a0 + size: 311184 + timestamp: 1779123989774 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda + sha256: 245c9ee8d688e23661b95e3c6dd7272ca936fabc03d423cdb3cdee1bbcf9f2f2 + md5: c2a01a08fc991620a74b32420e97868a depends: - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - - libzlib 1.3.1 hb9d3cd8_2 + - libzlib 1.3.2 h25fd6f3_2 license: Zlib license_family: Other purls: [] - size: 92286 - timestamp: 1727963153079 + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 95931 + timestamp: 1774072620848 - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_0.conda sha256: 9d79d176afe50361cc3fd4366bedff20852dbea1e5b03f358b55f12aca22d60d md5: 1fe43bd1fc86e22ad3eb0edec637f8a2 @@ -5513,6 +8345,65 @@ packages: purls: [] size: 631452 timestamp: 1758743294412 +- conda: https://conda.anaconda.org/conda-forge/noarch/aiobotocore-2.25.2-pyhcf101f3_0.conda + sha256: 922fb146148449c6bc374a37fa9edb89b3af1c385392391339206d3ab3d571a3 + md5: 6e90a60dbb939d24a6295e19377cf0e6 + depends: + - python >=3.10 + - aiohttp >=3.9.2,<4.0.0 + - aioitertools >=0.5.1,<1.0.0 + - botocore >=1.40.46,<1.40.71 + - python-dateutil >=2.1,<3.0.0 + - jmespath >=0.7.1,<2.0.0 + - multidict >=6.0.0,<7.0.0 + - wrapt >=1.10.10,<2.0.0 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/aiobotocore?source=hash-mapping + run_exports: {} + size: 80900 + timestamp: 1762893423043 +- conda: https://conda.anaconda.org/conda-forge/noarch/aiohappyeyeballs-2.7.1-pyhd8ed1ab_0.conda + sha256: 5ef589e5fd3736439781a9d48e68ce1e723b7081a471c02169908198eba4f448 + md5: e51e09bf3b91c62b7461a9f94e92c2c9 + depends: + - python >=3.10 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/aiohappyeyeballs?source=compressed-mapping + run_exports: {} + size: 24690 + timestamp: 1782986823268 +- conda: https://conda.anaconda.org/conda-forge/noarch/aioitertools-0.13.0-pyhd8ed1ab_0.conda + sha256: 41bc8d85274c5badabe6c333cdd2e77e9c6bc0fb64251211988a71e1fd83486b + md5: 65d5134ff98cb3727022a4f23993a2e6 + depends: + - python >=3.10 + - typing_extensions >=4.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/aioitertools?source=hash-mapping + run_exports: {} + size: 25450 + timestamp: 1768757675539 +- conda: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.4.0-pyhd8ed1ab_0.conda + sha256: 8dc149a6828d19bf104ea96382a9d04dae185d4a03cc6beb1bc7b84c428e3ca2 + md5: 421a865222cd0c9d83ff08bc78bf3a61 + depends: + - frozenlist >=1.1.0 + - python >=3.9 + - typing_extensions >=4.2 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/aiosignal?source=hash-mapping + run_exports: {} + size: 13688 + timestamp: 1751626573984 - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda sha256: 6c4456a138919dae9edd3ac1a74b6fbe5fd66c05675f54df2f8ab8c8d0cc6cea md5: 1fd9696649f65fd6611fcdb4ffec738a @@ -5668,6 +8559,29 @@ packages: purls: [] size: 4213 timestamp: 1737382993425 +- conda: https://conda.anaconda.org/conda-forge/noarch/bokeh-3.9.1-pyhd8ed1ab_0.conda + sha256: b3d3b93a17fa678e81cd5ff5309fe64c7feb65c71cb134f14e4fe2113dc0c8d5 + md5: 123fcfe0df4b6fc21804538e59cdaafe + depends: + - contourpy >=1.2 + - jinja2 >=2.9 + - narwhals >=1.13 + - numpy >=1.16 + - packaging >=16.8 + - pillow >=7.1.0 + - python >=3.10 + - pyyaml >=3.10 + - tornado >=6.2 + - xyzservices >=2021.09.1 + constrains: + - panel >=1.8.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/bokeh?source=hash-mapping + run_exports: {} + size: 4526315 + timestamp: 1781002115296 - conda: https://conda.anaconda.org/conda-forge/noarch/boto3-1.40.45-pyhd8ed1ab_0.conda sha256: e0d16c5bb07c9b032bba901a7671130a18d5e186a9c102863e966bf01486419a md5: 64ea112d8294850b29c06dae7f1fd19d @@ -5682,9 +8596,9 @@ packages: - pkg:pypi/boto3?source=hash-mapping size: 83940 timestamp: 1759584285379 -- conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.45-pyhd8ed1ab_0.conda - sha256: 710f6e213a18dc929cd835067747c8e51b4ee85911c2cda32802d2313aba2b64 - md5: 683f05d53133e5aa98006f49163e5d1b +- conda: https://conda.anaconda.org/conda-forge/noarch/botocore-1.40.70-pyhd8ed1ab_0.conda + sha256: 92e3b65d162600eec4c858a870e2b7593886d837c965ca51bf8bd1ed0e6f1e27 + md5: 280a8a31bface0a6b1cf49ea85004128 depends: - jmespath >=0.7.1,<2.0.0 - python >=3.10 @@ -5694,8 +8608,9 @@ packages: license_family: Apache purls: - pkg:pypi/botocore?source=hash-mapping - size: 8052555 - timestamp: 1759565290212 + run_exports: {} + size: 8150945 + timestamp: 1762813779810 - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-h4c7d964_0.conda sha256: bfb7f9f242f441fdcd80f1199edd2ecf09acea0f2bcef6f07d7cbb1a8131a345 md5: e54200a1cd1fe33d61c9df8d3b00b743 @@ -5782,6 +8697,19 @@ packages: - pkg:pypi/click?source=compressed-mapping size: 92148 timestamp: 1758270588199 +- conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + sha256: 4c287c2721d8a34c94928be8fe0e9a85754e90189dd4384a31b1806856b50a67 + md5: 61b8078a0905b12529abc622406cb62c + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cloudpickle?source=hash-mapping + run_exports: {} + size: 27353 + timestamp: 1765303462831 - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 md5: 962b9857ee8e7018c22f2776ffa0b2d7 @@ -5839,6 +8767,50 @@ packages: - pkg:pypi/cycler?source=hash-mapping size: 13399 timestamp: 1733332563512 +- conda: https://conda.anaconda.org/conda-forge/noarch/dask-2026.7.0-pyhc364b38_0.conda + sha256: f188b9fcb30fa422f4fa81baa7540a9e9c677bdd274a7bd513ef347e6c4f4926 + md5: 8763ed8bb5b9b2d184d7e7f7a670550b + depends: + - python >=3.10 + - dask-core >=2026.7.0,<2026.7.1.0a0 + - distributed >=2026.7.0,<2026.7.1.0a0 + - cytoolz >=0.11.2 + - lz4 >=4.3.2 + - numpy >=1.26 + - pandas >=2.0 + - bokeh >=3.1.0 + - jinja2 >=2.10.3 + - pyarrow >=16.0 + - python + constrains: + - openssl !=1.1.1e + license: BSD-3-Clause + purls: + - pkg:pypi/dask?source=compressed-mapping + run_exports: {} + size: 10392 + timestamp: 1783521976911 +- conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.7.0-pyhc364b38_0.conda + sha256: 8d400881cb6251bb3fe47888a0348740e639031b7deef323a4559fa3072433bd + md5: ca2ed20c7b3102d617bf5eef25fc9ee9 + depends: + - python >=3.10 + - click >=8.1 + - cloudpickle >=3.0.0 + - fsspec >=2021.9.0 + - packaging >=20.0 + - partd >=1.4.0 + - pyyaml >=5.4.1 + - toolz >=0.12.0 + - importlib-metadata >=4.13.0 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/dask?source=compressed-mapping + run_exports: {} + size: 1074754 + timestamp: 1783370469246 - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda sha256: c17c6b9937c08ad63cb20a26f403a3234088e57d4455600974a0ce865cb14017 md5: 9ce473d1d1be1cc3810856a48b3fab32 @@ -5861,6 +8833,48 @@ packages: - pkg:pypi/defusedxml?source=hash-mapping size: 24062 timestamp: 1615232388757 +- conda: https://conda.anaconda.org/conda-forge/noarch/deprecated-1.3.1-pyhd8ed1ab_1.conda + sha256: 7d57a7b8266043ffb99d092ebc25e89a0a2490bed4146b9432c83c2c476fa94d + md5: 5498feb783ab29db6ca8845f68fa0f03 + depends: + - python >=3.10 + - wrapt <3,>=1.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/deprecated?source=hash-mapping + run_exports: {} + size: 15896 + timestamp: 1768934186726 +- conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.7.0-pyhc364b38_0.conda + sha256: a41e2f18f8e76652e1e78ffdc10dc3e33c8ac1a38f76c9e3b02a3fd480169cb6 + md5: 603c2c6baa27a6734cb960123766baaf + depends: + - python >=3.10 + - click >=8.0 + - cloudpickle >=3.0.0 + - cytoolz >=0.12.0 + - dask-core >=2026.7.0,<2026.7.1.0a0 + - jinja2 >=2.10.3 + - locket >=1.0.0 + - msgpack-python >=1.0.2 + - packaging >=20.0 + - psutil >=5.8.0 + - pyyaml >=5.4.1 + - sortedcontainers >=2.0.5 + - tblib >=1.6.0 + - toolz >=0.12.0 + - tornado >=6.2.0 + - zict >=3.0.0 + - python + constrains: + - openssl !=1.1.1e + license: BSD-3-Clause + purls: + - pkg:pypi/distributed?source=hash-mapping + run_exports: {} + size: 843030 + timestamp: 1783433971453 - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda sha256: fa5966bb1718bbf6967a85075e30e4547901410cc7cb7b16daf68942e9a94823 md5: 24c1ca34138ee57de72a943237cde4cc @@ -5871,6 +8885,19 @@ packages: - pkg:pypi/docutils?source=hash-mapping size: 402700 timestamp: 1733217860944 +- conda: https://conda.anaconda.org/conda-forge/noarch/donfig-0.8.1.post1-pyhd8ed1ab_1.conda + sha256: d58e97d418f71703e822c422af5b9c431e3621a0ecdc8b0334c1ca33e076dfe7 + md5: c56a7fa5597ad78b62e1f5d21f7f8b8f + depends: + - python >=3.9 + - pyyaml + license: MIT + license_family: MIT + purls: + - pkg:pypi/donfig?source=hash-mapping + run_exports: {} + size: 22491 + timestamp: 1734368817583 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda sha256: ce61f4f99401a4bd455b89909153b40b9c823276aefcbb06f2044618696009ca md5: 72e42d28960d875c7654614f8b50939a @@ -5985,6 +9012,18 @@ packages: - pkg:pypi/fqdn?source=hash-mapping size: 16705 timestamp: 1733327494780 +- conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.6.0-pyhd8ed1ab_0.conda + sha256: fe0156e6d658be3531aad2a99e42e8ad1ee23c69837d469c44c1b6010373913d + md5: 7d7e6c826ba0743fc491ebee0e7b899c + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/fsspec?source=compressed-mapping + run_exports: {} + size: 149709 + timestamp: 1781615868173 - conda: https://conda.anaconda.org/conda-forge/noarch/future-1.0.0-pyhd8ed1ab_2.conda sha256: 45dfd037889b7075c5eb46394f93172de0be0b1624c7f802dd3ecc94b814d8e0 md5: 1054c53c95d85e35b88143a3eda66373 @@ -6655,6 +9694,18 @@ packages: - pkg:pypi/latexcodec?source=hash-mapping size: 18212 timestamp: 1592937373647 +- conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + sha256: 9afe0b5cfa418e8bdb30d8917c5a6cec10372b037924916f1f85b9f4899a67a6 + md5: 91e27ef3d05cc772ce627e51cff111c4 + depends: + - python >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/locket?source=hash-mapping + run_exports: {} + size: 8250 + timestamp: 1650660473123 - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda sha256: 0fbacdfb31e55964152b24d5567e9a9996e1e7902fb08eb7d91b5fd6ce60803a md5: fee3164ac23dfca50cfcc8b85ddefb81 @@ -6776,6 +9827,19 @@ packages: - pkg:pypi/myst-parser?source=hash-mapping size: 73074 timestamp: 1739381945342 +- conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.23.0-pyhcf101f3_0.conda + sha256: e7dbd69d1de038b0411636c50bd9a935d05ca37269c4a155a78571afbaa7994a + md5: a537eb35988a6f2b1ceda6cce83e2f0e + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/narwhals?source=compressed-mapping + run_exports: {} + size: 288381 + timestamp: 1782924928916 - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.2-pyhd8ed1ab_0.conda sha256: a20cff739d66c2f89f413e4ba4c6f6b59c50d5c30b5f0d840c13e8c9c2df9135 md5: 6bb0d77277061742744176ab555b723c @@ -6936,6 +10000,20 @@ packages: - pkg:pypi/parso?source=hash-mapping size: 81562 timestamp: 1755974222274 +- conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + sha256: 472fc587c63ec4f6eba0cc0b06008a6371e0a08a5986de3cf4e8024a47b4fe6c + md5: 0badf9c54e24cecfb0ad2f99d680c163 + depends: + - locket + - python >=3.9 + - toolz + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/partd?source=hash-mapping + run_exports: {} + size: 20884 + timestamp: 1715026639309 - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda sha256: 202af1de83b585d36445dc1fda94266697341994d1a3328fabde4989e1b3d07a md5: d0d408b1f18883a944376da5cf8101ea @@ -7231,6 +10309,18 @@ packages: - pkg:pypi/python-json-logger?source=hash-mapping size: 13383 timestamp: 1677079727691 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda + sha256: e943f9c15a6bdba2e1b9f423ab913b3f6b02197b0ef9f8e6b7464d78b59965b9 + md5: f6ad7450fc21e00ecc23812baed6d2e4 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/tzdata?source=hash-mapping + run_exports: {} + size: 146639 + timestamp: 1777068997932 - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda build_number: 8 sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 @@ -7331,6 +10421,21 @@ packages: - pkg:pypi/roman-numerals-py?source=hash-mapping size: 13348 timestamp: 1740240332327 +- conda: https://conda.anaconda.org/conda-forge/noarch/s3fs-2026.6.0-pyhd8ed1ab_0.conda + sha256: 24b60ce176c21de548239a7b256c5d0a39190108edbc53b10229d46bd74b2ddd + md5: a472598199c08f010f2e90b8e7886ef0 + depends: + - aiobotocore >=2.19.0,<4.0.0 + - aiohttp + - fsspec >=2026.6.0,<2026.6.1 + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/s3fs?source=hash-mapping + run_exports: {} + size: 36232 + timestamp: 1781621673975 - conda: https://conda.anaconda.org/conda-forge/noarch/s3transfer-0.14.0-pyhd8ed1ab_0.conda sha256: e401e1d3effaaaaefb388df7488b309f737bc3a6fe69be0ff8b826093b5ecb2f md5: 4e3089ce93822a25408c480dd53561b6 @@ -7426,6 +10531,18 @@ packages: - pkg:pypi/snowballstemmer?source=hash-mapping size: 73009 timestamp: 1747749529809 +- conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + sha256: d1e3e06b5cf26093047e63c8cc77b70d970411c5cbc0cb1fad461a8a8df599f7 + md5: 0401a17ae845fa72c7210e206ec5647d + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/sortedcontainers?source=hash-mapping + run_exports: {} + size: 28657 + timestamp: 1738440459037 - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8-pyhd8ed1ab_0.conda sha256: c978576cf9366ba576349b93be1cfd9311c00537622a2f9e14ba2b90c97cae9c md5: 18c019ccf43769d211f2cf78e9ad46c2 @@ -7629,6 +10746,19 @@ packages: - pkg:pypi/tabulate?source=hash-mapping size: 37554 timestamp: 1733589854804 +- conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda + sha256: 6b549360f687ee4d11bf85a6d6a276a30f9333df1857adb0fe785f0f8e9bcd60 + md5: f88bb644823094f436792f80fba3207e + depends: + - python >=3.10 + - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/tblib?source=hash-mapping + run_exports: {} + size: 19397 + timestamp: 1762956379123 - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda sha256: b300557c0382478cf661ddb520263508e4b3b5871b471410450ef2846e8c352c md5: efba281bbdae5f6b0a1d53c6d4a97c93 @@ -7695,6 +10825,18 @@ packages: - pkg:pypi/tomli?source=compressed-mapping size: 21238 timestamp: 1753796677376 +- conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + sha256: 4e379e1c18befb134247f56021fdf18e112fb35e64dd1691858b0a0f3bea9a45 + md5: c07a6153f8306e45794774cf9b13bd32 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/toolz?source=hash-mapping + run_exports: {} + size: 53978 + timestamp: 1760707830681 - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda sha256: f39a5620c6e8e9e98357507262a7869de2ae8cc07da8b7f84e517c9fd6c2b959 md5: 019a7385be9af33791c989871317e1ed @@ -7848,47 +10990,408 @@ packages: - pkg:pypi/win-inet-pton?source=hash-mapping size: 9555 timestamp: 1733130678956 +- conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda + sha256: 663ea9b00d68c2da309114923924686ab6d3f59ef1b196c5029ba16799e7bb07 + md5: 4487b9c371d0161d54b5c7bbd890c0fc + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/xyzservices?source=hash-mapping + run_exports: {} + size: 51732 + timestamp: 1774900074457 +- conda: https://conda.anaconda.org/conda-forge/noarch/zarr-3.2.1-pyhc364b38_0.conda + sha256: 88716d633ac7fdc896ce7aa00efdfc91df6363d51bebabfb60d34399c023a3d0 + md5: b787cd1f01e24b161261438a5589df51 + depends: + - python >=3.12 + - packaging >=22.0 + - numpy >=2 + - numcodecs >=0.14 + - typing_extensions >=4.13 + - donfig >=0.8 + - google-crc32c >=1.5 + - python + constrains: + - fsspec >=2023.10.0 + - obstore >=0.5.1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/zarr?source=hash-mapping + run_exports: {} + size: 367721 + timestamp: 1777989189792 +- conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda + sha256: 5488542dceeb9f2874e726646548ecc5608060934d6f9ceaa7c6a48c61f9cc8d + md5: e52c2ef711ccf31bb7f70ca87d144b9e + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/zict?source=hash-mapping + run_exports: {} + size: 36341 + timestamp: 1733261642963 - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda sha256: 7560d21e1b021fd40b65bfb72f67945a3fcb83d78ad7ccf37b8b3165ec3b68ad md5: df5e78d904988eb55042c0c97446079f depends: - - python >=3.9 + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/zipp?source=hash-mapping + size: 22963 + timestamp: 1749421737203 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aiohttp-3.14.1-py313h53c0e3e_0.conda + sha256: 79113895281a26605daf3f0776bb60053bf1de69dc62bd42c5f1afbc908c41df + md5: e068a8116541a671c61dcc7de46a5c80 + depends: + - __osx >=11.0 + - aiohappyeyeballs >=2.5.0 + - aiosignal >=1.4.0 + - attrs >=17.3.0 + - frozenlist >=1.1.1 + - multidict >=4.5,<7.0 + - propcache >=0.2.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + - typing_extensions >=4.4 + - yarl >=1.17.0,<2.0 + license: MIT AND Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/aiohttp?source=hash-mapping + run_exports: {} + size: 1059799 + timestamp: 1780913969743 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py313h6535dbc_1.conda + sha256: 23860322b12b1d4b365e9dca09a1b74449ee17b1701ee99cf7739dd5f4500b56 + md5: d15e31a7352fc18a07745df32e60ba7c + depends: + - __osx >=11.0 + - cffi >=1.0.1 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=compressed-mapping + size: 34013 + timestamp: 1759487134505 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda + sha256: b0747f9b1bc03d1932b4d8c586f39a35ac97e7e72fe6e63f2b2a2472d466f3c1 + md5: 57301986d02d30d6805fdce6c99074ee + depends: + - __osx >=11.0 + - libcxx >=16 + - libglib >=2.80.0,<3.0a0 + - libintl >=0.22.5,<1.0a0 + constrains: + - atk-1.0 2.38.0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 347530 + timestamp: 1713896411580 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.10.3-hc187ab6_4.conda + sha256: 78cd60168af96031b90994847956df311956b1c0b81dfabe630aae044bd1984d + md5: f64b4da8a239636e185fff25d7c797bb + depends: + - __osx >=11.0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-sdkutils >=0.2.7,<0.2.8.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-c-auth >=0.10.3,<0.10.4.0a0 + size: 116992 + timestamp: 1783461498328 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.9.14-h3623c0d_3.conda + sha256: a8bb9264c3ee5b05da987b81559b7312915febc0094fd5c4254caf6997b2bad0 + md5: d0312c791fa1ab9c81d15ea4d1a9ffbe + depends: + - __osx >=11.0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - aws-c-cal >=0.9.14,<0.9.15.0a0 + size: 45877 + timestamp: 1783165675964 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.14.1-h84a0fba_0.conda + sha256: 328ab8b74fac2b043d7eda06f74386212c2a30ee782dbcf5a4552289872fcb6d + md5: 8369bbde066237e9d9fbcf2092f6032d + depends: + - __osx >=11.0 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - aws-c-common >=0.14.1,<0.14.2.0a0 + size: 227180 + timestamp: 1782344247239 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.3.2-h35327cf_3.conda + sha256: 072644d0d1a5d68dab7c2d3a0ebf6e5014fd2b9de1bd850a0346a911a4649948 + md5: 40ec5893de3b45201d78154aa1c38a5d + depends: + - __osx >=11.0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-compression >=0.3.2,<0.3.3.0a0 + size: 21753 + timestamp: 1783166800084 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.7.1-h1e7fc79_3.conda + sha256: 60f4829c92c504153a4362b115a8b91bff76e79ccb4299df84a42d50b1d76874 + md5: 2f3e740464071503809847ca7c6773f5 + depends: + - __osx >=11.0 + - libcxx >=19 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-checksums >=0.2.10,<0.2.11.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + size: 54064 + timestamp: 1783193034353 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.11.0-he9547b6_3.conda + sha256: 984ac8de143c7bb5f4927f2e3d553774fcf87b4cb54b3f4e2bfbc1f771544ce7 + md5: 1a1e9d8298322cf45abd50e9971d1194 + depends: + - __osx >=11.0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-compression >=0.3.2,<0.3.3.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-http >=0.11.0,<0.11.1.0a0 + size: 177541 + timestamp: 1783192927726 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.26.3-h63484d5_6.conda + sha256: 18e4430614994afb1928813c95c101c0a6ec036a69ea7a95f858889a70d41feb + md5: 2315fe089cc47268f15504588be66d17 + depends: + - __osx >=11.0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-io >=0.26.3,<0.26.4.0a0 + size: 177252 + timestamp: 1783180129459 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.16.0-h73b2f1b_1.conda + sha256: 3e3f4a6148e5ab30fcb513eaad03913be29d1b5d449e17caf6bb3ad1b5f268ce + md5: 6fa39f3ef470a015d28e15d45849ff96 + depends: + - __osx >=11.0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-mqtt >=0.16.0,<0.16.1.0a0 + size: 158238 + timestamp: 1783205920117 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.12.6-hc78c590_1.conda + sha256: 3a497361048424e79d00945869ad43faad2cc1fa3dd6ca007fd1d7ad8797209f + md5: 2ea7c11aa22eb42074ec0b6ab3b20a6b + depends: + - __osx >=11.0 + - aws-c-auth >=0.10.3,<0.10.4.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-checksums >=0.2.10,<0.2.11.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-c-s3 >=0.12.6,<0.12.7.0a0 + size: 132874 + timestamp: 1783472097405 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.2.7-h35327cf_1.conda + sha256: 5af8ed494a989e9a4c8672ef9d8ce32723a189f346265dd920c9c853be96d66d + md5: de8a6ba9bbcc4476afeeee39bdcaff70 + depends: + - __osx >=11.0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-sdkutils >=0.2.7,<0.2.8.0a0 + size: 60782 + timestamp: 1783172649358 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.2.10-h35327cf_3.conda + sha256: ad993afb36281fa302d8c6af2ff167b1c250c251a41fa5498b6c21d812a70f8a + md5: 9f07664ca02c9c86a5774a4511f29123 + depends: + - __osx >=11.0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-checksums >=0.2.10,<0.2.11.0a0 + size: 92209 + timestamp: 1783166982759 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.40.1-h9ead58b_2.conda + sha256: 48b27a74da15b452c36ad8dd4ad9e513489b2f85b31cc0e17cc7b4ed660530f2 + md5: 19115c36cfc244d4300ee047115e3dce + depends: + - libcxx >=19 + - __osx >=11.0 + - aws-c-s3 >=0.12.6,<0.12.7.0a0 + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-mqtt >=0.16.0,<0.16.1.0a0 + - aws-c-sdkutils >=0.2.7,<0.2.8.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + - aws-c-auth >=0.10.3,<0.10.4.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + size: 275502 + timestamp: 1783483139609 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.833-h918993c_8.conda + sha256: e3fc94f14817503e5017c3c951ce1ea8a33119f741cd08318332efa51b67fdc5 + md5: 6351c229da097a9a5f537197c2b90278 + depends: + - libcxx >=19 + - __osx >=11.0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + - libzlib >=1.3.2,<2.0a0 + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + - libcurl >=8.21.0,<9.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-sdk-cpp >=1.11.833,<1.11.834.0a0 + size: 2834501 + timestamp: 1783536859562 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.16.3-he5ae378_0.conda + sha256: 9645073c4682ad3a334a2b06c5fc0ddc57fc9f0f537cc3123053634c92c28625 + md5: 4f42d997f42a8d34ff74cb677cf0c828 + depends: + - __osx >=11.0 + - libcurl >=8.19.0,<9.0a0 + - libcxx >=19 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + size: 290309 + timestamp: 1775239330289 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.13.3-h05177fb_2.conda + sha256: d950fb513311a15cd4ae663cdacb2c035122f609a9c790973661b38e0882e40e + md5: e1701f6b2ce6f47aeb6cbfab132403d8 + depends: + - __osx >=11.0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - libcxx >=19 + - openssl >=3.5.7,<4.0a0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-identity-cpp >=1.13.3,<1.13.4.0a0 + size: 168032 + timestamp: 1781268747743 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.18.0-h409340b_1.conda + sha256: 3f096d7cd6fd7788d57887c3d3e48be1ebc5c3a971666ddc0dd8a3493a5b7cb5 + md5: 0948246cb8e514e4ae1cfe7dbb4046c7 + depends: + - __osx >=11.0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + - libcxx >=19 license: MIT license_family: MIT - purls: - - pkg:pypi/zipp?source=hash-mapping - size: 22963 - timestamp: 1749421737203 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py313h6535dbc_1.conda - sha256: 23860322b12b1d4b365e9dca09a1b74449ee17b1701ee99cf7739dd5f4500b56 - md5: d15e31a7352fc18a07745df32e60ba7c + purls: [] + run_exports: + weak: + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + size: 436462 + timestamp: 1781284116423 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-common-cpp-12.14.0-h7cba3ab_1.conda + sha256: 9a1b6e5284f1f242330dfa1e21408ffd823a76df424108d8c319c2a46df61dba + md5: 2af5d1b5365c4a3de8ed8ec8438fe6ec depends: - __osx >=11.0 - - cffi >=1.0.1 - - python >=3.13,<3.14.0a0 - - python >=3.13,<3.14.0a0 *_cp313 - - python_abi 3.13.* *_cp313 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - libcxx >=19 + - libxml2 + - libxml2-16 >=2.14.6 + - openssl >=3.5.7,<4.0a0 license: MIT license_family: MIT - purls: - - pkg:pypi/argon2-cffi-bindings?source=compressed-mapping - size: 34013 - timestamp: 1759487134505 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda - sha256: b0747f9b1bc03d1932b4d8c586f39a35ac97e7e72fe6e63f2b2a2472d466f3c1 - md5: 57301986d02d30d6805fdce6c99074ee + purls: [] + run_exports: + weak: + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + size: 128710 + timestamp: 1781268693348 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-files-datalake-cpp-12.16.0-h16bb3af_1.conda + sha256: fb210d8d068df72a68c5fa655cf1f816792e278da3041c485478e7924d6e80eb + md5: 06ba52b8a66fd041f4910b547e3f3da1 depends: - __osx >=11.0 - - libcxx >=16 - - libglib >=2.80.0,<3.0a0 - - libintl >=0.22.5,<1.0a0 - constrains: - - atk-1.0 2.38.0 - license: LGPL-2.0-or-later - license_family: LGPL + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + - libcxx >=19 + license: MIT + license_family: MIT purls: [] - size: 347530 - timestamp: 1713896411580 + run_exports: + weak: + - azure-storage-files-datalake-cpp >=12.16.0,<12.16.1.0a0 + size: 206698 + timestamp: 1781541197750 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda sha256: c3fe902114b9a3ac837e1a32408cc2142c147ec054c1038d37aec6814343f48a md5: 925acfb50a750aa178f7a0aced77f351 @@ -7904,34 +11407,40 @@ packages: purls: [] size: 33602 timestamp: 1733513285902 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.1.0-h6caf38d_4.conda - sha256: 8aa8ee52b95fdc3ef09d476cbfa30df722809b16e6dca4a4f80e581012035b7b - md5: ce8659623cea44cc812bc0bfae4041c5 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-h7d5ae5b_1.conda + sha256: 422ac5c91f8ef07017c594d9135b7ae068157393d2a119b1908c7e350938579d + md5: 48ece20aa479be6ac9a284772827d00c depends: - __osx >=11.0 - - brotli-bin 1.1.0 h6caf38d_4 - - libbrotlidec 1.1.0 h6caf38d_4 - - libbrotlienc 1.1.0 h6caf38d_4 + - brotli-bin 1.2.0 hc919400_1 + - libbrotlidec 1.2.0 hc919400_1 + - libbrotlienc 1.2.0 hc919400_1 license: MIT license_family: MIT purls: [] - size: 20003 - timestamp: 1756599758165 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.1.0-h6caf38d_4.conda - sha256: e57d402b02c9287b7c02d9947d7b7b55a4f7d73341c210c233f6b388d4641e08 - md5: ab57f389f304c4d2eb86d8ae46d219c3 + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 20237 + timestamp: 1764018058424 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-hc919400_1.conda + sha256: e2d142052a83ff2e8eab3fe68b9079cad80d109696dc063a3f92275802341640 + md5: 377d015c103ad7f3371be1777f8b584c depends: - __osx >=11.0 - - libbrotlidec 1.1.0 h6caf38d_4 - - libbrotlienc 1.1.0 h6caf38d_4 + - libbrotlidec 1.2.0 hc919400_1 + - libbrotlienc 1.2.0 hc919400_1 license: MIT license_family: MIT purls: [] - size: 17373 - timestamp: 1756599741779 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py313hb4b7877_4.conda - sha256: a6402a7186ace5c3eb21ed4ce50eda3592c44ce38ab4e9a7ddd57d72b1e61fb3 - md5: 9518cd948fc334d66119c16a2106a959 + run_exports: {} + size: 18628 + timestamp: 1764018033635 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py313hde1f3bb_1.conda + sha256: 2e21dccccd68bedd483300f9ab87a425645f6776e6e578e10e0dd98c946e1be9 + md5: b03732afa9f4f54634d94eb920dfb308 depends: - __osx >=11.0 - libcxx >=19 @@ -7939,13 +11448,14 @@ packages: - python >=3.13,<3.14.0a0 *_cp313 - python_abi 3.13.* *_cp313 constrains: - - libbrotlicommon 1.1.0 h6caf38d_4 + - libbrotlicommon 1.2.0 hc919400_1 license: MIT license_family: MIT purls: - pkg:pypi/brotli?source=hash-mapping - size: 341104 - timestamp: 1756600117644 + run_exports: {} + size: 359568 + timestamp: 1764018359470 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda sha256: b456200636bd5fecb2bec63f7e0985ad2097cf1b83d60ce0b6968dffa6d02aa1 md5: 58fd217444c2a5701a44244faf518206 @@ -7956,16 +11466,19 @@ packages: purls: [] size: 125061 timestamp: 1757437486465 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda - sha256: b4bb55d0806e41ffef94d0e3f3c97531f322b3cb0ca1f7cdf8e47f62538b7a2b - md5: f8cd1beb98240c7edb1a95883360ccfa +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + sha256: 2995f2aed4e53725e5efbc28199b46bf311c3cab2648fc4f10c2227d6d5fa196 + md5: bcb3cba70cf1eec964a03b4ba7775f01 depends: - __osx >=11.0 license: MIT license_family: MIT purls: [] - size: 179696 - timestamp: 1744128058734 + run_exports: + weak: + - c-ares >=1.34.6,<2.0a0 + size: 180327 + timestamp: 1765215064054 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda sha256: 00439d69bdd94eaf51656fdf479e0c853278439d22ae151cabf40eb17399d95f md5: 38f6df8bc8c668417b904369a01ba2e2 @@ -8068,6 +11581,22 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 390053 timestamp: 1758501053435 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cytoolz-1.1.0-py313h0997733_2.conda + sha256: d272fa92cbf6b4de83a47702878e1ac75efc665d7f60f1c2818c8c33ebd4cfa6 + md5: 5b7dd41f7974dd5d52bf37cbc0322e84 + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + - toolz >=0.10.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cytoolz?source=hash-mapping + run_exports: {} + size: 594945 + timestamp: 1771856362962 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.17-py313hc37fe24_0.conda sha256: ada3d5ab7e33fdefe66b7d21f2a7876e6a00ba6d8866ee1b2101b9a34d1fad66 md5: 42070edf971f5e14d0f51670ea1fb5e0 @@ -8154,6 +11683,22 @@ packages: purls: [] size: 59391 timestamp: 1757438897523 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozenlist-1.8.0-py313h750ce70_0.conda + sha256: 5ccc41b81f2df99072f40e4c7ef79be095e8f8f313a686ef1e63c0337bbeff5f + md5: 9605407803c5fcdee162a969f234ca35 + depends: + - __osx >=11.0 + - libcxx >=19 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/frozenlist?source=hash-mapping + run_exports: {} + size: 51974 + timestamp: 1780000580140 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdal-3.11.4-py313h976e3e5_4.conda sha256: a75d0a8205d6a9da8e144957dfce2c26503f641af13844ffb175211b93b179ec md5: bee1bcc416d5ce304d4cbb12f1167311 @@ -8197,6 +11742,20 @@ packages: purls: [] size: 1536680 timestamp: 1755851888781 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gflags-2.2.2-hf9b8971_1005.conda + sha256: fd56ed8a1dab72ab90d8a8929b6f916a6d9220ca297ff077f8f04c5ed3408e20 + md5: 57a511a5905caa37540eb914dfcbf1fb + depends: + - __osx >=11.0 + - libcxx >=17 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - gflags >=2.2.2,<2.3.0a0 + size: 82090 + timestamp: 1726600145480 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/giflib-5.2.2-h93a5062_0.conda sha256: 843b3f364ff844137e37d5c0a181f11f6d51adcedd216f019d074e5aa5d7e09c md5: 95fa1486c77505330c20f7202492b913 @@ -8216,6 +11775,37 @@ packages: purls: [] size: 102231 timestamp: 1757404604900 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda + sha256: 9fc77de416953aa959039db72bc41bfa4600ae3ff84acad04a7d0c1ab9552602 + md5: fef68d0a95aa5b84b5c1a4f6f3bf40e1 + depends: + - __osx >=11.0 + - gflags >=2.2.2,<2.3.0a0 + - libcxx >=16 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - glog >=0.7.1,<0.8.0a0 + size: 112215 + timestamp: 1718284365403 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/google-crc32c-1.8.0-py313h11ab6f4_1.conda + sha256: 475172f172fbe15eb3dcb7a7c31f83792e5ca18064f1ae56502cdeaafca97592 + md5: 08e5ab1798fd9a890447baa09bb31e8f + depends: + - __osx >=11.0 + - libcrc32c >=1.1.2,<1.2.0a0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/google-crc32c?source=hash-mapping + run_exports: {} + size: 25467 + timestamp: 1768549431006 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda sha256: c507ae9989dbea7024aa6feaebb16cbf271faac67ac3f0342ef1ab747c20475d md5: 0fc46fee39e88bbcf5835f71a9d9a209 @@ -8423,20 +12013,23 @@ packages: - pkg:pypi/kiwisolver?source=hash-mapping size: 68324 timestamp: 1756467625109 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - sha256: 4442f957c3c77d69d9da3521268cad5d54c9033f1a73f99cde0a3658937b159b - md5: c6dc8a0fdec13a0565936655c33069a1 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-hfd3d5f3_1.conda + sha256: c740e4a2e7247776a9883158fdab50ae0732c8f67f96d8f1db8ad9da5e0b5222 + md5: 8780f41b013d19219faef9c82260744b depends: - __osx >=11.0 - - libcxx >=16 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - openssl >=3.3.1,<4.0a0 + - libcxx >=19 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - openssl >=3.5.7,<4.0a0 license: MIT license_family: MIT purls: [] - size: 1155530 - timestamp: 1719463474401 + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 1159780 + timestamp: 1781859501654 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.17-h7eeda09_0.conda sha256: 310a62c2f074ebd5aa43b3cd4b00d46385ce680fa2132ecee255a200e2d2f15f md5: 92a61fd30b19ebd5c1621a5bfe6d8b5f @@ -8460,6 +12053,24 @@ packages: purls: [] size: 188306 timestamp: 1745264362794 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260526.0-cxx17_h2062a1b_1.conda + sha256: 450026eb01a52acd0ff122e331ec9b8546c93790143214b73e1c14bc2b075b22 + md5: 8adfdc0215e979a0ce31be676883e0b3 + depends: + - __osx >=11.0 + - libcxx >=19 + constrains: + - libabseil-static =20260526.0=cxx17* + - abseil-cpp =20260526.0 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libabseil >=20260526.0,<20260527.0a0 + - libabseil =*=cxx17* + size: 1273408 + timestamp: 1780524599788 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libaec-1.1.4-h51d1e36_0.conda sha256: 0ea6b73b3fb1511615d9648186a7409e73b7a8d9b3d890d39df797730e3d1dbb md5: 8ed0f86b7a5529b98ec73b43a53ce800 @@ -8491,6 +12102,134 @@ packages: purls: [] size: 788668 timestamp: 1757963940681 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-24.0.0-ha20fcc7_10_cpu.conda + build_number: 10 + sha256: c2d9c240918d2a75971a585801df26afd27eef312e416b7b2f50dff64fc22159 + md5: 52e58c8167ae95ae3c58fe1f61f331fe + depends: + - __osx >=12.0 + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + - aws-sdk-cpp >=1.11.833,<1.11.834.0a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-identity-cpp >=1.13.3,<1.13.4.0a0 + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + - azure-storage-files-datalake-cpp >=12.16.0,<12.16.1.0a0 + - bzip2 >=1.0.8,<2.0a0 + - glog >=0.7.1,<0.8.0a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libcxx >=21 + - libgoogle-cloud >=3.6.0,<3.7.0a0 + - libgoogle-cloud-storage >=3.6.0,<3.7.0a0 + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libzlib >=1.3.2,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - orc >=2.3.0,<2.3.1.0a0 + - snappy >=1.2.2,<1.3.0a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - apache-arrow-proc =*=cpu + - parquet-cpp <0.0a0 + - arrow-cpp <0.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow >=24.0.0,<24.1.0a0 + size: 4264432 + timestamp: 1783212714533 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-24.0.0-heaff1e6_10_cpu.conda + build_number: 10 + sha256: b1bd01af5961a188899f5edd536225f2451976408c253d3dc30e098725e2d8a0 + md5: 02a3a6182a0719e2c7201c2810bc1f9d + depends: + - __osx >=12.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libarrow 24.0.0 ha20fcc7_10_cpu + - libarrow-compute 24.0.0 h93a2d87_10_cpu + - libcxx >=21 + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow-acero >=24.0.0,<24.1.0a0 + size: 520376 + timestamp: 1783213116397 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-compute-24.0.0-h93a2d87_10_cpu.conda + build_number: 10 + sha256: 7cd4fa86c44c9ed1793fe1cdb9c087f3c63a714ce9a56f8438a8a7d33bb3c1d3 + md5: 0497179d3ac498619b17658c5f51ab86 + depends: + - __osx >=12.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libarrow 24.0.0 ha20fcc7_10_cpu + - libcxx >=21 + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libre2-11 >=2025.11.5 + - libutf8proc >=2.11.3,<2.12.0a0 + - re2 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow-compute >=24.0.0,<24.1.0a0 + size: 2243389 + timestamp: 1783212837899 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-24.0.0-heaff1e6_10_cpu.conda + build_number: 10 + sha256: b82bcf79ffe1bfaa69dba0bf1b9db91d49c8c453c3ff29886501e2bc7316b8d5 + md5: deedd2bd04bd8ccaba4410b1c0b080d9 + depends: + - __osx >=12.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libarrow 24.0.0 ha20fcc7_10_cpu + - libarrow-acero 24.0.0 heaff1e6_10_cpu + - libarrow-compute 24.0.0 h93a2d87_10_cpu + - libcxx >=21 + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + - libparquet 24.0.0 h0de02aa_10_cpu + - libprotobuf >=7.35.1,<7.35.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow-dataset >=24.0.0,<24.1.0a0 + size: 518819 + timestamp: 1783213277646 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-substrait-24.0.0-h2208e57_10_cpu.conda + build_number: 10 + sha256: 6ec6cef72696f572821559fdb14bd611481ecf1110ab082173c954ebdce04513 + md5: d43b08bb84661e97b005f06521d18ccd + depends: + - __osx >=12.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libarrow 24.0.0 ha20fcc7_10_cpu + - libarrow-acero 24.0.0 heaff1e6_10_cpu + - libarrow-dataset 24.0.0 heaff1e6_10_cpu + - libcxx >=21 + - libprotobuf >=7.35.1,<7.35.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow-substrait >=24.0.0,<24.1.0a0 + size: 455646 + timestamp: 1783213339544 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-36_h51639a9_openblas.conda build_number: 36 sha256: acedf4c86be500172ed84a1bf37425e5c538f0494341ebdc829001cd37707564 @@ -8509,38 +12248,47 @@ packages: purls: [] size: 17733 timestamp: 1758397710974 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.1.0-h6caf38d_4.conda - sha256: 023b609ecc35bfee7935d65fcc5aba1a3ba6807cbba144a0730198c0914f7c79 - md5: 231cffe69d41716afe4525c5c1cc5ddd +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda + sha256: a7cb9e660531cf6fbd4148cff608c85738d0b76f0975c5fc3e7d5e92840b7229 + md5: 006e7ddd8a110771134fcc4e1e3a6ffa depends: - __osx >=11.0 license: MIT license_family: MIT purls: [] - size: 68938 - timestamp: 1756599687687 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.1.0-h6caf38d_4.conda - sha256: 7f1cf83a00a494185fc087b00c355674a0f12e924b1b500d2c20519e98fdc064 - md5: cb7e7fe96c9eee23a464afd57648d2cd + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + size: 79443 + timestamp: 1764017945924 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda + sha256: 2eae444039826db0454b19b52a3390f63bfe24f6b3e63089778dd5a5bf48b6bf + md5: 079e88933963f3f149054eec2c487bc2 depends: - __osx >=11.0 - - libbrotlicommon 1.1.0 h6caf38d_4 + - libbrotlicommon 1.2.0 hc919400_1 license: MIT license_family: MIT purls: [] - size: 29015 - timestamp: 1756599708339 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.1.0-h6caf38d_4.conda - sha256: a2f2c1c2369360147c46f48124a3a17f5122e78543275ff9788dc91a1d5819dc - md5: 4ce5651ae5cd6eebc5899f9bfe0eac3c + run_exports: + weak: + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 29452 + timestamp: 1764017979099 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda + sha256: 01436c32bb41f9cb4bcf07dda647ce4e5deb8307abfc3abdc8da5317db8189d1 + md5: b2b7c8288ca1a2d71ff97a8e6a1e8883 depends: - __osx >=11.0 - - libbrotlicommon 1.1.0 h6caf38d_4 + - libbrotlicommon 1.2.0 hc919400_1 license: MIT license_family: MIT purls: [] - size: 275791 - timestamp: 1756599724058 + run_exports: + weak: + - libbrotlienc >=1.2.0,<1.3.0a0 + size: 290754 + timestamp: 1764018009077 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-36_hb0561ab_openblas.conda build_number: 36 sha256: ee8b3386c9fe8668eb9013ffea5c60f7546d0d298931f7ec0637b08d796029de @@ -8556,22 +12304,38 @@ packages: purls: [] size: 17717 timestamp: 1758397731650 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.14.1-h73640d1_0.conda - sha256: 0055b68137309db41ec34c938d95aec71d1f81bd9d998d5be18f32320c3ccba0 - md5: 1af57c823803941dfc97305248a56d57 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcrc32c-1.1.2-hbdafb3b_0.tar.bz2 + sha256: 58477b67cc719060b5b069ba57161e20ba69b8695d154a719cb4b60caf577929 + md5: 32bd82a6a625ea6ce090a81c3d34edeb + depends: + - libcxx >=11.1.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libcrc32c >=1.1.2,<1.2.0a0 + size: 18765 + timestamp: 1633683992603 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.21.0-hd5a2499_1.conda + sha256: ba74311dc4805af2ba1365d85814199a563cb09950f68b97fcd4e939dc090855 + md5: 27c5b464c5fbee7411aab3bc0195f9c2 depends: - __osx >=11.0 - - krb5 >=1.21.3,<1.22.0a0 - - libnghttp2 >=1.64.0,<2.0a0 + - krb5 >=1.22.2,<1.23.0a0 + - libnghttp2 >=1.68.1,<2.0a0 - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 - zstd >=1.5.7,<1.6.0a0 license: curl license_family: MIT purls: [] - size: 403456 - timestamp: 1749033320430 + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 410285 + timestamp: 1782802574409 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.2-hf598326_0.conda sha256: 3de00998c8271f599d6ed9aea60dc0b3e5b1b7ff9f26f8eac95f86f135aa9beb md5: edfa256c5391f789384e470ce5c9f340 @@ -8612,6 +12376,19 @@ packages: purls: [] size: 107458 timestamp: 1702146414478 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libevent-2.1.12-h2757513_1.conda + sha256: 8c136d7586259bb5c0d2b913aaadc5b9737787ae4f40e3ad1beaf96c80b919b7 + md5: 1a109764bff3bdc7bdd84088347d71dc + depends: + - openssl >=3.1.1,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libevent >=2.1.12,<2.1.13.0a0 + size: 368167 + timestamp: 1685726248899 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda sha256: 8fbb17a56f51e7113ed511c5787e0dec0d4b10ef9df921c4fd1cccca0458f648 md5: b1ca5f21335782f71a8bd69bdc093f67 @@ -8756,16 +12533,86 @@ packages: purls: [] size: 3701880 timestamp: 1757404501093 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.3.0-h48b13b8_1.conda - sha256: 837fe775ba8ec9f08655bb924e28dba390d917423350333a75fd5eeac0776174 - md5: 6375717f5fcd756de929a06d0e40fab0 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-3.6.0-ha595bda_1.conda + sha256: 0491ed9fd13df09b852b3fb80da0547c40824c0adece883cdf2d2c60ca7a7b5d + md5: b230c3a3b759f05ac0e75cb530b4c47a + depends: + - __osx >=12.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcurl >=8.21.0,<9.0a0 + - libcxx >=19 + - libgrpc >=1.82.0,<1.83.0a0 + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - openssl >=3.5.7,<4.0a0 + constrains: + - libgoogle-cloud 3.6.0 *_1 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libgoogle-cloud >=3.6.0,<3.7.0a0 + size: 1790507 + timestamp: 1783157797995 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgoogle-cloud-storage-3.6.0-hc8aed40_1.conda + sha256: 5793351ee0ea2728190495a8e6d14ece2f8cbe7698de68739e1e06fadbe9d239 + md5: 94c39d0eefe4c8d1d5ac1c5580e3505c + depends: + - __osx >=12.0 + - libabseil + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl + - libcxx >=19 + - libgoogle-cloud 3.6.0 ha595bda_1 + - libzlib >=1.3.2,<2.0a0 + - openssl + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libgoogle-cloud-storage >=3.6.0,<3.7.0a0 + size: 525286 + timestamp: 1783157985912 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgrpc-1.82.1-hf3e839d_0.conda + sha256: b9d0f510488074e889ca3cecd2b7490e8e830f7a60e2e91809a90355bf4d1a57 + md5: 5f7f645a15eedc24da7f271dc2086bcf + depends: + - __osx >=12.0 + - c-ares >=1.34.6,<2.0a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcxx >=19 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libre2-11 >=2025.11.5 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 + - re2 + constrains: + - grpc-cpp =1.82.1 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libgrpc >=1.82.1,<1.83.0a0 + size: 4898235 + timestamp: 1783587327477 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.4.0-ha332bbd_0.conda + sha256: 4fcad3cbec60da940312e883b7866816517acc5f9baecfe9a778de57327a1b1b + md5: 7394850583ca88325244b68b532c7a39 depends: - __osx >=11.0 - libcxx >=19 license: Apache-2.0 OR BSD-3-Clause purls: [] - size: 581579 - timestamp: 1758894814983 + run_exports: + weak: + - libhwy >=1.4.0,<1.5.0a0 + size: 609931 + timestamp: 1776990524407 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda sha256: de0336e800b2af9a40bdd694b03870ac4a848161b35c8a2325704f123f185f03 md5: 4d5a7445f0b25b6a3ddbb56e790f5251 @@ -8796,20 +12643,23 @@ packages: purls: [] size: 553624 timestamp: 1745268405713 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.1-h7274d02_4.conda - sha256: 74b3ded8f7f85c20b7fce0d28dfd462c49880f88458846c4f8b946d7ecb94076 - md5: 3c87b077b788e7844f0c8b866c5621ac +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.11.2-h934fa54_1.conda + sha256: 948cf1370abb58e06a7c9554838c68672ef1d78e01c3fc4e62ccfc3072579645 + md5: 05bead8980f5ae6a070117dacec38b5b depends: - - __osx >=11.0 - - libbrotlidec >=1.1.0,<1.2.0a0 - - libbrotlienc >=1.1.0,<1.2.0a0 - libcxx >=19 - - libhwy >=1.3.0,<1.4.0a0 + - __osx >=11.0 + - libhwy >=1.4.0,<1.5.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 918558 - timestamp: 1757584152666 + run_exports: + weak: + - libjxl >=0.11,<1.0a0 + size: 1032419 + timestamp: 1777065264956 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libkml-1.3.0-he250239_1021.conda sha256: e578ba448489465b8fea743e214272a9fcfccb0d152ba1ff57657aaa76a0cd7d md5: 891bb2a18eaef684f37bd4fb942cd8b2 @@ -8884,22 +12734,25 @@ packages: purls: [] size: 685237 timestamp: 1757977534772 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.67.0-hc438710_0.conda - sha256: a07cb53b5ffa2d5a18afc6fd5a526a5a53dd9523fbc022148bd2f9395697c46d - md5: a4b4dd73c67df470d091312ab87bf6ae +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + sha256: 2bc7bc3978066f2c274ebcbf711850cc9ab92e023e433b9631958a098d11e10a + md5: 6ea18834adbc3b33df9bd9fb45eaf95b depends: - __osx >=11.0 - - c-ares >=1.34.5,<2.0a0 + - c-ares >=1.34.6,<2.0a0 - libcxx >=19 - libev >=4.33,<4.34.0a0 - libev >=4.33,<5.0a0 - libzlib >=1.3.1,<2.0a0 - - openssl >=3.5.2,<4.0a0 + - openssl >=3.5.5,<4.0a0 license: MIT license_family: MIT purls: [] - size: 575454 - timestamp: 1756835746393 + run_exports: + weak: + - libnghttp2 >=1.68.1,<2.0a0 + size: 576526 + timestamp: 1773854624224 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_h60d53f8_2.conda sha256: 7b8551a4d21cf0b19f9a162f1f283a201b17f1bd5a6579abbd0d004788c511fa md5: d004259fd8d3d2798b16299d6ad6c9e9 @@ -8915,6 +12768,60 @@ packages: purls: [] size: 4284696 timestamp: 1755471861128 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-1.27.0-h46c1d2a_1.conda + sha256: 9c1840af12f31643723a7ea7f6bde6a3a394783ced6aa65b75a9fc3ecb84f39f + md5: a6c2fc0c5ddf105b3d3e353c1383a492 + depends: + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcurl >=8.21.0,<9.0a0 + - libgrpc >=1.82.0,<1.83.0a0 + - libopentelemetry-cpp-headers 1.27.0 hce30654_1 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libzlib >=1.3.2,<2.0a0 + - nlohmann_json + - prometheus-cpp >=1.3.0,<1.4.0a0 + constrains: + - cpp-opentelemetry-sdk =1.27.0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + size: 564868 + timestamp: 1783133075023 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopentelemetry-cpp-headers-1.27.0-hce30654_1.conda + sha256: 01caeebb19f70563f4ab1b97fcdf06af13be8637e9776619c8840b49070b287c + md5: 7f61001d82fd50385d4f9fb57f936e95 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 394664 + timestamp: 1783133038717 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libparquet-24.0.0-h0de02aa_10_cpu.conda + build_number: 10 + sha256: 5173184c2a22eeac1d981f6ee11e21b95c798f6724c7c4a17aa436d62db69ab6 + md5: 361c2bfcc40f283fe70d5daf75d5db5c + depends: + - __osx >=12.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libarrow 24.0.0 ha20fcc7_10_cpu + - libcxx >=21 + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libthrift >=0.22.0,<0.22.1.0a0 + - openssl >=3.5.7,<4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libparquet >=24.0.0,<24.1.0a0 + size: 1098656 + timestamp: 1783213063748 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda sha256: a2e0240fb0c79668047b528976872307ea80cb330baf8bf6624ac2c6443449df md5: 4d0f5ce02033286551a32208a5519884 @@ -8925,6 +12832,41 @@ packages: purls: [] size: 287056 timestamp: 1753879907258 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-7.35.1-h8daa630_2.conda + sha256: 782d30325e61249e3240979e0f76d9dbf67867b1f38424cfdb1abb3a0f2cfa95 + md5: 7f77d9a99dd9e79e1f5be50d6632f675 + depends: + - __osx >=12.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcxx >=19 + - libzlib >=1.3.2,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libprotobuf >=7.35.1,<7.35.2.0a0 + size: 2900719 + timestamp: 1783168180812 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libre2-11-2025.11.05-h5d15848_2.conda + sha256: 06f49291605c379467987989ff08d44b470a23afeb690d7e078517871969bff7 + md5: b750c4f83563c7528634bdcfd28622ce + depends: + - __osx >=11.0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcxx >=19 + constrains: + - re2 2025.11.05.* + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libre2-11 >=2025.11.5 + size: 166043 + timestamp: 1781312641918 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.60.0-h5c55ec3_0.conda sha256: ca5a2de5d3f68e8d6443ea1bf193c1596a278e6f86018017c0ccd4928eaf8971 md5: 05ad1d6b6fb3b384f7a07128025725cb @@ -8953,15 +12895,18 @@ packages: purls: [] size: 192168 timestamp: 1755881132099 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda - sha256: fade8223e1e1004367d7101dd17261003b60aa576df6d7802191f8972f7470b1 - md5: a7ce36e284c5faaf93c220dfc39e3abd +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.22-h1a92334_1.conda + sha256: 202be45db5726757a8ea1f374f85aacc18c504f5ff15b2558496dff4c8779c48 + md5: 9ed5ab909c449bdcae72322e44875a18 depends: - __osx >=11.0 license: ISC purls: [] - size: 164972 - timestamp: 1716828607917 + run_exports: + weak: + - libsodium >=1.0.22,<1.0.23.0a0 + size: 247352 + timestamp: 1779164136206 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libspatialite-5.1.0-gpl_he9e465b_118.conda sha256: 42eebd57a06d38003072e81acfe50d7c50306fc0ec077016a771b3950b60b5d9 md5: 2f7972081a15d940ee21708828773a72 @@ -9008,6 +12953,23 @@ packages: purls: [] size: 279193 timestamp: 1745608793272 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libthrift-0.22.0-h1fb9c8a_2.conda + sha256: 568bb23db02b050c3903bec05edbcab84960c8c7e5a1710dac3109df997ac7f1 + md5: d006875f9a58a44f92aec9a7ebeb7150 + depends: + - __osx >=11.0 + - libcxx >=19 + - libevent >=2.1.12,<2.1.13.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libthrift >=0.22.0,<0.22.1.0a0 + size: 323017 + timestamp: 1777019893083 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h7dc4979_0.conda sha256: 6bc1b601f0d3ee853acd23884a007ac0a0290f3609dabb05a47fc5a0295e2b53 md5: 2bb9e04e2da869125e2dc334d665f00d @@ -9025,6 +12987,19 @@ packages: purls: [] size: 373640 timestamp: 1758278641520 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libutf8proc-2.11.3-h2431656_0.conda + sha256: ae1a82e62cd4e3c18e005ae7ff4358ed72b2bfbfe990d5a6a5587f81e9a100dc + md5: 2255add2f6ae77d0a96624a5cbde6d45 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libutf8proc >=2.11.3,<2.12.0a0 + size: 87916 + timestamp: 1768735311947 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda sha256: a4de3f371bb7ada325e1f27a4ef7bcc81b2b6a330e46fac9c2f78ac0755ea3dd md5: e5e7d467f80da752be17796b87fe6385 @@ -9122,18 +13097,21 @@ packages: purls: [] size: 125507 timestamp: 1730442214849 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b - md5: 369964e85dc26bfe78f41399b366c435 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + sha256: 361415a698514b19a852f5d1123c5da746d4642139904156ddfca7c922d23a05 + md5: bc5a5721b6439f2f62a84f2548136082 depends: - __osx >=11.0 constrains: - - zlib 1.3.1 *_2 + - zlib 1.3.2 *_2 license: Zlib license_family: Other purls: [] - size: 46438 - timestamp: 1727963202283 + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 47759 + timestamp: 1774072956767 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-21.1.2-h4a912ad_3.conda sha256: a30d442e9fc9d80cc8925324c8e10f33213c090deca4f45fadfc1ffc79a73a74 md5: b46e55b406cc7c8a8fbc9681268c2260 @@ -9164,6 +13142,23 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1382265 timestamp: 1759295172587 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-4.4.5-py313hd065f0a_1.conda + sha256: 71dfac3971dcd134c8a31b3f670d00b8d551e275fb386568ec11ab68d95fe540 + md5: ece4dab2afb98b065b69ce769a5c6c42 + depends: + - python + - lz4-c + - python 3.13.* *_cp313 + - __osx >=11.0 + - lz4-c >=1.10.0,<1.11.0a0 + - python_abi 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/lz4?source=hash-mapping + run_exports: {} + size: 126950 + timestamp: 1765026420116 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda sha256: 94d3e2a485dab8bdfdd4837880bde3dd0d701e2b97d6134b8806b7c8e69c8652 md5: 01511afc6cc1909c5303cf31be17b44f @@ -9247,6 +13242,37 @@ packages: purls: [] size: 78411 timestamp: 1746450560057 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgpack-python-1.2.1-py313h2af2deb_1.conda + sha256: b3a134697c01b05906e23c5ba94ffad6b2647d20a5126f0fe1e1a973dd7f627a + md5: f4aec3b27ac208543c6f19a9a783caee + depends: + - python + - __osx >=11.0 + - python 3.13.* *_cp313 + - libcxx >=19 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/msgpack?source=hash-mapping + run_exports: {} + size: 104951 + timestamp: 1782460888910 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/multidict-6.7.1-py313haf6918d_0.conda + sha256: 7766b348101dcb2cb0ff59c6e5245a295bfdc8355e62990d48c574e7d7474585 + md5: f958fcfdcf64155e1e33fb2d3bdb44e0 + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/multidict?source=hash-mapping + run_exports: {} + size: 87067 + timestamp: 1771611311391 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/muparser-2.3.5-h11e0b38_0.conda sha256: 5533e7e3d4b0819b4426f8a1b3f680e6b9c922cdae2b7fabcd0e8c59df22772a md5: 1cdbe54881794ee356d3cba7e3ed6668 @@ -9288,6 +13314,38 @@ packages: - pkg:pypi/netcdf4?source=hash-mapping size: 1007894 timestamp: 1756899779520 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.12.0-h784d473_1.conda + sha256: 1945fd5b64b74ef3d57926156fb0bfe88ee637c49f3273067f7231b224f1d26d + md5: 755cfa6c08ed7b7acbee20ccbf15a47c + constrains: + - nlohmann_json-abi ==3.12.0 + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 137595 + timestamp: 1768670878127 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numcodecs-0.16.5-py313h7d16b84_0.conda + sha256: adcdce0d5ca54fb7ca7433821b41a582d9f607391c08e84f636ed38a91794f05 + md5: 6a2c4584a1a126a1ecc459002bab966f + depends: + - __osx >=11.0 + - deprecated + - libcxx >=19 + - msgpack-python + - numpy >=1.23,<3 + - numpy >=1.24 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + - typing_extensions + license: MIT + license_family: MIT + purls: + - pkg:pypi/numcodecs?source=hash-mapping + run_exports: {} + size: 662641 + timestamp: 1764782646099 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.3-py313h9771d21_0.conda sha256: 0a8d31fbb49be666f17f746104255e3ccfdb27eac79fe9b8178c8f8bd6a6b2ee md5: 54cfeb1b41a3c21da642f9b925545478 @@ -9308,6 +13366,23 @@ packages: - pkg:pypi/numpy?source=hash-mapping size: 6749676 timestamp: 1757504939745 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/obstore-0.11.0-py313h7d00576_0.conda + sha256: 91b2050c02f72bb526153eb201ea05483ede43fe468991a7fa8309de9797bc48 + md5: 88270499375bfabbf7cf96065ddf7afd + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/obstore?source=hash-mapping + run_exports: {} + size: 3428690 + timestamp: 1782409911753 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hbfb3c88_0.conda sha256: dd73e8f1da7dd6a5494c5586b835cbe2ec68bace55610b1c4bf927400fe9c0d7 md5: 6bf3d24692c157a41c01ce0bd17daeea @@ -9322,17 +13397,100 @@ packages: purls: [] size: 319967 timestamp: 1758489514651 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.4-h5503f6c_0.conda - sha256: f0512629f9589392c2fb9733d11e753d0eab8fc7602f96e4d7f3bd95c783eb07 - md5: 71118318f37f717eefe55841adb172fd +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.3-hd24854e_0.conda + sha256: b3e3ca895c336d4eb91c5d2f244a312bdb59a0de8cfa0cc4c179225ab2f6bbfb + md5: 8187a86242741725bfa74785fe812979 depends: - __osx >=11.0 - ca-certificates license: Apache-2.0 license_family: Apache purls: [] - size: 3067808 - timestamp: 1759324763146 + run_exports: + weak: + - openssl >=3.6.3,<4.0a0 + size: 3102584 + timestamp: 1781069820667 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/orc-2.3.0-hef2a647_2.conda + sha256: ad53f1c8863742d4265efe5a16a5ec5aca17165986b80c90f9717894fc2bd413 + md5: 244920b29556027175e1a7f4e4944b1f + depends: + - tzdata + - __osx >=12.0 + - libcxx >=19 + - snappy >=1.2.2,<1.3.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - libzlib >=1.3.2,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libabseil >=20260526.0,<20260527.0a0 + - libabseil * cxx17* + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - orc >=2.3.0,<2.3.1.0a0 + size: 510965 + timestamp: 1783199561511 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-3.0.3-py313h1188861_0.conda + sha256: 5fd41083894c2b7b9ba3f02a0d4ddbab17c6c1f645fdc1f3f1325522eb2a1a28 + md5: 12dd2c60321105aa1f869373ae27de42 + depends: + - python + - numpy >=1.26.0 + - python-dateutil >=2.8.2 + - libcxx >=19 + - __osx >=11.0 + - python 3.13.* *_cp313 + - numpy >=1.23,<3 + - python_abi 3.13.* *_cp313 + constrains: + - adbc-driver-postgresql >=1.2.0 + - adbc-driver-sqlite >=1.2.0 + - beautifulsoup4 >=4.12.3 + - blosc >=1.21.3 + - bottleneck >=1.4.2 + - fastparquet >=2024.11.0 + - fsspec >=2024.10.0 + - gcsfs >=2024.10.0 + - html5lib >=1.1 + - hypothesis >=6.116.0 + - jinja2 >=3.1.5 + - lxml >=5.3.0 + - matplotlib >=3.9.3 + - numba >=0.60.0 + - numexpr >=2.10.2 + - odfpy >=1.4.1 + - openpyxl >=3.1.5 + - psycopg2 >=2.9.10 + - pyarrow >=13.0.0 + - pyiceberg >=0.8.1 + - pymysql >=1.1.1 + - pyqt5 >=5.15.9 + - pyreadstat >=1.2.8 + - pytables >=3.10.1 + - pytest >=8.3.4 + - pytest-xdist >=3.6.1 + - python-calamine >=0.3.0 + - pytz >=2024.2 + - pyxlsb >=1.0.10 + - qtpy >=2.4.2 + - scipy >=1.14.1 + - s3fs >=2024.10.0 + - sqlalchemy >=2.0.36 + - tabulate >=0.9.0 + - xarray >=2024.10.0 + - xlrd >=2.0.1 + - xlsxwriter >=3.2.0 + - zstandard >=0.23.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandas?source=hash-mapping + run_exports: {} + size: 14056402 + timestamp: 1778602842319 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda sha256: 705484ad60adee86cab1aad3d2d8def03a699ece438c864e8ac995f6f66401a6 md5: 7d57f8b4b7acfc75c777bc231f0d31be @@ -9416,6 +13574,38 @@ packages: purls: [] size: 2792592 timestamp: 1757930357072 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/prometheus-cpp-1.3.0-h0967b3e_0.conda + sha256: 851a77ae1a8e90db9b9f3c4466abea7afb52713c3d98ceb0d37ba6ff27df2eff + md5: 7172339b49c94275ba42fec3eaeda34f + depends: + - __osx >=11.0 + - libcurl >=8.10.1,<9.0a0 + - libcxx >=18 + - libzlib >=1.3.1,<2.0a0 + - zlib + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - prometheus-cpp >=1.3.0,<1.4.0a0 + size: 173220 + timestamp: 1730769371051 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/propcache-0.5.2-py313h65a2061_0.conda + sha256: f6bc11459bcecbaf9036fb6c45bff046e09afdb50bb7c5caefcf4cf95f691b8c + md5: 1d9e183f80d6ca6355912233fb88f871 + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/propcache?source=hash-mapping + run_exports: {} + size: 49583 + timestamp: 1780038405102 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.1.0-py313h6535dbc_0.conda sha256: e29785861f5a3af7feb010a5d58501994f672ca4c76a1676f5b80886dcce5613 md5: 7ef1ef75e236bea54eebb1df1584f8ee @@ -9440,6 +13630,45 @@ packages: purls: [] size: 8381 timestamp: 1726802424786 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-24.0.0-py313h39782a4_0.conda + sha256: 6ab666e0a675898c03c0510e2dc7ec5918469a8ad3e6ce16206060699fe58846 + md5: a341ffb196fb28e4ec4d35163715f359 + depends: + - libarrow-acero 24.0.0.* + - libarrow-dataset 24.0.0.* + - libarrow-substrait 24.0.0.* + - libparquet 24.0.0.* + - pyarrow-core 24.0.0 *_0_* + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 26800 + timestamp: 1776928878939 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyarrow-core-24.0.0-py313h23b330d_0_cpu.conda + sha256: 99645c5c6e8af3ad94292516376a84f0b001cfeb2167564d6988f9e2176e506f + md5: ded47b8ff6e483c35d4aa92ed94c159c + depends: + - __osx >=11.0 + - libarrow 24.0.0.* *cpu + - libarrow-compute 24.0.0.* *cpu + - libcxx >=21 + - libzlib >=1.3.2,<2.0a0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + constrains: + - numpy >=1.23,<3 + - apache-arrow-proc * cpu + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/pyarrow?source=hash-mapping + run_exports: {} + size: 3950862 + timestamp: 1776928825784 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pybtex-docutils-1.0.3-py313h8f79df9_3.conda sha256: c3b62db9b75842bc15687eafc258a79bcf16981a0369abb8783618dc15944b87 md5: e09568cbfe178e1930f966726e4ef01a @@ -9570,6 +13799,20 @@ packages: purls: [] size: 516376 timestamp: 1720814307311 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/re2-2025.11.05-h5d60da5_2.conda + sha256: ba1154085d70e8e8f94cfc38a018a13d1a734ff1bcc2ab384e4fc3e532b23066 + md5: a2578dc92c16ab5b6b183cfcdc8e8b79 + depends: + - libre2-11 2025.11.05 h5d15848_2 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libre2-11 >=2025.11.5 + - re2 + size: 27372 + timestamp: 1781312662146 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda sha256: 7db04684d3904f6151eff8673270922d31da1eea7fa73254d01c437f49702e34 md5: 63ef3f6e6d6d5c589e64f11263dc5676 @@ -9713,6 +13956,21 @@ packages: purls: [] size: 40625 timestamp: 1715010029254 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.17.3-py313hcdf3177_1.conda + sha256: 5919f7142db9344116760b797e4a5d28ca3961f927a2ba1c4a61d3f0f3282dd2 + md5: cd6b5084444b0b4ed22dde20355d4c4b + depends: + - __osx >=11.0 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/wrapt?source=hash-mapping + run_exports: {} + size: 62577 + timestamp: 1756851972334 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xerces-c-3.2.5-h92fc2f4_2.conda sha256: 863a7c2a991a4399d362d42c285ebc20748a4ea417647ebd3a171e2220c7457d md5: 50b7325437ef0901fe25dc5c9e743b88 @@ -9755,30 +14013,54 @@ packages: purls: [] size: 83386 timestamp: 1753484079473 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h888dc83_9.conda - sha256: b6f9c130646e5971f6cad708e1eee278f5c7eea3ca97ec2fdd36e7abb764a7b8 - md5: 26f39dfe38a2a65437c29d69906a0f68 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yarl-1.24.2-py313h65a2061_0.conda + sha256: 91b4d82dc6faf4ae17d88f641682a5e708423ee821c05806fcc6aca2f93bf429 + md5: a886816911625ede0bf2fe230787c1ab depends: - __osx >=11.0 + - idna >=2.0 + - multidict >=4.0 + - propcache >=0.2.1 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/yarl?source=hash-mapping + run_exports: {} + size: 147964 + timestamp: 1779246743925 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-h10816f8_11.conda + sha256: 01fd50d2801b23b59fafea6bf704a6c5faf0f5969104400eae0e6572cb2e5304 + md5: d31c0e54c4f9c51100ec8c812ee925d1 + depends: - libcxx >=19 - - libsodium >=1.0.20,<1.0.21.0a0 - - krb5 >=1.21.3,<1.22.0a0 + - __osx >=11.0 + - krb5 >=1.22.2,<1.23.0a0 + - libsodium >=1.0.22,<1.0.23.0a0 license: MPL-2.0 license_family: MOZILLA purls: [] - size: 244772 - timestamp: 1757371008525 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda - sha256: 58f8860756680a4831c1bf4f294e2354d187f2e999791d53b1941834c4b37430 - md5: e3170d898ca6cb48f1bb567afb92f775 + run_exports: + weak: + - zeromq >=4.3.5,<4.4.0a0 + size: 245404 + timestamp: 1779124076307 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda + sha256: 8dd2ac25f0ba714263aac5832d46985648f4bfb9b305b5021d702079badc08d2 + md5: f1c0bce276210bed45a04949cfe8dc20 depends: - __osx >=11.0 - - libzlib 1.3.1 h8359307_2 + - libzlib 1.3.2 h8088a28_2 license: Zlib license_family: Other purls: [] - size: 77606 - timestamp: 1727963209370 + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 81123 + timestamp: 1774072974535 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py313h9734d34_0.conda sha256: 8a1bf8a66c05f724e8a56cb1918eae70bcb467a7c5d43818e37e04d86332c513 md5: ce17795bf104a29a2c7ed0bba7a804cb @@ -9822,6 +14104,30 @@ packages: purls: [] size: 49468 timestamp: 1718213032772 +- conda: https://conda.anaconda.org/conda-forge/win-64/aiohttp-3.14.1-py313h51e1470_0.conda + sha256: d6368a2e48ed310cdc99e5ac0513b84513bbc5148641811a51f2acd7820b84e0 + md5: d899397f22c3651ae1071b64604e1605 + depends: + - aiohappyeyeballs >=2.5.0 + - aiosignal >=1.4.0 + - attrs >=17.3.0 + - frozenlist >=1.1.1 + - multidict >=4.5,<7.0 + - propcache >=0.2.0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - typing_extensions >=4.4 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - yarl >=1.17.0,<2.0 + license: MIT AND Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/aiohttp?source=hash-mapping + run_exports: {} + size: 1033618 + timestamp: 1780913488229 - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py313h5ea7bf4_1.conda sha256: e3b26e1003b2cb2c37ae5431c748ed3d273b4fa2a90bc98d25cbbd1c1acd4b3c md5: dd907d0561068c8b32ab4eed2c3fcee8 @@ -9838,6 +14144,319 @@ packages: - pkg:pypi/argon2-cffi-bindings?source=hash-mapping size: 38529 timestamp: 1759486576230 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.10.3-h0a1c2aa_4.conda + sha256: a87f863ffd39c69340fbda750eafde23097681e45bb08c04a8566e87b327a04a + md5: f191830ba03429449e6de9ba5f00a1b7 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-sdkutils >=0.2.7,<0.2.8.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-c-auth >=0.10.3,<0.10.4.0a0 + size: 127721 + timestamp: 1783461474966 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.9.14-h92e57be_3.conda + sha256: dd706664dd7ab021320634481f5b126f774c6b0526eab24b79f9a6a3961ea6ed + md5: 5057d17412a615efce912b41c6a26d82 + depends: + - aws-c-common >=0.14.1,<0.14.2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - aws-c-cal >=0.9.14,<0.9.15.0a0 + size: 54261 + timestamp: 1783165096177 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.14.1-hfd05255_0.conda + sha256: 803a59e9d4b324dc49aa9bc25f601f0766adbe344acf345015a1437210d9157e + md5: bda6f415d6bf1be6c3de28477c3c6d21 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - aws-c-common >=0.14.1,<0.14.2.0a0 + size: 240649 + timestamp: 1782343687763 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.3.2-h4c63076_3.conda + sha256: a41fdc98f48defd807d9befeec975cb772ceeb1ab1d944ecb7d73f2f13882d89 + md5: 5ee3f4b0f44948e8381d3ed85b968c6f + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-compression >=0.3.2,<0.3.3.0a0 + size: 23356 + timestamp: 1783166749848 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.7.1-h3c50fbe_3.conda + sha256: 3d5ef6650b27b7e2c678a0cee29ae913b0a4a6949ccdc687ee6ecd7c557665bb + md5: d004f2e9934cea9bbfd185d520f6eed0 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-checksums >=0.2.10,<0.2.11.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + size: 58202 + timestamp: 1783193002471 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.11.0-h2c07012_3.conda + sha256: 0957c39b445ab342bf0747334e4c823212c6e3297ac41f13d6bd96b221a51e0a + md5: 7eb24c10ea05776b52dc4659dffd9a38 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-compression >=0.3.2,<0.3.3.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-http >=0.11.0,<0.11.1.0a0 + size: 213356 + timestamp: 1783192901198 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.26.3-hf70332f_6.conda + sha256: 843e1d7f512adb0910c59ef0ae7772c87c8524878febfb793bf0106d813e82eb + md5: 7902637f56f67f42d298763e5d88a9e6 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-io >=0.26.3,<0.26.4.0a0 + size: 182541 + timestamp: 1783180053272 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.16.0-h2dc7e49_1.conda + sha256: 028277a6b5e31751164d3f9e4e92d20979c8eb5889d488ca4f8750f99ed57495 + md5: fd34addc1f8e7f8559c814e9bf1aa116 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-mqtt >=0.16.0,<0.16.1.0a0 + size: 215007 + timestamp: 1783205896800 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.12.6-hcb9225d_1.conda + sha256: b37e42dda013641efd411b5f5cd85e49dbc874943b5a7b4cc2628e3c2cb1837e + md5: bd68a03d89eb296fe3b41ee9a6696341 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-checksums >=0.2.10,<0.2.11.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-auth >=0.10.3,<0.10.4.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-c-s3 >=0.12.6,<0.12.7.0a0 + size: 144471 + timestamp: 1783472008846 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.2.7-h4c63076_1.conda + sha256: fc4a86838f740f1639f12fe39eaa6550126dd7330fbd0b7c4b85246f40e4ea2c + md5: 4914281d91c9385ae6a57be69489cc16 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-c-sdkutils >=0.2.7,<0.2.8.0a0 + size: 64731 + timestamp: 1783172605202 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.2.10-h4c63076_3.conda + sha256: e403379e7686613f198474f56dc7019bbe5cc080baa6dd55ceab6270a193e53e + md5: 3a97d5f5d53f730c6d03a3034f99f00b + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - aws-checksums >=0.2.10,<0.2.11.0a0 + size: 117111 + timestamp: 1783166923557 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.40.1-h5dbc166_2.conda + sha256: e913a861c0dac6519fb37205322094813647996bfef8e9b75abd05f131a31474 + md5: 33201131148d3750df61b4e8799d95e0 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - aws-c-s3 >=0.12.6,<0.12.7.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + - aws-c-auth >=0.10.3,<0.10.4.0a0 + - aws-c-cal >=0.9.14,<0.9.15.0a0 + - aws-c-sdkutils >=0.2.7,<0.2.8.0a0 + - aws-c-mqtt >=0.16.0,<0.16.1.0a0 + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + - aws-c-io >=0.26.3,<0.26.4.0a0 + - aws-c-http >=0.11.0,<0.11.1.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + size: 311681 + timestamp: 1783483069438 +- conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.833-h583dddf_8.conda + sha256: 27820794af36a15f7eeb3e8d546198ad3ac1235cdfa340ef955bce78ea7e9624 + md5: f9113b3ca39be21963faabc1117f192b + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libzlib >=1.3.2,<2.0a0 + - aws-c-event-stream >=0.7.1,<0.7.2.0a0 + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + - aws-c-common >=0.14.1,<0.14.2.0a0 + license: Apache-2.0 + purls: [] + run_exports: + weak: + - aws-sdk-cpp >=1.11.833,<1.11.834.0a0 + size: 21900231 + timestamp: 1783536831062 +- conda: https://conda.anaconda.org/conda-forge/win-64/azure-core-cpp-1.16.3-h49e36cd_0.conda + sha256: cfcbd49faf954343220ed5e5feb8be1f65070f9d077ad223c9b1958f3a73b075 + md5: 1e8ee0cbdb1822de68e488e98cd8a31e + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + size: 502079 + timestamp: 1775238920903 +- conda: https://conda.anaconda.org/conda-forge/win-64/azure-identity-cpp-1.13.3-hf9fdf8e_2.conda + sha256: a7809dbee931b757c7b4a3c5c9fa9fd6a7e7648e02e98216d97097364eadf1f9 + md5: d1842ddabf2087c50e5305fc4f613cec + depends: + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-identity-cpp >=1.13.3,<1.13.4.0a0 + size: 425897 + timestamp: 1781268289981 +- conda: https://conda.anaconda.org/conda-forge/win-64/azure-storage-blobs-cpp-12.18.0-h4fb3251_1.conda + sha256: 51533ea83750b76c89dca67e2967f2ef20be5b26406f76ecb0e85537237b7a2e + md5: 7d38f326139cefa0a9c2ec505c0177b1 + depends: + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + size: 800072 + timestamp: 1781283965517 +- conda: https://conda.anaconda.org/conda-forge/win-64/azure-storage-common-cpp-12.14.0-hf9fdf8e_1.conda + sha256: 38dcabc3ec9d8a8f7cda50c1508aca09d0dfc27bd9f3cf9d681f0496896a6760 + md5: dbaa5d09d06d06a5febe0984667aa6d2 + depends: + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + size: 256108 + timestamp: 1781268295342 +- conda: https://conda.anaconda.org/conda-forge/win-64/azure-storage-files-datalake-cpp-12.16.0-heb2e695_1.conda + sha256: 51d41dd74824cfc3dd0312378bb6d7bd6a9672aaf6f105834ee63cfbfd2fecbb + md5: 59a1891edca316111ad277dc3535f2f0 + depends: + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + - azure-storage-common-cpp >=12.14.0,<12.14.1.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - azure-storage-files-datalake-cpp >=12.16.0,<12.16.1.0a0 + size: 450656 + timestamp: 1781540588533 - conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda sha256: 9303a7a0e03cf118eab3691013f6d6cbd1cbac66efbc70d89b20f5d0145257c0 md5: 357d7be4146d5fec543bfaa96a8a40de @@ -9854,38 +14473,44 @@ packages: purls: [] size: 49840 timestamp: 1733513605730 -- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.1.0-hfd05255_4.conda - sha256: df2a43cc4a99bd184cb249e62106dfa9f55b3d06df9b5fc67072b0336852ff65 - md5: 441706c019985cf109ced06458e6f742 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-h2d644bc_1.conda + sha256: a4fffdf1c9b9d3d0d787e20c724cff3a284dfa3773f9ce609c93b1cfd0ce8933 + md5: bc58fdbced45bb096364de0fba1637af depends: - - brotli-bin 1.1.0 hfd05255_4 - - libbrotlidec 1.1.0 hfd05255_4 - - libbrotlienc 1.1.0 hfd05255_4 + - brotli-bin 1.2.0 hfd05255_1 + - libbrotlidec 1.2.0 hfd05255_1 + - libbrotlienc 1.2.0 hfd05255_1 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: [] - size: 20233 - timestamp: 1756599828380 -- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.1.0-hfd05255_4.conda - sha256: e92c783502d95743b49b650c9276e9c56c7264da55429a5e45655150a6d1b0cf - md5: ef022c8941d7dcc420c8533b0e419733 - depends: - - libbrotlidec 1.1.0 hfd05255_4 - - libbrotlienc 1.1.0 hfd05255_4 + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 20342 + timestamp: 1764017988883 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hfd05255_1.conda + sha256: e76966232ef9612de33c2087e3c92c2dc42ea5f300050735a3c646f33bce0429 + md5: 6abd7089eb3f0c790235fe469558d190 + depends: + - libbrotlidec 1.2.0 hfd05255_1 + - libbrotlienc 1.2.0 hfd05255_1 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: [] - size: 21425 - timestamp: 1756599802301 -- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py313hfe59770_4.conda - sha256: 0e98ebafd586c4da7d848f9de94770cb27653ba9232a2badb28f8a01f6e48fb5 - md5: 477bf04a8a3030368068ccd39b8c5532 + run_exports: {} + size: 22714 + timestamp: 1764017952449 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py313h3ebfc14_1.conda + sha256: 3558006cd6e836de8dff53cbe5f0b9959f96ea6a6776b4e14f1c524916dd956c + md5: 916a39a0261621b8c33e9db2366dd427 depends: - python >=3.13,<3.14.0a0 - python_abi 3.13.* *_cp313 @@ -9893,13 +14518,14 @@ packages: - vc >=14.3,<15 - vc14_runtime >=14.44.35208 constrains: - - libbrotlicommon 1.1.0 hfd05255_4 + - libbrotlicommon 1.2.0 hfd05255_1 license: MIT license_family: MIT purls: - - pkg:pypi/brotli?source=compressed-mapping - size: 323459 - timestamp: 1756600051044 + - pkg:pypi/brotli?source=hash-mapping + run_exports: {} + size: 335605 + timestamp: 1764018132514 - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_8.conda sha256: d882712855624641f48aa9dc3f5feea2ed6b4e6004585d3616386a18186fe692 md5: 1077e9333c41ff0be8edd1a5ec0ddace @@ -9912,6 +14538,21 @@ packages: purls: [] size: 55977 timestamp: 1757437738856 +- conda: https://conda.anaconda.org/conda-forge/win-64/c-ares-1.34.6-hfd05255_0.conda + sha256: 5e1e2e24ce279f77e421fcc0e5846c944a8a75f7cf6158427c7302b02984291a + md5: 7c6da34e5b6e60b414592c74582e28bf + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - c-ares >=1.34.6,<2.0a0 + size: 193550 + timestamp: 1765215100218 - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda sha256: b9f577bddb033dba4533e851853924bfe7b7c1623d0697df382eef177308a917 md5: 20e32ced54300292aff690a69c5e7b97 @@ -10017,6 +14658,23 @@ packages: - pkg:pypi/coverage?source=hash-mapping size: 415962 timestamp: 1758501048142 +- conda: https://conda.anaconda.org/conda-forge/win-64/cytoolz-1.1.0-py313h5ea7bf4_2.conda + sha256: 4c12f431ef979072781bc4d6363f36ab6d4bcd0797c93a62a4b5a16c9e6c45e0 + md5: 40263096906457fbffe51f7dc2265728 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - toolz >=0.10.0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cytoolz?source=hash-mapping + run_exports: {} + size: 563837 + timestamp: 1771855964667 - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.17-py313h927ade5_0.conda sha256: 83e33b2f0821ef043b502ed7261592eb18a7dcc43ec76213e2888d6fd99973e2 md5: 9b792915c34565e7856fa9682879ccd2 @@ -10104,6 +14762,22 @@ packages: purls: [] size: 64394 timestamp: 1757438741305 +- conda: https://conda.anaconda.org/conda-forge/win-64/frozenlist-1.8.0-py313h0c48a3b_0.conda + sha256: 1a8067f8fefe72fb1ef7a07a50ab76e80605cc1da0ad3be481cc7cef169ac247 + md5: 710096696e7cc291f9e0eab0334f4a45 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/frozenlist?source=hash-mapping + run_exports: {} + size: 50237 + timestamp: 1779999895192 - conda: https://conda.anaconda.org/conda-forge/win-64/gdal-3.11.4-py313hf168f70_4.conda sha256: 9d0a3c76c86f9e7efebcdce38ff88116659e8fe38b9f2ea18227d1c49a7abb80 md5: 1bf04550dbd1c4c66bd8ce5ba5d25823 @@ -10147,6 +14821,23 @@ packages: purls: [] size: 26238 timestamp: 1750744808182 +- conda: https://conda.anaconda.org/conda-forge/win-64/google-crc32c-1.8.0-py313h5327936_1.conda + sha256: e0e88233f98ed75a5a0c31a7918f2a340095d1df0d3fb6bbe4f5a56c65766d8b + md5: 4f05c693f8d450b49711610815442761 + depends: + - libcrc32c >=1.1.2,<1.2.0a0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/google-crc32c?source=hash-mapping + run_exports: {} + size: 28288 + timestamp: 1768549316332 - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-hac47afa_2.conda sha256: 5f1714b07252f885a62521b625898326ade6ca25fbc20727cfe9a88f68a54bfd md5: b785694dd3ec77a011ccf0c24725382b @@ -10318,19 +15009,22 @@ packages: - pkg:pypi/kiwisolver?source=hash-mapping size: 73810 timestamp: 1756467536678 -- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - sha256: 18e8b3430d7d232dad132f574268f56b3eb1a19431d6d5de8c53c29e6c18fa81 - md5: 31aec030344e962fbd7dbbbbd68e60a9 +- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h719d79b_1.conda + sha256: c55745796e762ba9e817ab1fc0f21f1a049e202f90fa762df39578f37923f6c2 + md5: 00335c2c4a98656554771aaf6f1a7400 depends: - - openssl >=3.3.1,<4.0a0 + - openssl >=3.5.7,<4.0a0 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: [] - size: 712034 - timestamp: 1719463874284 + run_exports: + weak: + - krb5 >=1.22.2,<1.23.0a0 + size: 750320 + timestamp: 1781859644591 - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.17-hbcf6048_0.conda sha256: 7712eab5f1a35ca3ea6db48ead49e0d6ac7f96f8560da8023e61b3dbe4f3b25d md5: 3538827f77b82a837fa681a4579e37a1 @@ -10357,39 +15051,179 @@ packages: purls: [] size: 164701 timestamp: 1745264384716 +- conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20260526.0-cxx17_h0eb2380_1.conda + sha256: 103c3c49368204d26c1c4ac82cd29b6c39adf0492501b0145f6a853f0c600b3f + md5: 50a46018a50bb1feeb4496dc98deae5b + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libabseil-static =20260526.0=cxx17* + - abseil-cpp =20260526.0 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libabseil >=20260526.0,<20260527.0a0 + - libabseil =*=cxx17* + size: 2007071 + timestamp: 1780524734178 - conda: https://conda.anaconda.org/conda-forge/win-64/libaec-1.1.4-h20038f6_0.conda sha256: 0be89085effce9fdcbb6aea7acdb157b18793162f68266ee0a75acf615d4929b md5: 85a2bed45827d77d5b308cb2b165404f depends: - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 - license: BSD-2-Clause - license_family: BSD + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 33847 + timestamp: 1749993666162 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.1-gpl_h26aea39_101.conda + sha256: e3f6da5de15a2bccaf45f98ac809a9b9c9770aecc01d36b78965fb9d220af89b + md5: 720078a9094367c92abe8a87feeb29df + depends: + - bzip2 >=1.0.8,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - lzo >=2.10,<3.0a0 + - openssl >=3.5.2,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 1103182 + timestamp: 1757963879296 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-24.0.0-h11fefc6_10_cpu.conda + build_number: 10 + sha256: c906e762cfe50b8135a66795f6b42cab51028aac817992b177bfdbb1427937f7 + md5: 0f8586c681b5eb53a3585450e9ca8315 + depends: + - aws-crt-cpp >=0.40.1,<0.40.2.0a0 + - aws-sdk-cpp >=1.11.833,<1.11.834.0a0 + - azure-core-cpp >=1.16.3,<1.16.4.0a0 + - azure-identity-cpp >=1.13.3,<1.13.4.0a0 + - azure-storage-blobs-cpp >=12.18.0,<12.18.1.0a0 + - azure-storage-files-datalake-cpp >=12.16.0,<12.16.1.0a0 + - bzip2 >=1.0.8,<2.0a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl >=8.21.0,<9.0a0 + - libgoogle-cloud >=3.6.0,<3.7.0a0 + - libgoogle-cloud-storage >=3.6.0,<3.7.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libzlib >=1.3.2,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - orc >=2.3.0,<2.3.1.0a0 + - snappy >=1.2.2,<1.3.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - arrow-cpp <0.0a0 + - parquet-cpp <0.0a0 + - apache-arrow-proc =*=cpu + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow >=24.0.0,<24.1.0a0 + size: 4408983 + timestamp: 1783216360006 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-acero-24.0.0-h7d8d6a5_10_cpu.conda + build_number: 10 + sha256: eaa4a445d7b1ee64e65f9119228a90c8a0c4c22e92274d854df5b929f8b0c68d + md5: 4d6ce9394a9e8734ce9c2db715e2293a + depends: + - libarrow 24.0.0 h11fefc6_10_cpu + - libarrow-compute 24.0.0 h081cd8e_10_cpu + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow-acero >=24.0.0,<24.1.0a0 + size: 446519 + timestamp: 1783216621759 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-compute-24.0.0-h081cd8e_10_cpu.conda + build_number: 10 + sha256: 67a9aee025ecafc38073532a778d79220002688590ea108c59632eed7f33006c + md5: 317418985f26a32b29685cd3b6271678 + depends: + - libarrow 24.0.0 h11fefc6_10_cpu + - libre2-11 >=2025.11.5 + - libutf8proc >=2.11.3,<2.12.0a0 + - re2 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libarrow-compute >=24.0.0,<24.1.0a0 + size: 1752332 + timestamp: 1783216453139 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-dataset-24.0.0-h7d8d6a5_10_cpu.conda + build_number: 10 + sha256: b0bdd82756e077de19ddafbef64f5ffe4e3f1a1dbda1f797bc3ab2144c8e5ff0 + md5: 2e6ac2a9c7becfc3250d37fe56b19c72 + depends: + - libarrow 24.0.0 h11fefc6_10_cpu + - libarrow-acero 24.0.0 h7d8d6a5_10_cpu + - libarrow-compute 24.0.0 h081cd8e_10_cpu + - libparquet 24.0.0 h7051d1f_10_cpu + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE purls: [] - size: 33847 - timestamp: 1749993666162 -- conda: https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.1-gpl_h26aea39_101.conda - sha256: e3f6da5de15a2bccaf45f98ac809a9b9c9770aecc01d36b78965fb9d220af89b - md5: 720078a9094367c92abe8a87feeb29df - depends: - - bzip2 >=1.0.8,<2.0a0 - - liblzma >=5.8.1,<6.0a0 - - libxml2 - - libxml2-16 >=2.14.6 - - libzlib >=1.3.1,<2.0a0 - - lz4-c >=1.10.0,<1.11.0a0 - - lzo >=2.10,<3.0a0 - - openssl >=3.5.2,<4.0a0 + run_exports: + weak: + - libarrow-dataset >=24.0.0,<24.1.0a0 + size: 428135 + timestamp: 1783216743295 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-substrait-24.0.0-hffb5221_10_cpu.conda + build_number: 10 + sha256: 4b9eccf01bee260eacf8f3c7bd3e23ef1d972686e2c7e5a3a8cd23cb75fd34e9 + md5: 7a0324ab8d6905ce5d4ad31e87cfc088 + depends: + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libarrow 24.0.0 h11fefc6_10_cpu + - libarrow-acero 24.0.0 h7d8d6a5_10_cpu + - libarrow-dataset 24.0.0 h7d8d6a5_10_cpu + - libprotobuf >=7.35.1,<7.35.2.0a0 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - - zstd >=1.5.7,<1.6.0a0 - license: BSD-2-Clause - license_family: BSD + license: Apache-2.0 + license_family: APACHE purls: [] - size: 1103182 - timestamp: 1757963879296 + run_exports: + weak: + - libarrow-substrait >=24.0.0,<24.1.0a0 + size: 366902 + timestamp: 1783216782963 - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-35_h5709861_mkl.conda build_number: 35 sha256: 4180e7ab27ed03ddf01d7e599002fcba1b32dcb68214ee25da823bac371ed362 @@ -10406,9 +15240,9 @@ packages: purls: [] size: 66044 timestamp: 1757003486248 -- conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.1.0-hfd05255_4.conda - sha256: 65d0aaf1176761291987f37c8481be132060cc3dbe44b1550797bc27d1a0c920 - md5: 58aec7a295039d8614175eae3a4f8778 +- conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hfd05255_1.conda + sha256: 5097303c2fc8ebf9f9ea9731520aa5ce4847d0be41764edd7f6dee2100b82986 + md5: 444b0a45bbd1cb24f82eedb56721b9c4 depends: - ucrt >=10.0.20348.0 - vc >=14.3,<15 @@ -10416,34 +15250,43 @@ packages: license: MIT license_family: MIT purls: [] - size: 71243 - timestamp: 1756599708777 -- conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.1.0-hfd05255_4.conda - sha256: aa03aff197ed503e38145d0d0f17c30382ac1c6d697535db24c98c272ef57194 - md5: bf0ced5177fec8c18a7b51d568590b7c + run_exports: + weak: + - libbrotlicommon >=1.2.0,<1.3.0a0 + size: 82042 + timestamp: 1764017799966 +- conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-hfd05255_1.conda + sha256: 3239ce545cf1c32af6fffb7fc7c75cb1ef5b6ea8221c66c85416bb2d46f5cccb + md5: 450e3ae947fc46b60f1d8f8f318b40d4 depends: - - libbrotlicommon 1.1.0 hfd05255_4 + - libbrotlicommon 1.2.0 hfd05255_1 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: [] - size: 33430 - timestamp: 1756599740173 -- conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.1.0-hfd05255_4.conda - sha256: a593cde3e728a1e0486a19537846380e3ce90ae9d6c22c1412466a49474eeeed - md5: 37f4669f8ac2f04d826440a8f3f42300 + run_exports: + weak: + - libbrotlidec >=1.2.0,<1.3.0a0 + size: 34449 + timestamp: 1764017851337 +- conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-hfd05255_1.conda + sha256: 3226df6b7df98734440739f75527d585d42ca2bfe912fbe8d1954c512f75341a + md5: ccd93cfa8e54fd9df4e83dbe55ff6e8c depends: - - libbrotlicommon 1.1.0 hfd05255_4 + - libbrotlicommon 1.2.0 hfd05255_1 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 license: MIT license_family: MIT purls: [] - size: 245418 - timestamp: 1756599770744 + run_exports: + weak: + - libbrotlienc >=1.2.0,<1.3.0a0 + size: 252903 + timestamp: 1764017901735 - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-35_h2a3cdd5_mkl.conda build_number: 35 sha256: 88939f6c1b5da75bd26ce663aa437e1224b26ee0dab5e60cecc77600975f397e @@ -10459,21 +15302,38 @@ packages: purls: [] size: 66398 timestamp: 1757003514529 -- conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.14.1-h88aaa65_0.conda - sha256: b2cface2cf35d8522289df7fffc14370596db6f6dc481cc1b6ca313faeac19d8 - md5: 836b9c08f34d2017dbcaec907c6a1138 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcrc32c-1.1.2-h0e60522_0.tar.bz2 + sha256: 75e60fbe436ba8a11c170c89af5213e8bec0418f88b7771ab7e3d9710b70c54e + md5: cd4cc2d0c610c8cb5419ccc979f2d6ce + depends: + - vc >=14.1,<15.0a0 + - vs2015_runtime >=14.16.27012 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libcrc32c >=1.1.2,<1.2.0a0 + size: 25694 + timestamp: 1633684287072 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.21.0-h8206538_1.conda + sha256: 4334b7298a1ec6da6260437fc873f0c9d180c9baf123616206c187d198591634 + md5: cc24b73ccbd90296a86d4ac7d67c1b26 depends: - - krb5 >=1.21.3,<1.22.0a0 + - krb5 >=1.22.2,<1.23.0a0 - libssh2 >=1.11.1,<2.0a0 - - libzlib >=1.3.1,<2.0a0 + - libzlib >=1.3.2,<2.0a0 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: curl license_family: MIT purls: [] - size: 368346 - timestamp: 1749033492826 + run_exports: + weak: + - libcurl >=8.21.0,<9.0a0 + size: 403518 + timestamp: 1782802617355 - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda sha256: 65347475c0009078887ede77efe60db679ea06f2b56f7853b9310787fe5ad035 md5: 08d988e266c6ae77e03d164b83786dc4 @@ -10486,6 +15346,22 @@ packages: purls: [] size: 156292 timestamp: 1747040812624 +- conda: https://conda.anaconda.org/conda-forge/win-64/libevent-2.1.12-h3671451_1.conda + sha256: af03882afb7a7135288becf340c2f0cf8aa8221138a9a7b108aaeb308a486da1 + md5: 25efbd786caceef438be46da78a7b5ef + depends: + - openssl >=3.1.1,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libevent >=2.1.12,<2.1.13.0a0 + size: 410555 + timestamp: 1685726568668 - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.1-hac47afa_0.conda sha256: 8432ca842bdf8073ccecf016ccc9140c41c7114dc4ec77ca754551c01f780845 md5: 3608ffde260281fa641e70d6e34b1b96 @@ -10644,6 +15520,74 @@ packages: purls: [] size: 535273 timestamp: 1759704363233 +- conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-3.6.0-hfe3f7e9_1.conda + sha256: bbb80f1e2e1de4ff57e78a6df02511433cc161cf4d6e3a9cfc77db28af595470 + md5: e6a3bc2d2de91d6b2e1fbfe413fe2fe0 + depends: + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcurl >=8.21.0,<9.0a0 + - libgrpc >=1.82.0,<1.83.0a0 + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libgoogle-cloud 3.6.0 *_1 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libgoogle-cloud >=3.6.0,<3.7.0a0 + size: 17376 + timestamp: 1783164899530 +- conda: https://conda.anaconda.org/conda-forge/win-64/libgoogle-cloud-storage-3.6.0-he04ea4c_1.conda + sha256: c13d060686a3c85a8f1b0459a04507eff6380b0abff674db74b228a1a07b94e1 + md5: e71ed8b296a3320c4c3b50258f70f13e + depends: + - libabseil + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl + - libgoogle-cloud 3.6.0 hfe3f7e9_1 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + run_exports: + weak: + - libgoogle-cloud-storage >=3.6.0,<3.7.0a0 + size: 17385 + timestamp: 1783165200165 +- conda: https://conda.anaconda.org/conda-forge/win-64/libgrpc-1.82.1-he6cc664_0.conda + sha256: 56db0effd12badb6075a2b5877ed72c06e61211b8c9a42843711c7b67a3324e1 + md5: 343241db44cbda08b4b41689608a07af + depends: + - c-ares >=1.34.6,<2.0a0 + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libre2-11 >=2025.11.5 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.7,<4.0a0 + - re2 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - grpc-cpp =1.82.1 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libgrpc >=1.82.1,<1.83.0a0 + size: 11674380 + timestamp: 1783586253244 - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.1-default_h64bd3f2_1002.conda sha256: 266dfe151066c34695dbdc824ba1246b99f016115ef79339cbcf005ac50527c1 md5: b0cac6e5b06ca5eeb14b4f7cf908619f @@ -10659,17 +15603,20 @@ packages: purls: [] size: 2414731 timestamp: 1757624335056 -- conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.3.0-ha71e874_1.conda - sha256: c722a04f065656b988a46dee87303ff0bf037179c50e2e76704b693def7f9a96 - md5: f4649d4b6bf40d616eda57d6255d2333 +- conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.4.0-h172a326_0.conda + sha256: 4b45bf59ee46d3c746272c27651da9ce709fda4eee8536c7424acea60d0e2ad0 + md5: aeca1cb6665f19e560c1fbd20b5bcf34 depends: - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 license: Apache-2.0 OR BSD-3-Clause purls: [] - size: 536186 - timestamp: 1758894243956 + run_exports: + weak: + - libhwy >=1.4.0,<1.5.0a0 + size: 562583 + timestamp: 1776989522919 - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 md5: 64571d1dd6cdcfa25d0664a5950fdaa2 @@ -10703,21 +15650,24 @@ packages: purls: [] size: 838154 timestamp: 1745268437136 -- conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.1-hb7713f0_4.conda - sha256: 019de576f4eb0ca78ba2466514f4f84b83e222d9be83ea920f6c0f3ae260b71a - md5: f0584648fbaf89d1cef77d94bc838d3a +- conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.11.2-h932607e_1.conda + sha256: 4715e22c602526c85da09f73865676add67e0995a944b821fbff84547a9db533 + md5: 327bce3eb1ef1875c7145e915d25bcd3 depends: - - libbrotlidec >=1.1.0,<1.2.0a0 - - libbrotlienc >=1.1.0,<1.2.0a0 - - libhwy >=1.3.0,<1.4.0a0 - - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libhwy >=1.4.0,<1.5.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 license: BSD-3-Clause license_family: BSD purls: [] - size: 1091608 - timestamp: 1757584385770 + run_exports: + weak: + - libjxl >=0.11,<1.0a0 + size: 1194926 + timestamp: 1777065171989 - conda: https://conda.anaconda.org/conda-forge/win-64/libkml-1.3.0-h538826c_1021.conda sha256: 81a6096a2db500f0c3527ae59398eacca0634c3381559713ab28022d711dd3bd md5: 431ec3b40b041576811641e2d643954e @@ -10797,6 +15747,57 @@ packages: purls: [] size: 678411 timestamp: 1757977349918 +- conda: https://conda.anaconda.org/conda-forge/win-64/libopentelemetry-cpp-1.27.0-h4cd8edf_1.conda + sha256: 453e42fcdc1770ef1e2f3f159f5b333c8b836aeaa5e141642fb2e34d80c7e5c4 + md5: 300e6113eae2004627a2c2233bbd5a55 + depends: + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libcurl >=8.21.0,<9.0a0 + - libgrpc >=1.82.0,<1.83.0a0 + - libopentelemetry-cpp-headers 1.27.0 h57928b3_1 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libzlib >=1.3.2,<2.0a0 + - nlohmann_json + - prometheus-cpp >=1.3.0,<1.4.0a0 + constrains: + - cpp-opentelemetry-sdk =1.27.0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libopentelemetry-cpp >=1.27.0,<1.28.0a0 + size: 3700486 + timestamp: 1783133622370 +- conda: https://conda.anaconda.org/conda-forge/win-64/libopentelemetry-cpp-headers-1.27.0-h57928b3_1.conda + sha256: be65204142e24fdaf48701747cf603a9f6c9b1efa6ea518113f2c7b1fa27e94e + md5: 85e4342d7e798c99a828d59303909d09 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 436532 + timestamp: 1783133530257 +- conda: https://conda.anaconda.org/conda-forge/win-64/libparquet-24.0.0-h7051d1f_10_cpu.conda + build_number: 10 + sha256: c7561fc975d9ff72e2cc470ced91090fad332cddf9b74cc523777ef5f83cb049 + md5: 8c78954e6e85505203dae39240e07fad + depends: + - libarrow 24.0.0 h11fefc6_10_cpu + - libthrift >=0.22.0,<0.22.1.0a0 + - openssl >=3.5.7,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libparquet >=24.0.0,<24.1.0a0 + size: 966042 + timestamp: 1783216585416 - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h7351971_1.conda sha256: e84b041f91c94841cb9b97952ab7f058d001d4a15ed4ce226ec5fdb267cc0fa5 md5: 3ae6e9f5c47c495ebeed95651518be61 @@ -10812,6 +15813,43 @@ packages: purls: [] size: 382709 timestamp: 1753879944850 +- conda: https://conda.anaconda.org/conda-forge/win-64/libprotobuf-7.35.1-h1a71995_2.conda + sha256: 0901c04727c76556a0afb1bf3f32e4ba9f4f8b50e8a6f96d02085f47875b70e0 + md5: 86341febfb82a8beb84058167f41c3d5 + depends: + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - libzlib >=1.3.2,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libprotobuf >=7.35.1,<7.35.2.0a0 + size: 7373575 + timestamp: 1783170105520 +- conda: https://conda.anaconda.org/conda-forge/win-64/libre2-11-2025.11.05-h45f70d9_2.conda + sha256: 255d32d01d72d7d89ab5c60c20b124216e0731428af350e40ed0e001d249bcb5 + md5: 52d096d535c65d26ffb4b84a4bba2aa1 + depends: + - libabseil * cxx17* + - libabseil >=20260526.0,<20260527.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - re2 2025.11.05.* + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libre2-11 >=2025.11.5 + size: 266747 + timestamp: 1781312690043 - conda: https://conda.anaconda.org/conda-forge/win-64/librttopo-1.1.0-h5ff11c1_19.conda sha256: 85980edd2af4adb864231a188c5ff08df5e5ed3b31481e863ddba7a7e0fd705e md5: 41949b02b89c2f2e08c5bd457295f237 @@ -10825,17 +15863,20 @@ packages: purls: [] size: 404359 timestamp: 1755880940428 -- conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda - sha256: 7bcb3edccea30f711b6be9601e083ecf4f435b9407d70fc48fbcf9e5d69a0fc6 - md5: 198bb594f202b205c7d18b936fa4524f +- conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.22-h6a83c73_1.conda + sha256: de45b71224da77a1c3a7dd48d8885eb957c9f05455d4f0828463293e7144330f + md5: 7d5abf7ca1bd00b43d273f44d93d05dc depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 license: ISC purls: [] - size: 202344 - timestamp: 1716828757533 + run_exports: + weak: + - libsodium >=1.0.22,<1.0.23.0a0 + size: 280234 + timestamp: 1779164124739 - conda: https://conda.anaconda.org/conda-forge/win-64/libspatialite-5.1.0-gpl_h3bf7137_118.conda sha256: 49541de2389fad582431a3989d96c41760e99682ea328022877d0eb6ab1667a9 md5: a00265bc2dea705fdae1c629f69e65d9 @@ -10885,6 +15926,24 @@ packages: purls: [] size: 292785 timestamp: 1745608759342 +- conda: https://conda.anaconda.org/conda-forge/win-64/libthrift-0.22.0-h2e43b2f_2.conda + sha256: 7ffb48755c4fc4a7cca454e4afea286e4fb47e50e153df1b006b14691f0f43d0 + md5: 42856184560e5cf901551fd414ad25c1 + depends: + - libevent >=2.1.12,<2.1.13.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.6,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - libthrift >=0.22.0,<0.22.1.0a0 + size: 634136 + timestamp: 1777019194906 - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.1-h550210a_0.conda sha256: d6cac6596ded0d5bbbc4198d7eb4db88da8c00236ebf5e2c8ad333ccde8965e2 md5: e23f29747d9d2aa2a39b594c114fac67 @@ -10902,6 +15961,21 @@ packages: purls: [] size: 992060 timestamp: 1758278535260 +- conda: https://conda.anaconda.org/conda-forge/win-64/libutf8proc-2.11.3-hb980946_0.conda + sha256: 5d82af0779eab283416240da792a0d2fe4f8213c447e9f04aeaab1801468a90c + md5: 5f34fcb6578ea9bdbfd53cc2cfb88200 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - libutf8proc >=2.11.3,<2.12.0a0 + size: 89061 + timestamp: 1768735187639 - conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda sha256: 7b6316abfea1007e100922760e9b8c820d6fc19df3f42fb5aca684cfacb31843 md5: f9bbae5e2537e3b06e0f7310ba76c893 @@ -11025,20 +16099,23 @@ packages: purls: [] size: 146856 timestamp: 1730442305774 -- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda - sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 - md5: 41fbfac52c601159df6c01f875de31b9 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + sha256: 88609816e0cc7452bac637aaf65783e5edf4fee8a9f8e22bdc3a75882c536061 + md5: dbabbd6234dea34040e631f87676292f depends: - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 constrains: - - zlib 1.3.1 *_2 + - zlib 1.3.2 *_2 license: Zlib license_family: Other purls: [] - size: 55476 - timestamp: 1727963768015 + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 58347 + timestamp: 1774072851498 - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-21.1.2-hfa2b4ca_3.conda sha256: 7fb9351d8b566dac4c3f7f20aaf200edfeb8d123ca98be5abe80e38e13f09ee5 md5: 166437478d1ec2f9815180e86a3acebd @@ -11072,6 +16149,24 @@ packages: - pkg:pypi/lxml?source=hash-mapping size: 1235180 timestamp: 1759295011274 +- conda: https://conda.anaconda.org/conda-forge/win-64/lz4-4.4.5-py313h4bbca4b_1.conda + sha256: c00d35e5228ac375658495e86d33e1daa819ed11161034a5799dcb07bfa328c9 + md5: e8df314d3f3fa27e935b6cb449d754f3 + depends: + - python + - lz4-c + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - lz4-c >=1.10.0,<1.11.0a0 + - python_abi 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/lz4?source=hash-mapping + run_exports: {} + size: 46606 + timestamp: 1765026422655 - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda sha256: 632cf3bdaf7a7aeb846de310b6044d90917728c73c77f138f08aa9438fc4d6b5 md5: 0b69331897a92fac3d8923549d48d092 @@ -11172,6 +16267,38 @@ packages: purls: [] size: 103088799 timestamp: 1753975600547 +- conda: https://conda.anaconda.org/conda-forge/win-64/msgpack-python-1.2.1-py313h1a38498_1.conda + sha256: e077fab86cde8d9f1f52adaddd80beb3bc3636bb3234c8523c95a087d340c5cb + md5: 26faa18b0b9a5466f65d0f309424babf + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/msgpack?source=compressed-mapping + run_exports: {} + size: 89413 + timestamp: 1782460810590 +- conda: https://conda.anaconda.org/conda-forge/win-64/multidict-6.7.1-py313hd650c13_0.conda + sha256: 3d842544d6a27914116e70677d0f73459c97c585f6daccebb447941104b72948 + md5: 6abba47ca64961ca5e8eac08f02a7142 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/multidict?source=hash-mapping + run_exports: {} + size: 91672 + timestamp: 1771610834790 - conda: https://conda.anaconda.org/conda-forge/win-64/muparser-2.3.5-he0c23c2_0.conda sha256: 57f78d8cd9a282d03cd7a7ffb1f42d570e1bbfb42d606e99de5c16e089067185 md5: 013aabb169d59009bdf7d70319360e9b @@ -11205,6 +16332,38 @@ packages: - pkg:pypi/netcdf4?source=hash-mapping size: 977498 timestamp: 1756899261482 +- conda: https://conda.anaconda.org/conda-forge/win-64/nlohmann_json-3.12.0-h5112557_1.conda + sha256: 045edd5d571c235de67472ad8fe03d9706b8426c4ba9a73f408f946034b6bc5e + md5: 24a9dde77833cc48289ef92b4e724da4 + constrains: + - nlohmann_json-abi ==3.12.0 + license: MIT + license_family: MIT + purls: [] + run_exports: {} + size: 134870 + timestamp: 1758194302226 +- conda: https://conda.anaconda.org/conda-forge/win-64/numcodecs-0.16.5-py313hc90dcd4_0.conda + sha256: 008d11564f2a3bac16ea0e323a02b24ca06fb28f8b74c01cde13098003c35b9b + md5: 4006d795b35200d0d6e28a1de84dfcc5 + depends: + - deprecated + - msgpack-python + - numpy >=1.23,<3 + - numpy >=1.24 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - typing_extensions + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/numcodecs?source=hash-mapping + run_exports: {} + size: 516677 + timestamp: 1764782612807 - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.3-py313hce7ae62_0.conda sha256: 183d004ade7c46d0b9003a747741dc0320773a226323483ba0592faafeabd681 md5: e23d89fa7007fa6a9bfe7a2a1cb58d61 @@ -11228,6 +16387,22 @@ packages: - pkg:pypi/numpy?source=hash-mapping size: 7463065 timestamp: 1757504921589 +- conda: https://conda.anaconda.org/conda-forge/win-64/obstore-0.11.0-py313hf61f64f_0.conda + sha256: a7b5f632ea05b456cf57e75ecbf17241d8c005649e85738161f443df54efc3ea + md5: 119fdb0f29195ee0da6e91b386fc37c8 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/obstore?source=hash-mapping + run_exports: {} + size: 4021566 + timestamp: 1782409045728 - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h24db6dd_0.conda sha256: 226c270a7e3644448954c47959c00a9bf7845f6d600c2a643db187118d028eee md5: 5af852046226bb3cb15c7f61c2ac020a @@ -11243,9 +16418,9 @@ packages: purls: [] size: 244860 timestamp: 1758489556249 -- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.4-h725018a_0.conda - sha256: 5ddc1e39e2a8b72db2431620ad1124016f3df135f87ebde450d235c212a61994 - md5: f28ffa510fe055ab518cbd9d6ddfea23 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.3-hf411b9b_0.conda + sha256: cb6e7ba0d010ee0d3249ce9886de3d7613d26d9965d4c95666fa66b9c4c31001 + md5: e99f95734a326c0fd4d02bbd995150d4 depends: - ca-certificates - ucrt >=10.0.20348.0 @@ -11254,8 +16429,93 @@ packages: license: Apache-2.0 license_family: Apache purls: [] - size: 9218823 - timestamp: 1759326176247 + run_exports: + weak: + - openssl >=3.6.3,<4.0a0 + size: 9414790 + timestamp: 1781071745579 +- conda: https://conda.anaconda.org/conda-forge/win-64/orc-2.3.0-hf7eca67_2.conda + sha256: 583aecbbc3107a0e846910df0f541b2db3d323421686bdbd1fe4fba145c33dd3 + md5: b4b3a02bfae7029cc0be9607501651c7 + depends: + - tzdata + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - zstd >=1.5.7,<1.6.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - libprotobuf >=7.35.1,<7.35.2.0a0 + - libabseil >=20260526.0,<20260527.0a0 + - libabseil * cxx17* + - snappy >=1.2.2,<1.3.0a0 + - libzlib >=1.3.2,<2.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: + weak: + - orc >=2.3.0,<2.3.1.0a0 + size: 1455977 + timestamp: 1783199562595 +- conda: https://conda.anaconda.org/conda-forge/win-64/pandas-3.0.3-py313h26f5e95_0.conda + sha256: 8c8d33497c0142d5c55011b31d4d3122fea97c3144f8c2d118404dbfc41dc072 + md5: 9ceae84ab5002af792f42f1abc7ce997 + depends: + - python + - numpy >=1.26.0 + - python-dateutil >=2.8.2 + - python-tzdata + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - numpy >=1.23,<3 + - python_abi 3.13.* *_cp313 + constrains: + - adbc-driver-postgresql >=1.2.0 + - adbc-driver-sqlite >=1.2.0 + - beautifulsoup4 >=4.12.3 + - blosc >=1.21.3 + - bottleneck >=1.4.2 + - fastparquet >=2024.11.0 + - fsspec >=2024.10.0 + - gcsfs >=2024.10.0 + - html5lib >=1.1 + - hypothesis >=6.116.0 + - jinja2 >=3.1.5 + - lxml >=5.3.0 + - matplotlib >=3.9.3 + - numba >=0.60.0 + - numexpr >=2.10.2 + - odfpy >=1.4.1 + - openpyxl >=3.1.5 + - psycopg2 >=2.9.10 + - pyarrow >=13.0.0 + - pyiceberg >=0.8.1 + - pymysql >=1.1.1 + - pyqt5 >=5.15.9 + - pyreadstat >=1.2.8 + - pytables >=3.10.1 + - pytest >=8.3.4 + - pytest-xdist >=3.6.1 + - python-calamine >=0.3.0 + - pytz >=2024.2 + - pyxlsb >=1.0.10 + - qtpy >=2.4.2 + - scipy >=1.14.1 + - s3fs >=2024.10.0 + - sqlalchemy >=2.0.36 + - tabulate >=0.9.0 + - xarray >=2024.10.0 + - xlrd >=2.0.1 + - xlsxwriter >=3.2.0 + - zstandard >=0.23.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandas?source=hash-mapping + run_exports: {} + size: 13792436 + timestamp: 1778602664436 - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h03d888a_0.conda sha256: dcda7e9bedc1c87f51ceef7632a5901e26081a1f74a89799a3e50dbdc801c0bd md5: 452d6d3b409edead3bd90fc6317cd6d4 @@ -11352,6 +16612,40 @@ packages: purls: [] size: 2769241 timestamp: 1757930535432 +- conda: https://conda.anaconda.org/conda-forge/win-64/prometheus-cpp-1.3.0-hcea2f5d_0.conda + sha256: ed08acd2ce6c69063693193450df89e8695e8b1251b399d34fb56ab45d900cbc + md5: 128297355faf0afcb84e22e43d472101 + depends: + - libcurl >=8.10.1,<9.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - zlib + license: MIT + license_family: MIT + purls: [] + run_exports: + weak: + - prometheus-cpp >=1.3.0,<1.4.0a0 + size: 183665 + timestamp: 1730769570131 +- conda: https://conda.anaconda.org/conda-forge/win-64/propcache-0.5.2-py313hd650c13_0.conda + sha256: 1990323bce20bcfc3b23cf88850ff4bec5ecaae7624c2b83abe43d1f193c1ebc + md5: ec0abb7838da95de35c1ab1a6e3d892a + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/propcache?source=hash-mapping + run_exports: {} + size: 48598 + timestamp: 1780037809033 - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.1.0-py313h5ea7bf4_0.conda sha256: 7a4d59ab9d40bb16bb8e0d0c47a74aa3eb4f31be91b4bf245baa50ac1eb92b13 md5: 6a0bc86fb86d7a3c3290ffde53dde0a6 @@ -11379,6 +16673,46 @@ packages: purls: [] size: 9389 timestamp: 1726802555076 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-24.0.0-py313hfa70ccb_0.conda + sha256: e941ae4257e50a6ff4303d6b73e4201adc0fa057d50c38ba3564285733361156 + md5: c2753bfbbb6517540349fbbe57b97c05 + depends: + - libarrow-acero 24.0.0.* + - libarrow-dataset 24.0.0.* + - libarrow-substrait 24.0.0.* + - libparquet 24.0.0.* + - pyarrow-core 24.0.0 *_0_* + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: [] + run_exports: {} + size: 27130 + timestamp: 1776928417640 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyarrow-core-24.0.0-py313hc76bb52_0_cpu.conda + sha256: be0a908caec334c9660106cf42eddca9f85efc59ff691eceee9ec058d3de385c + md5: 3ec655e3a3e3698520d890bef9b3d1b9 + depends: + - libarrow 24.0.0.* *cpu + - libarrow-compute 24.0.0.* *cpu + - libzlib >=1.3.2,<2.0a0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libprotobuf >=6.33.5 + - apache-arrow-proc * cpu + - numpy >=1.23,<3 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/pyarrow?source=hash-mapping + run_exports: {} + size: 3608065 + timestamp: 1776928411961 - conda: https://conda.anaconda.org/conda-forge/win-64/pybtex-docutils-1.0.3-py313hfa70ccb_3.conda sha256: d6026d44eb3556840db7c3c94a1458ee2fb8368c9b5f63d9987ebc4ef37a9f4c md5: 4939f04f0f34df8096827380a36ead6f @@ -11517,6 +16851,20 @@ packages: purls: [] size: 1377020 timestamp: 1720814433486 +- conda: https://conda.anaconda.org/conda-forge/win-64/re2-2025.11.05-haef9524_2.conda + sha256: eb11c0e948c2576378a8e7f22768a967044d54bfbdea9ef358d31074b91c21f0 + md5: 5d51319ef836277672d665c2fc04bab0 + depends: + - libre2-11 2025.11.05 h45f70d9_2 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: + weak: + - libre2-11 >=2025.11.5 + - re2 + size: 222198 + timestamp: 1781312702688 - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.27.1-py313hfbe8231_1.conda sha256: 0a300df3466cb98834d60f1863db60fc8d4d0cbbd732b153d3a656654f307fa2 md5: 9fbdb4338b80392e5d59529712f30763 @@ -11716,6 +17064,17 @@ packages: purls: [] size: 113963 timestamp: 1753739198723 +- conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.44.35208-h38c0c73_31.conda + sha256: 8b20152d00e1153ccb1ed377a160110482f286a6d85a82b57ffcd60517d523a7 + md5: d75abcfbc522ccd98082a8c603fce34c + depends: + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + purls: [] + run_exports: {} + size: 18249 + timestamp: 1753739241918 - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 sha256: 9df10c5b607dd30e05ba08cbd940009305c75db242476f4e845ea06008b0a283 md5: 1cee351bf20b830d991dbe0bc8cd7dfe @@ -11723,6 +17082,22 @@ packages: license_family: MIT purls: [] size: 1176306 +- conda: https://conda.anaconda.org/conda-forge/win-64/wrapt-1.17.3-py313h5ea7bf4_1.conda + sha256: 260a3295f39565c28be9232a11ca7ee435af6e9366ffd2569ff29a63e7c144a0 + md5: 3e199c8db04833fe628867462aeaca24 + depends: + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/wrapt?source=hash-mapping + run_exports: {} + size: 63385 + timestamp: 1756851987645 - conda: https://conda.anaconda.org/conda-forge/win-64/xerces-c-3.2.5-he0c23c2_2.conda sha256: 759ae22a0a221dc1c0ba39684b0dcf696aab4132478e17e56a0366ded519e54e md5: 82b6eac3c198271e98b48d52d79726d8 @@ -11856,36 +17231,58 @@ packages: purls: [] size: 63944 timestamp: 1753484092156 -- conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h5bddc39_9.conda - sha256: 690cf749692c8ea556646d1a47b5824ad41b2f6dfd949e4cdb6c44a352fcb1aa - md5: a6c8f8ee856f7c3c1576e14b86cd8038 +- conda: https://conda.anaconda.org/conda-forge/win-64/yarl-1.24.2-py313hd650c13_0.conda + sha256: e3cd6601474e9a808233df49193f339f1484fd4b0259e29863301a38f33596af + md5: 66967bcbc121922df483e718df9f5825 depends: + - idna >=2.0 + - multidict >=4.0 + - propcache >=0.2.1 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/yarl?source=hash-mapping + run_exports: {} + size: 151505 + timestamp: 1779246206706 +- conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h3a581c9_11.conda + sha256: c3e279cb309b153152fcdd6ee6d039ad996d563c849f06be39d85b8e3351df25 + md5: f016c0c5f9c01549b259146614786192 + depends: - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - ucrt >=10.0.20348.0 - - libsodium >=1.0.20,<1.0.21.0a0 - - krb5 >=1.21.3,<1.22.0a0 + - libsodium >=1.0.22,<1.0.23.0a0 + - krb5 >=1.22.2,<1.23.0a0 license: MPL-2.0 license_family: MOZILLA purls: [] - size: 265212 - timestamp: 1757370864284 -- conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.1-h2466b09_2.conda - sha256: 8c688797ba23b9ab50cef404eca4d004a948941b6ee533ead0ff3bf52012528c - md5: be60c4e8efa55fddc17b4131aa47acbd + run_exports: + weak: + - zeromq >=4.3.5,<4.3.6.0a0 + size: 265717 + timestamp: 1779124031378 +- conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda + sha256: ef408f85f664a4b9c9dac3cb2e36154d9baa15a88984ea800e11060e0f2394a1 + md5: 5187ecf958be3c39110fe691cbd6873e depends: - - libzlib 1.3.1 h2466b09_2 + - libzlib 1.3.2 hfd05255_2 - ucrt >=10.0.20348.0 - - vc >=14.2,<15 - - vc14_runtime >=14.29.30139 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 license: Zlib license_family: Other purls: [] - size: 107439 - timestamp: 1727963788936 + run_exports: + weak: + - libzlib >=1.3.2,<2.0a0 + size: 850351 + timestamp: 1774072891049 - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py313h5fd188c_0.conda sha256: d20a163a466621e41c3d06bc5dc3040603b8b0bfa09f493e2bfc0dbd5aa6e911 md5: edfe43bb6e955d4d9b5e7470ea92dead @@ -11929,6 +17326,7 @@ packages: - matplotlib - netcdf4 - numpy + - platformdirs - python-dateutil - pyyaml - scipy>=1.10.1 @@ -11941,17 +17339,24 @@ packages: - sphinxcontrib-bibtex ; extra == 'doc' - sphinx-design ; extra == 'doc' - sphinx-rtd-theme ; extra == 'doc' - - cartopy ; extra == 'all' - geoid-toolkit ; extra == 'all' - h5py ; extra == 'all' - jupyterlab ; extra == 'all' - jupyterlab-widgets ; extra == 'all' - notebook ; extra == 'all' - - shapely ; extra == 'all' + - dask ; extra == 'aws' + - obstore ; extra == 'aws' + - pyarrow ; extra == 'aws' + - s3fs ; extra == 'aws' + - zarr>=3 ; extra == 'aws' - flake8 ; extra == 'dev' - pytest>=4.6 ; extra == 'dev' - pytest-cov ; extra == 'dev' - pytest-xdist ; extra == 'dev' + - cartopy ; extra == 'map' + - gdal ; extra == 'map' + - pyshp ; extra == 'map' + - shapely ; extra == 'map' requires_python: ~=3.6 - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl name: graphviz diff --git a/pyproject.toml b/pyproject.toml index f87fadb4..57894a69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,15 +16,14 @@ keywords = [ authors = [ {name = "Tyler Sutterley"}, - {name = "Hugo Lecomte"}, - {name = "Yara Mohajerani"}, - {name = "Isabella Velicogna"}, {email = "tsutterl@uw.edu"} ] -maintainers = [{ name = "gravity-toolkit contributors" }] +maintainers = [ + { name = "gravity-toolkit contributors" } +] license = "MIT" license-files = ["LICENSE"] -readme = "README.rst" +readme = {file = "README.md", content-type = "text/markdown"} requires-python = "~=3.6" dependencies = [ @@ -34,6 +33,7 @@ dependencies = [ "matplotlib", "netCDF4", "numpy", + "platformdirs", "python-dateutil", "pyyaml", "scipy>=1.10.1", @@ -44,13 +44,12 @@ classifiers=[ "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Physics", ] @@ -62,8 +61,10 @@ Issues = "https://github.com/tsutterley/gravity-toolkit/issues" [project.optional-dependencies] doc = ["docutils", "graphviz", "myst-nb", "numpydoc", "sphinx", "sphinx-argparse>=0.4", "sphinxcontrib-bibtex", "sphinx-design", "sphinx_rtd_theme"] -all = ["cartopy", "geoid-toolkit", "h5py", "jupyterlab", "jupyterlab_widgets", "notebook", "shapely"] +all = ["geoid-toolkit", "h5py", "jupyterlab", "jupyterlab_widgets", "notebook"] +aws = ["dask", "obstore", "pyarrow", "s3fs", "zarr>=3"] dev = ["flake8", "pytest>=4.6", "pytest-cov", "pytest-xdist"] +map = ["cartopy", "gdal", "pyshp", "shapely"] [tool.setuptools.packages.find] exclude = ["test*", "run*"] @@ -103,6 +104,29 @@ precision = 2 [tool.ruff] line-length = 80 indent-width = 4 +exclude = [ + ".git", + ".pixi", + "build", + "run", + "test", +] + +[tool.ruff.lint] +select = [ + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", + # ruff-specific + "RUF", +] [tool.ruff.lint.pydocstyle] convention = "numpy" @@ -110,6 +134,7 @@ convention = "numpy" [tool.ruff.format] quote-style = "single" indent-style = "space" +line-ending = "auto" docstring-code-format = false [tool.pixi.workspace] @@ -124,8 +149,10 @@ gravity-toolkit = { path = ".", editable = true } [tool.pixi.environments] default = { solve-group = "default" } all = { features = ["all"], solve-group = "default" } -dev = { features = ["all", "dev", "doc"], solve-group = "default" } +aws = { features = ["all", "aws"], solve-group = "default" } +dev = { features = ["all", "dev", "map", "doc"], solve-group = "default" } doc = { features = ["all", "doc"], solve-group = "default" } +map = { features = ["all", "map"], solve-group = "default" } [tool.pixi.tasks.start] cmd = "jupyter lab" @@ -138,10 +165,16 @@ cmd = "pixi workspace export conda-environment {{ flags }} > environment.yml" description = "Export workspace to a conda environment file" [tool.pixi.feature.doc.tasks.docs] -args = [ { "arg" = "flags", "default" = "" } ] -cmd = "make html {{ flags }}" +args = [ { "arg" = "builder", "default" = "html"}, { "arg" = "flags", "default" = "" } ] +cmd = "make {{ builder }} {{ flags }}" +cwd = "doc" +description = "Create documentation with sphinx" + +[tool.pixi.feature.doc.tasks.clean-docs] +args = [ { "arg" = "builder", "default" = "html"}, { "arg" = "flags", "default" = "" } ] +cmd = "make clean && make {{ builder }} {{ flags }}" cwd = "doc" -description = "Create HTML documentation with sphinx" +description = "Remove build artifacts and rebuild the documentation with sphinx" [tool.pixi.feature.dev.tasks.test] args = [ { "arg" = "flags", "default" = "" }, { "arg" = "test", "default" = "." } ] @@ -166,12 +199,12 @@ lxml = "*" matplotlib-base = "*" netcdf4 = "*" numpy = "*" +platformdirs = "*" python-dateutil = "*" pyyaml = "*" scipy = "*" [tool.pixi.feature.all.dependencies] -cartopy = "*" geoid-toolkit = "*" h5py = "*" ipympl = "*" @@ -179,7 +212,13 @@ ipywidgets = "*" jupyterlab = "*" jupyterlab_widgets = "*" notebook = "*" -shapely = "*" + +[tool.pixi.feature.aws.dependencies] +dask = "*" +obstore = "*" +pyarrow = "*" +s3fs = "*" +zarr = ">=3" [tool.pixi.feature.dev.dependencies] flake8 = "*" @@ -198,3 +237,9 @@ sphinx-argparse = "*" sphinxcontrib-bibtex = "*" sphinx-design = "*" sphinx_rtd_theme = "*" + +[tool.pixi.feature.map.dependencies] +cartopy = "*" +gdal = "*" +pyshp = "*" +shapely = "*" diff --git a/scripts/calc_harmonic_resolution.py b/scripts/calc_harmonic_resolution.py index ed32d69a..a720e351 100755 --- a/scripts/calc_harmonic_resolution.py +++ b/scripts/calc_harmonic_resolution.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" calc_harmonic_resolution.py Written by Tyler Sutterley (04/2022) @@ -36,9 +36,11 @@ Updated 08/2013: changed SPH_CAP option to (Y/N) Written 01/2013 """ + import argparse import numpy as np + # PURPOSE: Calculates minimum spatial resolution that can be resolved # from spherical harmonics of a maximum degree def calc_harmonic_resolution(LMAX, RADIUS=6371.0008, SPH_CAP=False): @@ -59,39 +61,56 @@ def calc_harmonic_resolution(LMAX, RADIUS=6371.0008, SPH_CAP=False): # Smallest diameter of a spherical cap that can be resolved by the # harmonics. Size of the smallest bump, half-wavelength, which can # be produced by the clm/slm - psi_min = 4.0*RADIUS*np.arcsin(1.0/(LMAX+1.0)) + psi_min = 4.0 * RADIUS * np.arcsin(1.0 / (LMAX + 1.0)) else: # Shortest half-wavelength that can be resolved by the clm/slm # This estimation is based on the number of possible zeros along # the equator - psi_min = np.pi*RADIUS/LMAX + psi_min = np.pi * RADIUS / LMAX return psi_min + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser() - parser.add_argument('--lmax','-l', metavar='LMAX', - type=int, nargs='+', - help='maximum degree of spherical harmonics') - parser.add_argument('--radius','-R', - type=float, default=6371.0008, - help='Average radius of the Earth in kilometers') - parser.add_argument('--cap','-C', - default=False, action='store_true', - help='Calculate smallest possible bump that can be resolved') + parser.add_argument( + '--lmax', + '-l', + metavar='LMAX', + type=int, + nargs='+', + help='maximum degree of spherical harmonics', + ) + parser.add_argument( + '--radius', + '-R', + type=float, + default=6371.0008, + help='Average radius of the Earth in kilometers', + ) + parser.add_argument( + '--cap', + '-C', + default=False, + action='store_true', + help='Calculate smallest possible bump that can be resolved', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # for each entered spherical harmonic degree for LMAX in args.lmax: - psi_min = calc_harmonic_resolution(LMAX, - RADIUS=args.radius, SPH_CAP=args.cap) - print('{0:5d}: {1:0.4f} km'.format(LMAX,psi_min)) + psi_min = calc_harmonic_resolution( + LMAX, RADIUS=args.radius, SPH_CAP=args.cap + ) + print('{0:5d}: {1:0.4f} km'.format(LMAX, psi_min)) + # run main program if __name__ == '__main__': diff --git a/scripts/calc_mascon.py b/scripts/calc_mascon.py index 48f7dd7e..525cb0db 100644 --- a/scripts/calc_mascon.py +++ b/scripts/calc_mascon.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" calc_mascon.py Written by Tyler Sutterley (05/2023) @@ -247,6 +247,7 @@ Updated 02/2012: Added sensitivity kernels Written 02/2012 """ + from __future__ import print_function, division import sys @@ -261,6 +262,7 @@ import scipy.linalg import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -270,9 +272,16 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate a regional time-series through a least # squares mascon process -def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, +def calc_mascon( + base_dir, + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -309,8 +318,8 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, RECONSTRUCT_FILE=None, LANDMASK=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # directory setup base_dir = pathlib.Path(base_dir).expanduser().absolute() # recursively create output directory if not currently existing @@ -329,8 +338,9 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, parser = re.compile(r'^(?!\#|\%|$)', re.VERBOSE) # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) @@ -348,19 +358,18 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, order_str = f'M{MMAX:d}' if (MMAX != LMAX) else '' # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) gw_str = '' # Read Ocean function and convert to Ylms for redistribution - if (REDISTRIBUTE_MASCONS | REDISTRIBUTE_REMOVED): + if REDISTRIBUTE_MASCONS | REDISTRIBUTE_REMOVED: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) ocean_str = '_OCN' else: # not distributing uniformly over ocean @@ -370,18 +379,36 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # replacing low-degree harmonics with SLR values if specified # include degree 1 (geocenter) harmonics if specified # correcting for Pole-Tide and Atmospheric Jumps if specified - Ylms = gravtk.grace_input_months(base_dir, PROC, DREL, DSET, LMAX, - START, END, MISSING, SLR_C20, DEG1, MMAX=MMAX, SLR_21=SLR_21, - SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + Ylms = gravtk.grace_input_months( + base_dir, + PROC, + DREL, + DSET, + LMAX, + START, + END, + MISSING, + SLR_C20, + DEG1, + MMAX=MMAX, + SLR_21=SLR_21, + SLR_22=SLR_22, + SLR_C30=SLR_C30, + SLR_C40=SLR_C40, + SLR_C50=SLR_C50, + DEG1_FILE=DEG1_FILE, + MODEL_DEG1=MODEL_DEG1, + ATM=ATM, + POLE_TIDE=POLE_TIDE, + ) # create harmonics object from GRACE/GRACE-FO data GRACE_Ylms = gravtk.harmonics().from_dict(Ylms) # use a mean file for the static field to remove if MEAN_FILE: # read data form for input mean file (ascii, netCDF4, HDF5, gfc) - mean_Ylms = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GRACE_Ylms.subtract(mean_Ylms) else: @@ -395,7 +422,9 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # using standard GRACE/GRACE-FO harmonics ds_str = '' # full path to directory for specific GRACE/GRACE-FO product - GRACE_Ylms.directory = pathlib.Path(Ylms['directory']).expanduser().absolute() + GRACE_Ylms.directory = ( + pathlib.Path(Ylms['directory']).expanduser().absolute() + ) # date information of GRACE/GRACE-FO coefficients n_files = len(GRACE_Ylms.time) @@ -415,35 +444,37 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, if REMOVE_FILES: # extend list if a single format was entered for all files if len(REMOVE_FORMAT) < len(REMOVE_FILES): - REMOVE_FORMAT = REMOVE_FORMAT*len(REMOVE_FILES) + REMOVE_FORMAT = REMOVE_FORMAT * len(REMOVE_FILES) # for each file to be removed - for REMOVE_FILE,REMOVEFORM in zip(REMOVE_FILES,REMOVE_FORMAT): - if REMOVEFORM in ('ascii','netCDF4','HDF5'): + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - Ylms = gravtk.harmonics().from_file(REMOVE_FILE, - format=REMOVEFORM) - elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'): + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,removeform = REMOVEFORM.split('-') + _, removeform = REMOVEFORM.split('-') # index containing files in data format - Ylms = gravtk.harmonics().from_index(REMOVE_FILE, - format=removeform) + Ylms = gravtk.harmonics().from_index( + REMOVE_FILE, format=removeform + ) # reduce to GRACE/GRACE-FO months and truncate to degree and order - Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX,mmax=MMAX) + Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX, mmax=MMAX) # distribute removed Ylms uniformly over the ocean if REDISTRIBUTE_REMOVED: # calculate ratio between total removed mass and # a uniformly distributed cm of water over the ocean - ratio = Ylms.clm[0,0,:]/ocean_Ylms.clm[0,0] + ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0] # for each spherical harmonic - for m in range(0,MMAX+1):# MMAX+1 to include MMAX - for l in range(m,LMAX+1):# LMAX+1 to include LMAX + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX # remove the ratio*ocean Ylms from Ylms # note: x -= y is equivalent to x = x - y - Ylms.clm[l,m,:] -= ratio*ocean_Ylms.clm[l,m] - Ylms.slm[l,m,:] -= ratio*ocean_Ylms.slm[l,m] + Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m] + Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m] # filter removed coefficients if DESTRIPE: Ylms = Ylms.destripe() @@ -458,14 +489,17 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, construct_Ylms.month[:] = np.copy(GRACE_Ylms.month) if RECONSTRUCT: # input index for reconstructed spherical harmonic datafiles - RECONSTRUCT_FILE = pathlib.Path(RECONSTRUCT_FILE).expanduser().absolute() + RECONSTRUCT_FILE = ( + pathlib.Path(RECONSTRUCT_FILE).expanduser().absolute() + ) with RECONSTRUCT_FILE.open(mode='r', encoding='utf8') as f: file_list = [l for l in f.read().splitlines() if parser.match(l)] # for each valid file in the index (iterate over mascons) for reconstruct_file in file_list: # read reconstructed spherical harmonics - Ylms = gravtk.harmonics().from_file(reconstruct_file, - format=DATAFORM) + Ylms = gravtk.harmonics().from_file( + reconstruct_file, format=DATAFORM + ) # truncate clm and slm matrices to LMAX/MMAX # add harmonics object to total construct_Ylms.add(Ylms.truncate(lmax=LMAX, mmax=MMAX)) @@ -490,24 +524,25 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, mascon_name = [] # for each valid file in the index (iterate over mascons) mascon_list = [] - for k,fi in enumerate(mascon_files): + for k, fi in enumerate(mascon_files): # read mascon spherical harmonics - Ylms = gravtk.harmonics().from_file(fi, - format=MASCON_FORMAT, date=False) + Ylms = gravtk.harmonics().from_file( + fi, format=MASCON_FORMAT, date=False + ) # Calculating the total mass of each mascon (1 cmwe uniform) - total_area[k] = 4.0*np.pi*(rad_e**3)*rho_e*Ylms.clm[0,0]/3.0 + total_area[k] = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0 # distribute mascon mass uniformly over the ocean if REDISTRIBUTE_MASCONS: # calculate ratio between total mascon mass and # a uniformly distributed cm of water over the ocean - ratio = Ylms.clm[0,0]/ocean_Ylms.clm[0,0] + ratio = Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0] # for each spherical harmonic - for m in range(0,MMAX+1):# MMAX+1 to include MMAX - for l in range(m,LMAX+1):# LMAX+1 to include LMAX + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX # remove ratio*ocean Ylms from mascon Ylms # note: x -= y is equivalent to x = x - y - Ylms.clm[l,m] -= ratio*ocean_Ylms.clm[l,m] - Ylms.slm[l,m] -= ratio*ocean_Ylms.slm[l,m] + Ylms.clm[l, m] -= ratio * ocean_Ylms.clm[l, m] + Ylms.slm[l, m] -= ratio * ocean_Ylms.slm[l, m] # truncate mascon spherical harmonics to d/o LMAX/MMAX and add to list mascon_list.append(Ylms.truncate(lmax=LMAX, mmax=MMAX)) # stem is the mascon file without directory or suffix @@ -522,8 +557,18 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # calculating GRACE/GRACE-FO error (Wahr et al. 2006) # output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) - fargs = (PROC,DREL,DSET,LMAX,order_str,ds_str,atm_str,GRACE_Ylms.month[0], - GRACE_Ylms.month[-1], suffix[DATAFORM]) + fargs = ( + PROC, + DREL, + DSET, + LMAX, + order_str, + ds_str, + atm_str, + GRACE_Ylms.month[0], + GRACE_Ylms.month[-1], + suffix[DATAFORM], + ) delta_format = '{0}_{1}_{2}_DELTA_CLM_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}' DELTA_FILE = GRACE_Ylms.directory.joinpath(delta_format.format(*fargs)) # check full path of the GRACE directory for delta file @@ -535,38 +580,41 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # Delta coefficients of GRACE time series (Error components) delta_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - delta_Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - delta_Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + delta_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) + delta_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) # Smoothing Half-Width (CNES is a 10-day solution) # All other solutions are monthly solutions (HFWTH for annual = 6) - if ((PROC == 'CNES') and (DREL in ('RL01','RL02'))): + if (PROC == 'CNES') and (DREL in ('RL01', 'RL02')): HFWTH = 19 else: HFWTH = 6 # Equal to the noise of the smoothed time-series # for each spherical harmonic order - for m in range(0,MMAX+1):# MMAX+1 to include MMAX + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX # for each spherical harmonic degree - for l in range(m,LMAX+1):# LMAX+1 to include LMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX # Delta coefficients of GRACE time series - for cs,csharm in enumerate(['clm','slm']): + for cs, csharm in enumerate(['clm', 'slm']): # calculate GRACE Error (Noise of smoothed time-series) # With Annual and Semi-Annual Terms val1 = getattr(GRACE_Ylms, csharm) - smth = gravtk.time_series.smooth(GRACE_Ylms.time, - val1[l,m,:], HFWTH=HFWTH) + smth = gravtk.time_series.smooth( + GRACE_Ylms.time, val1[l, m, :], HFWTH=HFWTH + ) # number of smoothed points nsmth = len(smth['data']) tsmth = np.mean(smth['time']) # GRACE/GRACE-FO delta Ylms # variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) - val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth) + val2[l, m] = np.sqrt(np.sum(smth['noise'] ** 2) / nsmth) # attributes for output files attributes = {} attributes['title'] = 'GRACE/GRACE-FO Spherical Harmonic Errors' - attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # save GRACE/GRACE-FO delta harmonics to file delta_Ylms.time = np.copy(tsmth) delta_Ylms.month = np.int64(nsmth) @@ -577,8 +625,7 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, output_files.append(DELTA_FILE) else: # read GRACE/GRACE-FO delta harmonics from file - delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, - format=DATAFORM) + delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, format=DATAFORM) # truncate GRACE/GRACE-FO delta clm and slm to d/o LMAX/MMAX delta_Ylms = delta_Ylms.truncate(lmax=LMAX, mmax=MMAX) tsmth = np.squeeze(delta_Ylms.time) @@ -586,7 +633,9 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # Calculating the number of cos and sin harmonics between LMIN and LMAX # taking into account MMAX (if MMAX == LMAX then LMAX-MMAX=0) - n_harm=np.int64(LMAX**2 - LMIN**2 + 2*LMAX + 1 - (LMAX-MMAX)**2 - (LMAX-MMAX)) + n_harm = np.int64( + LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX) ** 2 - (LMAX - MMAX) + ) # Initialing harmonics for least squares fitting # mascon kernel @@ -614,7 +663,7 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # Creating column array of clm/slm coefficients # Order is [C00...C6060,S11...S6060] # Switching between Cosine and Sine Stokes - for cs,csharm in enumerate(['clm','slm']): + for cs, csharm in enumerate(['clm', 'slm']): # copy cosine and sin harmonics mascon_harm = getattr(mascon_Ylms, csharm) grace_harm = getattr(GRACE_Ylms, csharm) @@ -624,72 +673,87 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, delta_harm = getattr(delta_Ylms, csharm) # for each spherical harmonic degree # +1 to include LMAX - for l in range(LMIN,LMAX+1): + for l in range(LMIN, LMAX + 1): # for each spherical harmonic order # Sine Stokes for (m=0) = 0 - mm = np.min([MMAX,l]) + mm = np.min([MMAX, l]) # +1 to include l or MMAX (whichever is smaller) - for m in range(cs,mm+1): + for m in range(cs, mm + 1): # Mascon Spherical Harmonics - M_lm[ii,:] = np.copy(mascon_harm[l,m,:]) + M_lm[ii, :] = np.copy(mascon_harm[l, m, :]) # GRACE Spherical Harmonics # Correcting GRACE Harmonics for GIA and Removed Terms - Y_lm[ii,:] = grace_harm[l,m,:] - GIA_harm[l,m,:] - \ - remove_harm[l,m,:] - construct_harm[l,m,:] + Y_lm[ii, :] = ( + grace_harm[l, m, :] + - GIA_harm[l, m, :] + - remove_harm[l, m, :] + - construct_harm[l, m, :] + ) # GRACE delta spherical harmonics - delta_lm[ii] = np.copy(delta_harm[l,m]) + delta_lm[ii] = np.copy(delta_harm[l, m]) # degree dependent factor to convert to mass - fact[ii] = (2.0*l + 1.0)/(1.0 + LOVE.kl[l]) + fact[ii] = (2.0 * l + 1.0) / (1.0 + LOVE.kl[l]) # degree dependent smoothing wt_lm[ii] = np.copy(wt[l]) # add 1 to counter ii += 1 # Converting mascon coefficients to fit method - if (FIT_METHOD == 1): + if FIT_METHOD == 1: # Fitting Sensitivity Kernel as mass coefficients # converting M_lm to mass coefficients of the kernel for i in range(n_harm): - MA_lm[i,:] = M_lm[i,:]*wt_lm[i]*fact[i] - fit_factor = wt_lm*fact - elif (FIT_METHOD == 2): + MA_lm[i, :] = M_lm[i, :] * wt_lm[i] * fact[i] + fit_factor = wt_lm * fact + elif FIT_METHOD == 2: # Fitting Sensitivity Kernel as geoid coefficients for i in range(n_harm): - MA_lm[:,:] = M_lm[i,:]*wt_lm[i] - fit_factor = wt_lm*np.ones((n_harm)) + MA_lm[:, :] = M_lm[i, :] * wt_lm[i] + fit_factor = wt_lm * np.ones((n_harm)) # Fitting the sensitivity kernel from the input kernel for i in range(n_harm): # setting kern_i equal to 1 for d/o kern_i = np.zeros((n_harm)) # converting to mass coefficients if specified - kern_i[i] = 1.0*fit_factor[i] + kern_i[i] = 1.0 * fit_factor[i] # spherical harmonics solution for the # mascon sensitivity kernels - if (SOLVER == 'inv'): + if SOLVER == 'inv': kern_lm = np.dot(np.linalg.inv(MA_lm), kern_i) - elif (SOLVER == 'lstsq'): + elif SOLVER == 'lstsq': kern_lm = np.linalg.lstsq(MA_lm, kern_i, rcond=-1)[0] elif SOLVER in ('gelsd', 'gelsy', 'gelss'): - kern_lm, res, rnk, s = scipy.linalg.lstsq(MA_lm, kern_i, - lapack_driver=SOLVER) + kern_lm, res, rnk, s = scipy.linalg.lstsq( + MA_lm, kern_i, lapack_driver=SOLVER + ) # calculate the sensitivity kernel for each mascon for k in range(n_mas): - A_lm[i,k] = kern_lm[k]*total_area[k] + A_lm[i, k] = kern_lm[k] * total_area[k] # for each mascon for k in range(n_mas): # Multiply the Satellite error (noise of a smoothed time-series # with annual and semi-annual components) by the sensitivity kernel # Converting to Gigatonnes - M_delta[k] = np.sqrt(np.sum((delta_lm*A_lm[:,k])**2))/1e15 + M_delta[k] = np.sqrt(np.sum((delta_lm * A_lm[:, k]) ** 2)) / 1e15 # output filename format (for both LMAX==MMAX and LMAX != MMAX cases): # mascon name, GRACE dataset, GIA model, LMAX, (MMAX,) # Gaussian smoothing, filter flag, remove reconstructed fields flag # output GRACE error file - fargs = (mascon_name[k], dset_str, gia_str.upper(), atm_str, ocean_str, - LMAX, order_str, gw_str, ds_str, construct_str) + fargs = ( + mascon_name[k], + dset_str, + gia_str.upper(), + atm_str, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + construct_str, + ) file_format = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}{9}.txt' output_file = OUTPUT_DIRECTORY.joinpath(file_format.format(*fargs)) @@ -700,14 +764,20 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, fid = output_file.open(mode='w', encoding='utf8') # for each date formatting_string = '{0:03d} {1:12.4f} {2:16.10f} {3:16.10f} {4:16.5f}' - for t,mon in enumerate(GRACE_Ylms.month): + for t, mon in enumerate(GRACE_Ylms.month): # Summing over all spherical harmonics for mascon k, and time t # multiplies by the degree dependent factor to convert # the harmonics into mass coefficients # Converting mascon mass time-series from g to gigatonnes - mascon[k,t] = np.sum(A_lm[:,k]*Y_lm[:,t])/1e15 + mascon[k, t] = np.sum(A_lm[:, k] * Y_lm[:, t]) / 1e15 # output to file - args=(mon,GRACE_Ylms.time[t],mascon[k,t],M_delta[k],total_area[k]/1e10) + args = ( + mon, + GRACE_Ylms.time[t], + mascon[k, t], + M_delta[k], + total_area[k] / 1e10, + ) print(formatting_string.format(*args), file=fid) # close the output file fid.close() @@ -719,10 +789,11 @@ def calc_mascon(base_dir, PROC, DREL, DSET, LMAX, RAD, # return the list of output files return output_files + # PURPOSE: print a file log for the GRACE mascon analysis def output_log_file(input_arguments, output_files): # format: calc_mascon_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'calc_mascon_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -739,10 +810,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE mascon analysis def output_error_log_file(input_arguments): # format: calc_mascon_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'calc_mascon_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -758,6 +830,7 @@ def output_error_log_file(input_arguments): # close the log file fid.close() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -765,74 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') - parser.add_argument('--output-directory','-O', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Output directory for mascon files') + 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', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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' @@ -848,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/ @@ -874,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] @@ -1034,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 f3563e2f..c6e9e215 100644 --- a/scripts/calc_sensitivity_kernel.py +++ b/scripts/calc_sensitivity_kernel.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" calc_sensitivity_kernel.py Written by Tyler Sutterley (05/2023) @@ -145,6 +145,7 @@ Updated 03/2012: edited to use new gen_stokes time-series option Written 02/2012 """ + from __future__ import print_function, division import sys @@ -159,6 +160,7 @@ import scipy.linalg import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -168,9 +170,12 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: calculate a regional time-series through a least # squares mascon process -def calc_sensitivity_kernel(LMAX, RAD, +def calc_sensitivity_kernel( + LMAX, + RAD, LMIN=None, MMAX=None, LOVE_NUMBERS=0, @@ -186,8 +191,8 @@ def calc_sensitivity_kernel(LMAX, RAD, INTERVAL=None, BOUNDS=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # file information suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5')[DATAFORM] # file parser for reading index files @@ -204,8 +209,9 @@ def calc_sensitivity_kernel(LMAX, RAD, output_files = [] # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) @@ -219,19 +225,18 @@ def calc_sensitivity_kernel(LMAX, RAD, order_str = f'M{MMAX:d}' if (MMAX != LMAX) else '' # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) gw_str = '' # Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_MASCONS: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) ocean_str = '_OCN' else: # not distributing uniformly over ocean @@ -249,24 +254,23 @@ def calc_sensitivity_kernel(LMAX, RAD, mascon_name = [] # for each valid file in the index (iterate over mascons) mascon_list = [] - for k,fi in enumerate(mascon_files): + for k, fi in enumerate(mascon_files): # read mascon spherical harmonics - Ylms = gravtk.harmonics().from_file(fi, - format=DATAFORM, date=False) + Ylms = gravtk.harmonics().from_file(fi, format=DATAFORM, date=False) # Calculating the total mass of each mascon (1 cmwe uniform) - total_area[k] = 4.0*np.pi*(rad_e**3)*rho_e*Ylms.clm[0,0]/3.0 + total_area[k] = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0 # distribute mascon mass uniformly over the ocean if REDISTRIBUTE_MASCONS: # calculate ratio between total mascon mass and # a uniformly distributed cm of water over the ocean - ratio = Ylms.clm[0,0]/ocean_Ylms.clm[0,0] + ratio = Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0] # for each spherical harmonic - for m in range(0,MMAX+1):# MMAX+1 to include MMAX - for l in range(m,LMAX+1):# LMAX+1 to include LMAX + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX # remove ratio*ocean Ylms from mascon Ylms # note: x -= y is equivalent to x = x - y - Ylms.clm[l,m] -= ratio*ocean_Ylms.clm[l,m] - Ylms.slm[l,m] -= ratio*ocean_Ylms.slm[l,m] + Ylms.clm[l, m] -= ratio * ocean_Ylms.clm[l, m] + Ylms.slm[l, m] -= ratio * ocean_Ylms.slm[l, m] # truncate mascon spherical harmonics to d/o LMAX/MMAX and add to list mascon_list.append(Ylms.truncate(lmax=LMAX, mmax=MMAX)) # stem is the mascon file without directory or suffix @@ -281,7 +285,9 @@ def calc_sensitivity_kernel(LMAX, RAD, # Calculating the number of cos and sin harmonics between LMIN and LMAX # taking into account MMAX (if MMAX == LMAX then LMAX-MMAX=0) - n_harm=np.int64(LMAX**2 - LMIN**2 + 2*LMAX + 1 - (LMAX-MMAX)**2 - (LMAX-MMAX)) + n_harm = np.int64( + LMAX**2 - LMIN**2 + 2 * LMAX + 1 - (LMAX - MMAX) ** 2 - (LMAX - MMAX) + ) # Initialing harmonics for least squares fitting # mascon kernel @@ -303,42 +309,42 @@ def calc_sensitivity_kernel(LMAX, RAD, # Creating column array of clm/slm coefficients # Order is [C00...C6060,S11...S6060] # Switching between Cosine and Sine Stokes - for cs,csharm in enumerate(['clm','slm']): + for cs, csharm in enumerate(['clm', 'slm']): # copy cosine and sin harmonics mascon_harm = getattr(mascon_Ylms, csharm) # for each spherical harmonic degree # +1 to include LMAX - for l in range(LMIN,LMAX+1): + for l in range(LMIN, LMAX + 1): # for each spherical harmonic order # Sine Stokes for (m=0) = 0 - mm = np.min([MMAX,l]) + mm = np.min([MMAX, l]) # +1 to include l or MMAX (whichever is smaller) - for m in range(cs,mm+1): + for m in range(cs, mm + 1): # Mascon Spherical Harmonics - M_lm[ii,:] = np.copy(mascon_harm[l,m,:]) + M_lm[ii, :] = np.copy(mascon_harm[l, m, :]) # degree dependent factor to convert to mass - fact[ii] = (2.0*l + 1.0)/(1.0 + LOVE.kl[l]) + fact[ii] = (2.0 * l + 1.0) / (1.0 + LOVE.kl[l]) # degree dependent factor to convert from mass - coeff_inv = 0.75/(np.pi*rho_e*rad_e**3) - fact_inv[ii] = coeff_inv*(1.0 + LOVE.kl[l])/(2.0*l + 1.0) + coeff_inv = 0.75 / (np.pi * rho_e * rad_e**3) + fact_inv[ii] = coeff_inv * (1.0 + LOVE.kl[l]) / (2.0 * l + 1.0) # degree dependent smoothing wt_lm[ii] = np.copy(wt[l]) # add 1 to counter ii += 1 # Converting mascon coefficients to fit method - if (FIT_METHOD == 1): + if FIT_METHOD == 1: # Fitting Sensitivity Kernel as mass coefficients # converting M_lm to mass coefficients of the kernel for i in range(n_harm): - MA_lm[i,:] = M_lm[i,:]*wt_lm[i]*fact[i] - fit_factor = wt_lm*fact + MA_lm[i, :] = M_lm[i, :] * wt_lm[i] * fact[i] + fit_factor = wt_lm * fact inv_fit_factor = np.copy(fact_inv) - elif (FIT_METHOD == 2): + elif FIT_METHOD == 2: # Fitting Sensitivity Kernel as geoid coefficients for i in range(n_harm): - MA_lm[:,:] = M_lm[i,:]*wt_lm[i] - fit_factor = wt_lm*np.ones((n_harm)) + MA_lm[:, :] = M_lm[i, :] * wt_lm[i] + fit_factor = wt_lm * np.ones((n_harm)) inv_fit_factor = np.ones((n_harm)) # Fitting the sensitivity kernel from the input kernel @@ -346,19 +352,20 @@ def calc_sensitivity_kernel(LMAX, RAD, # setting kern_i equal to 1 for d/o kern_i = np.zeros((n_harm)) # converting to mass coefficients if specified - kern_i[i] = 1.0*fit_factor[i] + kern_i[i] = 1.0 * fit_factor[i] # spherical harmonics solution for the # mascon sensitivity kernels - if (SOLVER == 'inv'): + if SOLVER == 'inv': kern_lm = np.dot(np.linalg.inv(MA_lm), kern_i) - elif (SOLVER == 'lstsq'): + elif SOLVER == 'lstsq': kern_lm = np.linalg.lstsq(MA_lm, kern_i, rcond=-1)[0] elif SOLVER in ('gelsd', 'gelsy', 'gelss'): - kern_lm, res, rnk, s = scipy.linalg.lstsq(MA_lm, kern_i, - lapack_driver=SOLVER) + kern_lm, res, rnk, s = scipy.linalg.lstsq( + MA_lm, kern_i, lapack_driver=SOLVER + ) # calculate the sensitivity kernel for each mascon for k in range(n_mas): - A_lm[i,k] = kern_lm[k]*total_area[k] + A_lm[i, k] = kern_lm[k] * total_area[k] # free up larger variables del M_lm, MA_lm, wt_lm, fact, fact_inv, fit_factor @@ -367,24 +374,24 @@ def calc_sensitivity_kernel(LMAX, RAD, # kernel calculated as outlined in Tiwari (2009) and Jacobs (2012) # Initializing output sensitivity kernel (both spatial and Ylms) kern_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - kern_Ylms.clm = np.zeros((LMAX+1, MMAX+1, n_mas)) - kern_Ylms.slm = np.zeros((LMAX+1, MMAX+1, n_mas)) + kern_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1, n_mas)) + kern_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1, n_mas)) kern_Ylms.time = np.copy(total_area) # counter variable for deconstructing the mascon column arrays ii = 0 # Switching between Cosine and Sine Stokes - for cs,csharm in enumerate(['clm','slm']): + for cs, csharm in enumerate(['clm', 'slm']): # for each spherical harmonic degree # +1 to include LMAX - for l in range(LMIN,LMAX+1): + for l in range(LMIN, LMAX + 1): # for each spherical harmonic order # Sine Stokes for (m=0) = 0 - mm = np.min([MMAX,l]) + mm = np.min([MMAX, l]) # +1 to include l or MMAX (whichever is smaller) - for m in range(cs,mm+1): + for m in range(cs, mm + 1): # inv_fit_factor: normalize from mass harmonics temp = getattr(kern_Ylms, csharm) - temp[l,m,:] = inv_fit_factor[ii]*A_lm[ii,:] + temp[l, m, :] = inv_fit_factor[ii] * A_lm[ii, :] # add 1 to counter ii += 1 # free up larger variables @@ -398,11 +405,10 @@ def calc_sensitivity_kernel(LMAX, RAD, # get harmonics for mascon Ylms = kern_Ylms.index(k, date=False) # output sensitivity kernel to file - args = (mascon_name[k],ocean_str,LMAX,order_str,gw_str,suffix) + args = (mascon_name[k], ocean_str, LMAX, order_str, gw_str, suffix) FILE1 = '{0}_SKERNEL_CLM{1}_L{2:d}{3}{4}.{5}'.format(*args) output_file = OUTPUT_DIRECTORY.joinpath(FILE1) - Ylms.to_file(output_file, format=DATAFORM, - date=False, **attributes) + Ylms.to_file(output_file, format=DATAFORM, date=False, **attributes) # change the permissions mode output_file.chmod(mode=MODE) # add output files to list object @@ -418,30 +424,34 @@ 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) # Computing plms for converting to spatial domain - theta = (90.0-grid.lat)*np.pi/180.0 + theta = np.radians(90.0 - grid.lat) PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(theta)) # for each mascon @@ -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,111 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Output directory for mascon files') + parser.add_argument( + '--output-directory', + '-O', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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] @@ -637,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 17df8b35..a7269579 100644 --- a/scripts/combine_harmonics.py +++ b/scripts/combine_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" combine_harmonics.py Written by Tyler Sutterley (10/2023) Converts a file from the spherical harmonic domain into the spatial domain @@ -108,6 +108,7 @@ can output a non-global grid by setting bounding box parameters Written 07/2018 """ + from __future__ import print_function import sys @@ -121,6 +122,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -130,8 +132,11 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: converts from the spherical harmonic domain into the spatial domain -def combine_harmonics(INPUT_FILE, OUTPUT_FILE, +def combine_harmonics( + INPUT_FILE, + OUTPUT_FILE, LMAX=None, MMAX=None, LOVE_NUMBERS=0, @@ -147,8 +152,8 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, MEAN_FILE=None, DATAFORM=None, DATE=False, - MODE=0o775): - + MODE=0o775, +): # verify inputs INPUT_FILE = pathlib.Path(INPUT_FILE).expanduser().absolute() OUTPUT_FILE = pathlib.Path(OUTPUT_FILE).expanduser().absolute() @@ -157,7 +162,9 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, # attributes for output files attributes = dict(ROOT={}) attributes['ROOT']['product_type'] = 'gravity_field' - attributes['ROOT']['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['ROOT']['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # upper bound of spherical harmonic orders (default = LMAX) if MMAX is None: @@ -166,14 +173,16 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, # read input spherical harmonic coefficients from file if DATAFORM in ('ascii', 'netCDF4', 'HDF5'): dataform = copy.copy(DATAFORM) - input_Ylms = gravtk.harmonics().from_file(INPUT_FILE, - format=DATAFORM, date=DATE) + input_Ylms = gravtk.harmonics().from_file( + INPUT_FILE, format=DATAFORM, date=DATE + ) attributes['ROOT']['lineage'] = input_Ylms.filename.name elif DATAFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,dataform = DATAFORM.split('-') - input_Ylms = gravtk.harmonics().from_index(INPUT_FILE, - format=dataform, date=DATE) + _, dataform = DATAFORM.split('-') + input_Ylms = gravtk.harmonics().from_index( + INPUT_FILE, format=dataform, date=DATE + ) attributes['ROOT']['lineage'] = [f.name for f in input_Ylms.filename] # reform harmonic dimensions to be l,m,t # truncate to degree and order LMAX, MMAX @@ -181,13 +190,15 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, # remove mean file from input Ylms if MEAN_FILE: - mean_Ylms = gravtk.harmonics().from_file(MEAN_FILE, - format=DATAFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=DATAFORM, date=False + ) input_Ylms.subtract(mean_Ylms) # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth parameters attributes['ROOT']['earth_model'] = LOVE.model attributes['ROOT']['earth_love_numbers'] = LOVE.citation @@ -199,28 +210,27 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, # distribute total mass uniformly over the ocean if REDISTRIBUTE: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) # calculate ratio between total mass and a uniformly distributed # layer of water over the ocean - ratio = input_Ylms.clm[0,0,:]/ocean_Ylms.clm[0,0] + ratio = input_Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0] # for each spherical harmonic - for m in range(0,MMAX+1):# MMAX+1 to include MMAX - for l in range(m,LMAX+1):# LMAX+1 to include LMAX + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX # remove the ratio*ocean Ylms from Ylms # note: x -= y is equivalent to x = x - y - input_Ylms.clm[l,m,:] -= ratio*ocean_Ylms.clm[l,m] - input_Ylms.slm[l,m,:] -= ratio*ocean_Ylms.slm[l,m] + input_Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m] + input_Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m] # if using a decorrelation filter (Isabella's destriping Routine) if DESTRIPE: input_Ylms = input_Ylms.destripe() # Gaussian smoothing - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) else: - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) # Output spatial data grid = gravtk.spatial() @@ -229,25 +239,25 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, nt = len(input_Ylms.time) # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output Degree Interval - if (INTERVAL == 1): + if INTERVAL == 1: # (0:360,90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - grid.lon = dlon*np.arange(0,nlon) - grid.lat = 90.0 - dlat*np.arange(0,nlat) - elif (INTERVAL == 2): + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + grid.lon = dlon * np.arange(0, nlon) + grid.lat = 90.0 - dlat * np.arange(0, nlat) + elif INTERVAL == 2: # (Degree spacing)/2 - grid.lon = np.arange(dlon/2.0,360+dlon/2.0,dlon) - grid.lat = np.arange(90.0-dlat/2.0,-90.0-dlat/2.0,-dlat) + grid.lon = np.arange(dlon / 2.0, 360 + dlon / 2.0, dlon) + grid.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat) nlon = len(grid.lon) nlat = len(grid.lat) - elif (INTERVAL == 3): + elif INTERVAL == 3: # non-global grid set with BOUNDS parameter - minlon,maxlon,minlat,maxlat = BOUNDS.copy() - grid.lon = np.arange(minlon+dlon/2.0, maxlon+dlon/2.0, dlon) - grid.lat = np.arange(maxlat-dlat/2.0, minlat-dlat/2.0, -dlat) + minlon, maxlon, minlat, maxlat = BOUNDS.copy() + grid.lon = np.arange(minlon + dlon / 2.0, maxlon + dlon / 2.0, dlon) + grid.lat = np.arange(maxlat - dlat / 2.0, minlat - dlat / 2.0, -dlat) nlon = len(grid.lon) nlat = len(grid.lat) # output spatial grid @@ -273,126 +283,207 @@ def combine_harmonics(INPUT_FILE, OUTPUT_FILE, attributes['ROOT']['earth_gravity_constant'] = f'{factors.GM:0.3f} cm^3/s^2' # Computing plms for converting to spatial domain - theta = (90.0 - grid.lat)*np.pi/180.0 + theta = np.radians(90.0 - grid.lat) 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 482ade8c..00342b1b 100644 --- a/scripts/convert_harmonics.py +++ b/scripts/convert_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" convert_harmonics.py Written by Tyler Sutterley (10/2023) Converts a file from the spatial domain into the spherical harmonic domain @@ -87,6 +87,7 @@ Updated 04/2020: updates to reading load love numbers Written 10/2019 """ + from __future__ import print_function import sys @@ -100,6 +101,7 @@ import numpy as np import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -109,8 +111,11 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: converts from the spatial domain into the spherical harmonic domain -def convert_harmonics(INPUT_FILE, OUTPUT_FILE, +def convert_harmonics( + INPUT_FILE, + OUTPUT_FILE, LMAX=None, MMAX=None, UNITS=None, @@ -122,8 +127,8 @@ def convert_harmonics(INPUT_FILE, OUTPUT_FILE, HEADER=None, DATAFORM=None, DATE=False, - MODE=0o775): - + MODE=0o775, +): # verify inputs INPUT_FILE = pathlib.Path(INPUT_FILE).expanduser().absolute() OUTPUT_FILE = pathlib.Path(OUTPUT_FILE).expanduser().absolute() @@ -140,38 +145,50 @@ def convert_harmonics(INPUT_FILE, OUTPUT_FILE, MMAX = np.copy(LMAX) # Grid spacing - dlon,dlat = (DDEG,DDEG) if (np.ndim(DDEG) == 0) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG, DDEG) if (np.ndim(DDEG) == 0) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # read spatial file in data format # expand dimensions - if (DATAFORM == 'ascii'): + if DATAFORM == 'ascii': # ascii (.txt) - input_spatial = gravtk.spatial(fill_value=FILL_VALUE).from_ascii( - INPUT_FILE, header=HEADER, spacing=[dlon,dlat], nlat=nlat, - nlon=nlon, date=DATE).expand_dims() - elif (DATAFORM == 'netCDF4'): + input_spatial = ( + gravtk.spatial(fill_value=FILL_VALUE) + .from_ascii( + INPUT_FILE, + header=HEADER, + spacing=[dlon, dlat], + nlat=nlat, + nlon=nlon, + date=DATE, + ) + .expand_dims() + ) + elif DATAFORM == 'netCDF4': # netcdf (.nc) - input_spatial = gravtk.spatial().from_netCDF4( - INPUT_FILE, date=DATE).expand_dims() - elif (DATAFORM == 'HDF5'): + input_spatial = ( + gravtk.spatial().from_netCDF4(INPUT_FILE, date=DATE).expand_dims() + ) + elif DATAFORM == 'HDF5': # HDF5 (.H5) - input_spatial = gravtk.spatial().from_HDF5( - INPUT_FILE, date=DATE).expand_dims() + input_spatial = ( + gravtk.spatial().from_HDF5(INPUT_FILE, date=DATE).expand_dims() + ) # convert missing values to zero input_spatial.replace_invalid(0.0) # input data shape nlat, nlon, nt = input_spatial.shape # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth parameters attributes['earth_model'] = LOVE.model attributes['earth_love_numbers'] = LOVE.citation @@ -187,16 +204,24 @@ def convert_harmonics(INPUT_FILE, OUTPUT_FILE, attributes['earth_gravity_constant'] = f'{factors.GM:0.3f} cm^3/s^2' # calculate associated Legendre polynomials - th = (90.0 - input_spatial.lat)*np.pi/180.0 + th = np.radians(90.0 - input_spatial.lat) PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(th)) # 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 87456404..770d9498 100644 --- a/scripts/grace_mean_harmonics.py +++ b/scripts/grace_mean_harmonics.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_mean_harmonics.py Written by Tyler Sutterley (05/2023) @@ -110,6 +110,7 @@ with the multiprocessing module Written 05/2014 """ + from __future__ import print_function import sys @@ -123,6 +124,7 @@ import collections import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -132,9 +134,15 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: import GRACE/GRACE-FO files for a given months range # calculate the mean of the spherical harmonics and output to file -def grace_mean_harmonics(base_dir, PROC, DREL, DSET, LMAX, +def grace_mean_harmonics( + base_dir, + PROC, + DREL, + DSET, + LMAX, START=None, END=None, MISSING=None, @@ -153,8 +161,8 @@ def grace_mean_harmonics(base_dir, PROC, DREL, DSET, LMAX, MEAN_FILE=None, MEANFORM=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # input directory setup base_dir = pathlib.Path(base_dir).expanduser().absolute() @@ -173,17 +181,34 @@ def grace_mean_harmonics(base_dir, PROC, DREL, DSET, LMAX, attributes['max_order'] = MMAX # data formats for output: ascii, netCDF4, HDF5, gfc - suffix = dict(ascii='txt',netCDF4='nc',HDF5='H5',gfc='gfc')[MEANFORM] + suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5', gfc='gfc')[MEANFORM] # reading GRACE months for input date range # replacing low-degree harmonics with SLR values if specified # include degree 1 (geocenter) harmonics if specified # correcting for Pole Tide Drift and Atmospheric Jumps if specified - input_Ylms = gravtk.grace_input_months(base_dir, PROC, DREL, DSET, LMAX, - START, END, MISSING, SLR_C20, DEG1, MMAX=MMAX, SLR_21=SLR_21, - SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + input_Ylms = gravtk.grace_input_months( + base_dir, + PROC, + DREL, + DSET, + LMAX, + START, + END, + MISSING, + SLR_C20, + DEG1, + MMAX=MMAX, + SLR_21=SLR_21, + SLR_22=SLR_22, + SLR_C30=SLR_C30, + SLR_C40=SLR_C40, + SLR_C50=SLR_C50, + DEG1_FILE=DEG1_FILE, + MODEL_DEG1=MODEL_DEG1, + ATM=ATM, + POLE_TIDE=POLE_TIDE, + ) grace_Ylms = gravtk.harmonics().from_dict(input_Ylms) # descriptor string for processing parameters grace_str = input_Ylms['title'] @@ -194,13 +219,25 @@ def grace_mean_harmonics(base_dir, PROC, DREL, DSET, LMAX, # number of months nt = grace_Ylms.shape[-1] # calculate RMS of harmonic errors - mean_Ylms.eclm = np.sqrt(np.sum(input_Ylms['eclm']**2,axis=2)/nt) - mean_Ylms.eslm = np.sqrt(np.sum(input_Ylms['eslm']**2,axis=2)/nt) + mean_Ylms.eclm = np.sqrt(np.sum(input_Ylms['eclm'] ** 2, axis=2) / nt) + mean_Ylms.eslm = np.sqrt(np.sum(input_Ylms['eslm'] ** 2, axis=2) / nt) # default output filename if not entering via parameter file if not MEAN_FILE: - DIRECTORY = pathlib.Path(input_Ylms['directory']).expanduser().absolute() - args = (PROC,DREL,DSET,grace_str,LMAX,order_str,START,END,suffix) + DIRECTORY = ( + pathlib.Path(input_Ylms['directory']).expanduser().absolute() + ) + args = ( + PROC, + DREL, + DSET, + grace_str, + LMAX, + order_str, + START, + END, + suffix, + ) file_format = '{0}_{1}_{2}_MEAN_CLM{3}_L{4:d}{5}_{6:03d}-{7:03d}.{8}' MEAN_FILE = DIRECTORY.joinpath(file_format.format(*args)) else: @@ -210,33 +247,35 @@ def grace_mean_harmonics(base_dir, PROC, DREL, DSET, LMAX, DIRECTORY.mkdir(mode=MODE, parents=True, exist_ok=True) # output spherical harmonics for the static field - if (MEANFORM == 'gfc'): + if MEANFORM == 'gfc': # output mean field to gfc format mean_Ylms.attributes['ROOT'] = attributes mean_Ylms.to_gfc(MEAN_FILE, verbose=VERBOSE) else: # add attributes from input GRACE fields attributes.update(input_Ylms.get('attributes')) - attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # output mean field to specified file format mean_Ylms.attributes['ROOT'] = attributes - mean_Ylms.to_file(MEAN_FILE, format=MEANFORM, - verbose=VERBOSE) + mean_Ylms.to_file(MEAN_FILE, format=MEANFORM, verbose=VERBOSE) # change the permissions mode MEAN_FILE.chmod(mode=MODE) # return the output file return MEAN_FILE + # PURPOSE: additional routines for the harmonics module class mean(gravtk.harmonics): def __init__(self, **kwargs): super().__init__(**kwargs) - self.center=None - self.release='RLxx' - self.product=None - self.eclm=None - self.eslm=None + self.center = None + self.release = 'RLxx' + self.product = None + self.eclm = None + self.eslm = None def from_harmonics(self, temp): """ @@ -244,8 +283,16 @@ def from_harmonics(self, temp): """ self = mean(lmax=temp.lmax, mmax=temp.mmax) # try to assign variables to self - for key in ['clm','slm','eclm','eslm','filename', - 'center','release','product']: + for key in [ + 'clm', + 'slm', + 'eclm', + 'eslm', + 'filename', + 'center', + 'release', + 'product', + ]: try: val = getattr(temp, key) setattr(self, key, np.copy(val)) @@ -265,20 +312,28 @@ def to_gfc(self, filename, **kwargs): """ self.filename = pathlib.Path(filename).expanduser().absolute() # set default verbosity - kwargs.setdefault('verbose',False) + kwargs.setdefault('verbose', False) logging.info(str(self.filename)) # open the output file fid = self.filename.open(mode='w', encoding='utf8') # print the header informat self.print_header(fid) # output file format - file_format = ('{0:3} {1:4d} {2:4d} {3:+18.12E} {4:+18.12E} ' - '{5:11.5E} {6:11.5E}') + file_format = ( + '{0:3} {1:4d} {2:4d} {3:+18.12E} {4:+18.12E} {5:11.5E} {6:11.5E}' + ) # write to file for each spherical harmonic degree and order - for m in range(0, self.mmax+1): - for l in range(m, self.lmax+1): - args = ('gfc', l, m, self.clm[l,m], self.slm[l,m], - self.eclm[l,m], self.eslm[l,m]) + for m in range(0, self.mmax + 1): + for l in range(m, self.lmax + 1): + args = ( + 'gfc', + l, + m, + self.clm[l, m], + self.slm[l, m], + self.eclm[l, m], + self.eslm[l, m], + ) print(file_format.format(*args), file=fid) # close the output file fid.close() @@ -286,22 +341,26 @@ def to_gfc(self, filename, **kwargs): # PURPOSE: print gfc header to top of file def print_header(self, fid): # print header - fid.write('{0} {1}\n'.format('begin_of_head',73*'=')) - for att_name,att_val in self.attributes['ROOT'].items(): + fid.write('{0} {1}\n'.format('begin_of_head', 73 * '=')) + for att_name, att_val in self.attributes['ROOT'].items(): fid.write('{0:30}{1}\n'.format(att_name, att_val)) - fid.write('{0:30}{1:+16.10E}\n'.format('earth_gravity_constant', - 3.986004415E+14)) - fid.write('{0:30}{1:+16.10E}\n'.format('radius',6.378136300E+06)) - fid.write('{0:30}{1}\n'.format('errors','uncalibrated')) - fid.write('{0:30}{1}\n'.format('norm','fully_normalized')) - args = ('key','L','M','C','S','sigma C','sigma S') + fid.write( + '{0:30}{1:+16.10E}\n'.format( + 'earth_gravity_constant', 3.986004415e14 + ) + ) + fid.write('{0:30}{1:+16.10E}\n'.format('radius', 6.378136300e06)) + fid.write('{0:30}{1}\n'.format('errors', 'uncalibrated')) + fid.write('{0:30}{1}\n'.format('norm', 'fully_normalized')) + args = ('key', 'L', 'M', 'C', 'S', 'sigma C', 'sigma S') fid.write('\n{0:7}{1:5}{2:10}{3:20}{4:15}{5:13}{6:7}\n'.format(*args)) - fid.write('{0} {1}\n'.format('end_of_head',75*'=')) + fid.write('{0} {1}\n'.format('end_of_head', 75 * '=')) + # PURPOSE: print a file log for the GRACE/GRACE-FO mean program def output_log_file(input_arguments, output_file): # format: GRACE_mean_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_mean_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.directory) @@ -317,10 +376,11 @@ def output_log_file(input_arguments, output_file): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE/GRACE-FO mean program def output_error_log_file(input_arguments): # format: GRACE_mean_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_mean_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.directory) @@ -336,59 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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/ @@ -400,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] @@ -492,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 af4db8ec..b3507d3a 100644 --- a/scripts/grace_raster_grids.py +++ b/scripts/grace_raster_grids.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_raster_grids.py Written by Tyler Sutterley (06/2024) @@ -151,6 +151,7 @@ Updated 03/2024: increase mask buffer to twice the smoothing radius Written 08/2023 """ + from __future__ import print_function import sys @@ -168,6 +169,7 @@ geoidtk = gravtk.utilities.import_dependency('geoid_toolkit') pyproj = gravtk.utilities.import_dependency('pyproj') + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -177,28 +179,36 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: try to get the projection information def get_projection(PROJECTION): # EPSG projection code try: crs = pyproj.CRS.from_epsg(int(PROJECTION)) - except (ValueError,pyproj.exceptions.CRSError): + except (ValueError, pyproj.exceptions.CRSError): pass else: return crs # coordinate reference system string try: crs = pyproj.CRS.from_string(PROJECTION) - except (ValueError,pyproj.exceptions.CRSError): + except (ValueError, pyproj.exceptions.CRSError): pass else: return crs # no projection can be made raise pyproj.exceptions.CRSError + # PURPOSE: import GRACE/GRACE-FO files for a given months range # Converts the GRACE/GRACE-FO harmonics applying the specified procedures -def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, +def grace_raster_grids( + base_dir, + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -233,8 +243,8 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, LANDMASK=None, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, - MODE=0o775): - + MODE=0o775, +): # recursively create output directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -247,7 +257,9 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, attributes['ROOT']['product_name'] = DSET attributes['ROOT']['product_type'] = 'gravity_field' attributes['ROOT']['title'] = 'GRACE/GRACE-FO Spatial Data' - attributes['ROOT']['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['ROOT']['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # list object of output files for file logs (full path) output_files = [] @@ -255,15 +267,16 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, suffix = dict(netCDF4='nc', HDF5='H5')[DATAFORM] # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth model and love numbers attributes['ROOT']['earth_model'] = LOVE.model attributes['ROOT']['earth_love_numbers'] = LOVE.citation attributes['ROOT']['reference_frame'] = LOVE.reference # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): + if RAD != 0: gw_str = f'_r{RAD:0.0f}km' attributes['ROOT']['smoothing_radius'] = f'{RAD:0.0f} km' else: @@ -281,11 +294,28 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # replacing low-degree harmonics with SLR values if specified # include degree 1 (geocenter) harmonics if specified # correcting for Pole-Tide and Atmospheric Jumps if specified - Ylms = gravtk.grace_input_months(base_dir, PROC, DREL, DSET, LMAX, - START, END, MISSING, SLR_C20, DEG1, MMAX=MMAX, SLR_21=SLR_21, - SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + Ylms = gravtk.grace_input_months( + base_dir, + PROC, + DREL, + DSET, + LMAX, + START, + END, + MISSING, + SLR_C20, + DEG1, + MMAX=MMAX, + SLR_21=SLR_21, + SLR_22=SLR_22, + SLR_C30=SLR_C30, + SLR_C40=SLR_C40, + SLR_C50=SLR_C50, + DEG1_FILE=DEG1_FILE, + MODEL_DEG1=MODEL_DEG1, + ATM=ATM, + POLE_TIDE=POLE_TIDE, + ) # convert to harmonics object and remove mean if specified GRACE_Ylms = gravtk.harmonics().from_dict(Ylms) nt = len(GRACE_Ylms.time) @@ -297,8 +327,9 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, if MEAN_FILE: # read data form for input mean file (ascii, netCDF4, HDF5, gfc) MEAN_FILE = pathlib.Path(MEAN_FILE).expanduser().absolute() - mean_Ylms = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GRACE_Ylms.subtract(mean_Ylms) attributes['ROOT']['lineage'].append(MEAN_FILE.name) @@ -331,17 +362,15 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # default file prefix if not FILE_PREFIX: - fargs = (PROC,DREL,DSET,Ylms['title'],gia_str) + fargs = (PROC, DREL, DSET, Ylms['title'], gia_str) FILE_PREFIX = '{0}_{1}_{2}{3}{4}_'.format(*fargs) # read Land-Sea Mask and convert to spherical harmonics - land_Ylms = gravtk.land_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + land_Ylms = gravtk.land_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) # Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_REMOVED: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) ocean_str = '_OCN' else: ocean_str = '' @@ -354,37 +383,41 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, if REMOVE_FILES: # extend list if a single format was entered for all files if len(REMOVE_FORMAT) < len(REMOVE_FILES): - REMOVE_FORMAT = REMOVE_FORMAT*len(REMOVE_FILES) + REMOVE_FORMAT = REMOVE_FORMAT * len(REMOVE_FILES) # for each file to be removed - for REMOVE_FILE,REMOVEFORM in zip(REMOVE_FILES,REMOVE_FORMAT): - if REMOVEFORM in ('ascii','netCDF4','HDF5'): + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - Ylms = gravtk.harmonics().from_file(REMOVE_FILE, - format=REMOVEFORM) + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) attributes['ROOT']['lineage'].append(Ylms.name) - elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'): + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,removeform = REMOVEFORM.split('-') + _, removeform = REMOVEFORM.split('-') # index containing files in data format - Ylms = gravtk.harmonics().from_index(REMOVE_FILE, - format=removeform) - attributes['ROOT']['lineage'].extend([f.name for f in Ylms.filename]) + Ylms = gravtk.harmonics().from_index( + REMOVE_FILE, format=removeform + ) + attributes['ROOT']['lineage'].extend( + [f.name for f in Ylms.filename] + ) # reduce to GRACE/GRACE-FO months and truncate to degree and order - Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX,mmax=MMAX) + Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX, mmax=MMAX) # distribute removed Ylms uniformly over the ocean if REDISTRIBUTE_REMOVED: # calculate ratio between total removed mass and # a uniformly distributed cm of water over the ocean - ratio = Ylms.clm[0,0,:]/ocean_Ylms.clm[0,0] + ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0] # for each spherical harmonic - for m in range(0,MMAX+1):# MMAX+1 to include MMAX - for l in range(m,LMAX+1):# LMAX+1 to include LMAX + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX # remove the ratio*ocean Ylms from Ylms # note: x -= y is equivalent to x = x - y - Ylms.clm[l,m,:] -= ratio*ocean_Ylms.clm[l,m] - Ylms.slm[l,m,:] -= ratio*ocean_Ylms.slm[l,m] + Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m] + Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m] # filter removed coefficients if DESTRIPE: Ylms = Ylms.destripe() @@ -400,10 +433,10 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # dictionary of coordinate reference system variables crs_to_dict = crs1.to_dict() # Climate and Forecast (CF) Metadata Conventions - if (crs1.to_epsg() == 4326): - y_cf,x_cf = crs1.cs_to_cf() + if crs1.to_epsg() == 4326: + y_cf, x_cf = crs1.cs_to_cf() else: - x_cf,y_cf = crs1.cs_to_cf() + x_cf, y_cf = crs1.cs_to_cf() # output spatial units # Setting units factor for output @@ -425,19 +458,21 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # projection attributes attributes['crs'] = {} # add projection attributes - attributes['crs']['standard_name'] = \ - crs1.to_cf()['grid_mapping_name'].title() + attributes['crs']['standard_name'] = crs1.to_cf()[ + 'grid_mapping_name' + ].title() attributes['crs']['spatial_epsg'] = crs1.to_epsg() attributes['crs']['spatial_ref'] = crs1.to_wkt() attributes['crs']['proj4_params'] = crs1.to_proj4() - for att_name,att_val in crs1.to_cf().items(): + for att_name, att_val in crs1.to_cf().items(): attributes['crs'][att_name] = att_val - if ('lat_0' in crs_to_dict.keys() and (crs1.to_epsg() != 4326)): - attributes['crs']['latitude_of_projection_origin'] = \ - crs_to_dict['lat_0'] + if 'lat_0' in crs_to_dict.keys() and (crs1.to_epsg() != 4326): + attributes['crs']['latitude_of_projection_origin'] = crs_to_dict[ + 'lat_0' + ] # x and y - attributes['x'],attributes['y'] = ({},{}) - for att_name in ['long_name','standard_name','units']: + attributes['x'], attributes['y'] = ({}, {}) + for att_name in ['long_name', 'standard_name', 'units']: attributes['x'][att_name] = x_cf[att_name] attributes['y'][att_name] = y_cf[att_name] # time @@ -457,40 +492,48 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # output data variables output = {} # projection variable - output['crs'] = np.array((),dtype=np.byte) + output['crs'] = np.array((), dtype=np.byte) # spacing and bounds of output grid - dx,dy = np.broadcast_to(np.atleast_1d(SPACING),(2,)) - xmin,xmax,ymin,ymax = np.copy(BOUNDS) + dx, dy = np.broadcast_to(np.atleast_1d(SPACING), (2,)) + xmin, xmax, ymin, ymax = np.copy(BOUNDS) # create x and y from spacing and bounds - output['x'] = np.arange(xmin + dx/2.0, xmax + dx, dx) - output['y'] = np.arange(ymin + dx/2.0, ymax + dy, dy) - ny,nx = (len(output['y']),len(output['x'])) - gridx, gridy = np.meshgrid(output['x'],output['y']) + output['x'] = np.arange(xmin + dx / 2.0, xmax + dx, dx) + output['y'] = np.arange(ymin + dx / 2.0, ymax + dy, dy) + ny, nx = (len(output['y']), len(output['x'])) + gridx, gridy = np.meshgrid(output['x'], output['y']) gridlon, gridlat = transformer.transform(gridx, gridy) # semimajor axis of ellipsoid [m] a_axis = crs1.ellipsoid.semi_major_metre # ellipsoidal flattening - flat = 1.0/crs1.ellipsoid.inverse_flattening + flat = 1.0 / crs1.ellipsoid.inverse_flattening # calculate geocentric latitude and convert to degrees latitude_geocentric = geoidtk.spatial.geocentric_latitude( - gridlon, gridlat, a_axis=a_axis, flat=flat) + gridlon, gridlat, a_axis=a_axis, flat=flat + ) # calculate spatial mask with an extended radius THRESHOLD = 0.025 - mask = gravtk.clenshaw_summation(land_Ylms.clm, land_Ylms.slm, - gridlon.flatten(), latitude_geocentric.flatten(), RAD=2*RAD, - UNITS=1, LMAX=LMAX, LOVE=LOVE) - ii,jj = np.nonzero(mask.reshape(ny,nx) > THRESHOLD) + mask = gravtk.clenshaw_summation( + land_Ylms.clm, + land_Ylms.slm, + gridlon.flatten(), + latitude_geocentric.flatten(), + RAD=2 * RAD, + UNITS=1, + LMAX=LMAX, + LOVE=LOVE, + ) + ii, jj = np.nonzero(mask.reshape(ny, nx) > THRESHOLD) # output gridded raster data - output['z'] = np.ma.zeros((ny,nx,nt), fill_value=fill_value) - output['z'].mask = np.ones((ny,nx,nt), dtype=bool) + output['z'] = np.ma.zeros((ny, nx, nt), fill_value=fill_value) + output['z'].mask = np.ones((ny, nx, nt), dtype=bool) output['time'] = np.zeros((nt)) # converting harmonics to truncated, smoothed coefficients in units # combining harmonics to calculate output raster grids - for i,grace_month in enumerate(GRACE_Ylms.month): + for i, grace_month in enumerate(GRACE_Ylms.month): logging.debug(grace_month) # GRACE/GRACE-FO harmonics for time t Ylms = GRACE_Ylms.index(i) @@ -502,26 +545,37 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # truncate minimum degree to LMIN Ylms.truncate(LMAX, lmin=LMIN, mmax=MMAX) # convert spherical harmonics to output raster grid - output['z'].data[ii,jj,i] = gravtk.clenshaw_summation( - Ylms.clm, Ylms.slm, gridlon[ii,jj], latitude_geocentric[ii,jj], - RAD=RAD, UNITS=units, LMAX=LMAX, LOVE=LOVE) - output['z'].mask[ii,jj,i] = False + output['z'].data[ii, jj, i] = gravtk.clenshaw_summation( + Ylms.clm, + Ylms.slm, + gridlon[ii, jj], + latitude_geocentric[ii, jj], + RAD=RAD, + UNITS=units, + LMAX=LMAX, + LOVE=LOVE, + ) + output['z'].mask[ii, jj, i] = False # copy time variables for month output['time'][i] = np.copy(Ylms.time) # convert masked values to fill value output['z'].data[output['z'].mask] = output['z'].fill_value # output raster files to netCDF4 or HDF5 - FILE = (f'{FILE_PREFIX}{units}_L{LMAX:d}{order_str}{gw_str}{ds_str}_' - f'{START:03d}-{END:03d}.{suffix}') + FILE = ( + f'{FILE_PREFIX}{units}_L{LMAX:d}{order_str}{gw_str}{ds_str}_' + f'{START:03d}-{END:03d}.{suffix}' + ) output_file = OUTPUT_DIRECTORY.joinpath(FILE) # use spatial functions from geoid toolkit to write rasters - if (DATAFORM == 'netCDF4'): - geoidtk.spatial.to_netCDF4(output, attributes, output_file, - data_type='grid') - elif (DATAFORM == 'HDF5'): - geoidtk.spatial.to_HDF5(output, attributes, output_file, - data_type='grid') + if DATAFORM == 'netCDF4': + geoidtk.spatial.to_netCDF4( + output, attributes, output_file, data_type='grid' + ) + elif DATAFORM == 'HDF5': + geoidtk.spatial.to_HDF5( + output, attributes, output_file, data_type='grid' + ) # set the permissions mode of the output files output_file.chmod(mode=MODE) # add file to list @@ -530,10 +584,11 @@ def grace_raster_grids(base_dir, PROC, DREL, DSET, LMAX, RAD, # return the list of output files return output_files + # PURPOSE: print a file log for the GRACE analysis def output_log_file(input_arguments, output_files): # format: GRACE_processing_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_processing_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -550,10 +605,11 @@ def output_log_file(input_arguments, output_files): # close the log file fid.close() + # PURPOSE: print a error file log for the GRACE analysis def output_error_log_file(input_arguments): # format: GRACE_processing_failed_run_2002-04-01_PID-70335.log - args = (time.strftime('%Y-%m-%d',time.localtime()), os.getpid()) + args = (time.strftime('%Y-%m-%d', time.localtime()), os.getpid()) LOGFILE = 'GRACE_processing_failed_run_{0}_PID-{1:d}.log'.format(*args) # create a unique log and open the log file DIRECTORY = pathlib.Path(input_arguments.output_directory) @@ -569,100 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') - parser.add_argument('--output-directory','-O', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Output directory for raster files') - parser.add_argument('--file-prefix','-P', + 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', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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' @@ -678,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/ @@ -704,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] @@ -835,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 f20672b5..a1fc5e22 100755 --- a/scripts/grace_spatial_error.py +++ b/scripts/grace_spatial_error.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_spatial_error.py Written by Tyler Sutterley (05/2023) @@ -161,6 +161,7 @@ Updated 05/2013: algorithm updates following python processing scheme Written 08/2012 """ + from __future__ import print_function import sys @@ -176,6 +177,7 @@ import collections import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -185,9 +187,16 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: import GRACE files for a given months range # Estimates the GRACE/GRACE-FO errors applying the specified procedures -def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, +def grace_spatial_error( + base_dir, + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -217,8 +226,8 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # recursively create output directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -238,21 +247,22 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth model and love numbers attributes['earth_model'] = LOVE.model attributes['earth_love_numbers'] = LOVE.citation attributes['reference_frame'] = LOVE.reference # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' attributes['smoothing_radius'] = f'{RAD:0.0f} km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) gw_str = '' # flag for spherical harmonic order @@ -268,11 +278,28 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, # replacing low-degree harmonics with SLR values if specified # include degree 1 (geocenter) harmonics if specified # correcting for Pole-Tide and Atmospheric Jumps if specified - Ylms = gravtk.grace_input_months(base_dir, PROC, DREL, DSET, LMAX, - START, END, MISSING, SLR_C20, DEG1, MMAX=MMAX, SLR_21=SLR_21, - SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + Ylms = gravtk.grace_input_months( + base_dir, + PROC, + DREL, + DSET, + LMAX, + START, + END, + MISSING, + SLR_C20, + DEG1, + MMAX=MMAX, + SLR_21=SLR_21, + SLR_22=SLR_22, + SLR_C30=SLR_C30, + SLR_C40=SLR_C40, + SLR_C50=SLR_C50, + DEG1_FILE=DEG1_FILE, + MODEL_DEG1=MODEL_DEG1, + ATM=ATM, + POLE_TIDE=POLE_TIDE, + ) # convert to harmonics object and remove mean if specified GRACE_Ylms = gravtk.harmonics().from_dict(Ylms) # add attributes for input GRACE/GRACE-FO spherical harmonics @@ -283,8 +310,9 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, if MEAN_FILE: # read data form for input mean file (ascii, netCDF4, HDF5, gfc) MEAN_FILE = pathlib.Path(MEAN_FILE).expanduser().absolute() - mean_Ylms = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GRACE_Ylms.subtract(mean_Ylms) attributes['lineage'].append(MEAN_FILE.name) @@ -302,15 +330,27 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, ds_str = '' # full path to directory for specific GRACE/GRACE-FO product - GRACE_Ylms.directory = pathlib.Path(Ylms['directory']).expanduser().absolute() + GRACE_Ylms.directory = ( + pathlib.Path(Ylms['directory']).expanduser().absolute() + ) # default file prefix if not FILE_PREFIX: - FILE_PREFIX = '{0}_{1}_{2}{3}_'.format(PROC,DREL,DSET,Ylms['title']) + FILE_PREFIX = '{0}_{1}_{2}{3}_'.format(PROC, DREL, DSET, Ylms['title']) # calculating GRACE error (Wahr et al 2006) # output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) - fargs = (PROC,DREL,DSET,LMAX,order_str,ds_str,atm_str,GRACE_Ylms.month[0], - GRACE_Ylms.month[-1],suffix[DATAFORM]) + fargs = ( + PROC, + DREL, + DSET, + LMAX, + order_str, + ds_str, + atm_str, + GRACE_Ylms.month[0], + GRACE_Ylms.month[-1], + suffix[DATAFORM], + ) delta_format = '{0}_{1}_{2}_DELTA_CLM_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}' DELTA_FILE = GRACE_Ylms.directory.joinpath(delta_format.format(*fargs)) # check full path of the GRACE directory for delta file @@ -322,34 +362,35 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, # Delta coefficients of GRACE time series (Error components) delta_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - delta_Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - delta_Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + delta_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) + delta_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) # Smoothing Half-Width (CNES is a 10-day solution) # 365/10/2 = 18.25 (next highest is 19) # All other solutions are monthly solutions (HFWTH for annual = 6) - if ((PROC == 'CNES') and (DREL in ('RL01','RL02'))): + if (PROC == 'CNES') and (DREL in ('RL01', 'RL02')): HFWTH = 19 else: HFWTH = 6 # Equal to the noise of the smoothed time-series # for each spherical harmonic order - for m in range(0,MMAX+1):# MMAX+1 to include MMAX + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX # for each spherical harmonic degree - for l in range(m,LMAX+1):# LMAX+1 to include LMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX # Delta coefficients of GRACE time series - for cs,csharm in enumerate(['clm','slm']): + for cs, csharm in enumerate(['clm', 'slm']): # Constrained GRACE Error (Noise of smoothed time-series) # With Annual and Semi-Annual Terms val1 = getattr(GRACE_Ylms, csharm) - smth = gravtk.time_series.smooth(GRACE_Ylms.time, - val1[l,m,:], HFWTH=HFWTH) + smth = gravtk.time_series.smooth( + GRACE_Ylms.time, val1[l, m, :], HFWTH=HFWTH + ) # number of smoothed points nsmth = len(smth['data']) tsmth = np.mean(smth['time']) # GRACE/GRACE-FO delta Ylms # variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) - val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth) + val2[l, m] = np.sqrt(np.sum(smth['noise'] ** 2) / nsmth) # attributes for output files kwargs = {} @@ -365,8 +406,7 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, output_files.append(DELTA_FILE) else: # read GRACE/GRACE-FO delta harmonics from file - delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, - format=DATAFORM) + delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, format=DATAFORM) # truncate GRACE/GRACE-FO delta clm and slm to d/o LMAX/MMAX delta_Ylms = delta_Ylms.truncate(lmax=LMAX, mmax=MMAX) tsmth = np.squeeze(delta_Ylms.time) @@ -375,25 +415,25 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, # Output spatial data object delta = gravtk.spatial() # Output Degree Spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Output Degree Interval - if (INTERVAL == 1): + if INTERVAL == 1: # (-180:180,90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - delta.lon = -180 + dlon*np.arange(0,nlon) - delta.lat = 90.0 - dlat*np.arange(0,nlat) - elif (INTERVAL == 2): + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + delta.lon = -180 + dlon * np.arange(0, nlon) + delta.lat = 90.0 - dlat * np.arange(0, nlat) + elif INTERVAL == 2: # (Degree spacing)/2 - delta.lon = np.arange(-180+dlon/2.0,180+dlon/2.0,dlon) - delta.lat = np.arange(90.0-dlat/2.0,-90.0-dlat/2.0,-dlat) + delta.lon = np.arange(-180 + dlon / 2.0, 180 + dlon / 2.0, dlon) + delta.lat = np.arange(90.0 - dlat / 2.0, -90.0 - dlat / 2.0, -dlat) nlon = len(delta.lon) nlat = len(delta.lat) - elif (INTERVAL == 3): + elif INTERVAL == 3: # non-global grid set with BOUNDS parameter - minlon,maxlon,minlat,maxlat = BOUNDS.copy() - delta.lon = np.arange(minlon+dlon/2.0, maxlon+dlon/2.0, dlon) - delta.lat = np.arange(maxlat-dlat/2.0, minlat-dlat/2.0, -dlat) + minlon, maxlon, minlat, maxlat = BOUNDS.copy() + delta.lon = np.arange(minlon + dlon / 2.0, maxlon + dlon / 2.0, dlon) + delta.lat = np.arange(maxlat - dlat / 2.0, minlat - dlat / 2.0, -dlat) nlon = len(delta.lon) nlat = len(delta.lat) @@ -419,43 +459,58 @@ def grace_spatial_error(base_dir, PROC, DREL, DSET, LMAX, RAD, delta.attributes['ROOT'] = attributes # Computing plms for converting to spatial domain - phi = delta.lon[np.newaxis,:]*np.pi/180.0 - theta = (90.0 - delta.lat)*np.pi/180.0 + 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,106 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') - parser.add_argument('--output-directory','-O', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + 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', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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/ @@ -614,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] @@ -722,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 ea65903a..b9b09759 100755 --- a/scripts/grace_spatial_maps.py +++ b/scripts/grace_spatial_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" grace_spatial_maps.py Written by Tyler Sutterley (05/2023) @@ -182,6 +182,7 @@ Updated 06/2020: using spatial data class for output operations Updated 05/2020: for public release """ + from __future__ import print_function import sys @@ -197,6 +198,7 @@ import collections import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -206,9 +208,16 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: import GRACE/GRACE-FO files for a given months range # Converts the GRACE/GRACE-FO harmonics applying the specified procedures -def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, +def grace_spatial_maps( + base_dir, + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -244,8 +253,8 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # recursively create output directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -265,21 +274,22 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, suffix = dict(ascii='txt', netCDF4='nc', HDF5='H5') # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # add attributes for earth model and love numbers attributes['earth_model'] = LOVE.model attributes['earth_love_numbers'] = LOVE.citation attributes['reference_frame'] = LOVE.reference # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' attributes['smoothing_radius'] = f'{RAD:0.0f} km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) gw_str = '' # flag for spherical harmonic order @@ -293,11 +303,28 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # replacing low-degree harmonics with SLR values if specified # include degree 1 (geocenter) harmonics if specified # correcting for Pole-Tide and Atmospheric Jumps if specified - Ylms = gravtk.grace_input_months(base_dir, PROC, DREL, DSET, LMAX, - START, END, MISSING, SLR_C20, DEG1, MMAX=MMAX, SLR_21=SLR_21, - SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + Ylms = gravtk.grace_input_months( + base_dir, + PROC, + DREL, + DSET, + LMAX, + START, + END, + MISSING, + SLR_C20, + DEG1, + MMAX=MMAX, + SLR_21=SLR_21, + SLR_22=SLR_22, + SLR_C30=SLR_C30, + SLR_C40=SLR_C40, + SLR_C50=SLR_C50, + DEG1_FILE=DEG1_FILE, + MODEL_DEG1=MODEL_DEG1, + ATM=ATM, + POLE_TIDE=POLE_TIDE, + ) # convert to harmonics object and remove mean if specified GRACE_Ylms = gravtk.harmonics().from_dict(Ylms) # add attributes for input GRACE/GRACE-FO spherical harmonics @@ -307,8 +334,9 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, if MEAN_FILE: # read data form for input mean file (ascii, netCDF4, HDF5, gfc) MEAN_FILE = pathlib.Path(MEAN_FILE).expanduser().absolute() - mean_Ylms = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GRACE_Ylms.subtract(mean_Ylms) attributes['lineage'].append(MEAN_FILE.name) @@ -341,14 +369,13 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # default file prefix if not FILE_PREFIX: - fargs = (PROC,DREL,DSET,Ylms['title'],gia_str) + fargs = (PROC, DREL, DSET, Ylms['title'], gia_str) FILE_PREFIX = '{0}_{1}_{2}{3}{4}_'.format(*fargs) # Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_REMOVED: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, - MMAX=MMAX, LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) ocean_str = '_OCN' else: ocean_str = '' @@ -361,37 +388,39 @@ def grace_spatial_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, if REMOVE_FILES: # extend list if a single format was entered for all files if len(REMOVE_FORMAT) < len(REMOVE_FILES): - REMOVE_FORMAT = REMOVE_FORMAT*len(REMOVE_FILES) + REMOVE_FORMAT = REMOVE_FORMAT * len(REMOVE_FILES) # for each file to be removed - for REMOVE_FILE,REMOVEFORM in zip(REMOVE_FILES,REMOVE_FORMAT): - if REMOVEFORM in ('ascii','netCDF4','HDF5'): + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - Ylms = gravtk.harmonics().from_file(REMOVE_FILE, - format=REMOVEFORM) + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) attributes['lineage'].append(Ylms.name) - elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'): + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,removeform = REMOVEFORM.split('-') + _, removeform = REMOVEFORM.split('-') # index containing files in data format - Ylms = gravtk.harmonics().from_index(REMOVE_FILE, - format=removeform) + Ylms = gravtk.harmonics().from_index( + REMOVE_FILE, format=removeform + ) attributes['lineage'].extend([f.name for f in Ylms.filename]) # reduce to GRACE/GRACE-FO months and truncate to degree and order - Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX,mmax=MMAX) + Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX, mmax=MMAX) # distribute removed Ylms uniformly over the ocean if REDISTRIBUTE_REMOVED: # calculate ratio between total removed mass and # a uniformly distributed cm of water over the ocean - ratio = Ylms.clm[0,0,:]/ocean_Ylms.clm[0,0] + ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0] # for each spherical harmonic - for m in range(0,MMAX+1):# MMAX+1 to include MMAX - for l in range(m,LMAX+1):# LMAX+1 to include LMAX + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX # remove the ratio*ocean Ylms from Ylms # note: x -= y is equivalent to x = x - y - Ylms.clm[l,m,:] -= ratio*ocean_Ylms.clm[l,m] - Ylms.slm[l,m,:] -= ratio*ocean_Ylms.slm[l,m] + Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m] + Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m] # filter removed coefficients if DESTRIPE: Ylms = Ylms.destripe() @@ -403,30 +432,30 @@ 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) # Computing plms for converting to spatial domain - theta = (90.0-grid.lat)*np.pi/180.0 + theta = np.radians(90.0 - grid.lat) PLM, dPLM = gravtk.plm_holmes(LMAX, np.cos(theta)) # output spatial units @@ -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,98 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') - parser.add_argument('--output-directory','-O', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + 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', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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' @@ -632,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/ @@ -658,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] @@ -790,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 78ab5500..1063ebf9 100644 --- a/scripts/mascon_reconstruct.py +++ b/scripts/mascon_reconstruct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" mascon_reconstruct.py Written by Tyler Sutterley (05/2023) @@ -113,6 +113,7 @@ Updated 09/2014: Converted to function with main args Updated 05/2014 """ + from __future__ import print_function import sys @@ -125,6 +126,7 @@ import traceback import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -134,9 +136,13 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: Reconstruct spherical harmonic fields from the mascon # time series calculated in calc_mascon -def mascon_reconstruct(DSET, LMAX, RAD, +def mascon_reconstruct( + DSET, + LMAX, + RAD, START=None, END=None, MMAX=None, @@ -152,8 +158,8 @@ def mascon_reconstruct(DSET, LMAX, RAD, RECONSTRUCT_FILE=None, LANDMASK=None, OUTPUT_DIRECTORY=None, - MODE=0o775): - + MODE=0o775, +): # create output directory if currently non-existent OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -187,8 +193,9 @@ def mascon_reconstruct(DSET, LMAX, RAD, file_format = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}_{9:03d}-{10:03d}.{11}' # read load love numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # Earth Parameters factors = gravtk.units(lmax=LMAX).harmonic(*LOVE) # Average Density of the Earth [g/cm^3] @@ -198,8 +205,7 @@ def mascon_reconstruct(DSET, LMAX, RAD, # Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_MASCONS: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, - LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) ocean_str = '_OCN' else: # not distributing uniformly over ocean @@ -211,24 +217,25 @@ def mascon_reconstruct(DSET, LMAX, RAD, mascon_files = [l for l in f.read().splitlines() if parser.match(l)] # for each mascon file - for k,mascon_file in enumerate(mascon_files): + for k, mascon_file in enumerate(mascon_files): # read mascon spherical harmonics - Ylms = gravtk.harmonics().from_file(mascon_file, - format=DATAFORM, date=False) + Ylms = gravtk.harmonics().from_file( + mascon_file, format=DATAFORM, date=False + ) # Calculating the total mass of each mascon (1 cmwe uniform) - total_area = 4.0*np.pi*(rad_e**3)*rho_e*Ylms.clm[0,0]/3.0 + total_area = 4.0 * np.pi * (rad_e**3) * rho_e * Ylms.clm[0, 0] / 3.0 # distribute mascon mass uniformly over the ocean if REDISTRIBUTE_MASCONS: # calculate ratio between total mascon mass and # a uniformly distributed cm of water over the ocean - ratio = Ylms.clm[0,0]/ocean_Ylms.clm[0,0] + ratio = Ylms.clm[0, 0] / ocean_Ylms.clm[0, 0] # for each spherical harmonic - for m in range(0,MMAX+1):# MMAX+1 to include MMAX - for l in range(m,LMAX+1):# LMAX+1 to include LMAX + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX # remove ratio*ocean Ylms from mascon Ylms # note: x -= y is equivalent to x = x - y - Ylms.clm[l,m] -= ratio*ocean_Ylms.clm[l,m] - Ylms.slm[l,m] -= ratio*ocean_Ylms.slm[l,m] + Ylms.clm[l, m] -= ratio * ocean_Ylms.clm[l, m] + Ylms.slm[l, m] -= ratio * ocean_Ylms.slm[l, m] # truncate mascon spherical harmonics to d/o LMAX/MMAX Ylms = Ylms.truncate(lmax=LMAX, mmax=MMAX) # mascon_name is the mascon file without directory or suffix @@ -240,25 +247,48 @@ def mascon_reconstruct(DSET, LMAX, RAD, # mascon name, GRACE dataset, GIA model, LMAX, (MMAX,) # Gaussian smoothing, filter flag, remove reconstructed fields flag # output GRACE error file - args = (mascon_name,dset_str,gia_str.upper(),atm_str,ocean_str, - LMAX,order_str,gw_str,ds_str) + args = ( + mascon_name, + dset_str, + gia_str.upper(), + atm_str, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + ) file_input = '{0}{1}{2}{3}{4}_L{5:d}{6}{7}{8}.txt'.format(*args) mascon_data_input = np.loadtxt(OUTPUT_DIRECTORY.joinpath(file_input)) # convert mascon time-series from Gt to cmwe - mascon_sigma = 1e15*mascon_data_input[:,2]/total_area + mascon_sigma = 1e15 * mascon_data_input[:, 2] / total_area # mascon time-series Ylms mascon_Ylms = Ylms.scale(mascon_sigma) - mascon_Ylms.time = mascon_data_input[:,1].copy() - mascon_Ylms.month = mascon_data_input[:,0].astype(np.int64) + mascon_Ylms.time = mascon_data_input[:, 1].copy() + mascon_Ylms.month = mascon_data_input[:, 0].astype(np.int64) # output to file: no ascii option - args = (mascon_name,dset_str,gia_str.upper(),atm_str,ocean_str, - LMAX,order_str,gw_str,ds_str,START,END,suffix[DATAFORM]) + args = ( + mascon_name, + dset_str, + gia_str.upper(), + atm_str, + ocean_str, + LMAX, + order_str, + gw_str, + ds_str, + START, + END, + suffix[DATAFORM], + ) output_file = OUTPUT_DIRECTORY.joinpath(file_format.format(*args)) # attributes for output files attributes = {} - attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # output harmonics to file mascon_Ylms.to_file(output_file, format=DATAFORM, **attributes) # print file name to index @@ -270,59 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Output directory for mascon files') + parser.add_argument( + '--output-directory', + '-O', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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' @@ -338,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] @@ -413,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 @@ -421,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 f936dc1f..e3bbdafd 100755 --- a/scripts/piecewise_grace_maps.py +++ b/scripts/piecewise_grace_maps.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" piecewise_grace_maps.py -Written by Tyler Sutterley (06/2023) +Written by Tyler Sutterley (07/2026) Reads in GRACE/GRACE-FO spatial files and fits a piecewise regression model at each grid point for breakpoint analysis @@ -60,6 +60,7 @@ utilities.py: download and management utilities for files UPDATE HISTORY: + Updated 07/2026: use np.hypot to calculate the sum of two squares Updated 06/2023: append amplitude and phase titles when creating flags more tidal aliasing periods using values from Ray and Luthcke (2006) Updated 05/2023: split S2 tidal aliasing terms into GRACE and GRACE-FO eras @@ -96,6 +97,7 @@ Updated 06/2015: added output_files for log files Written 09/2013 """ + from __future__ import print_function, division import sys @@ -108,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) @@ -117,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, @@ -135,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(): @@ -167,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 @@ -216,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.nonzero(grid.month == BREAKPOINT) + (breakpoint_index,) = np.flatnonzero(grid.month == BREAKPOINT) # Setting output parameters coef_str = ['x0', 'px1', 'px1'] @@ -242,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') @@ -271,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') @@ -279,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 @@ -287,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 @@ -302,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]}' @@ -347,41 +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 - ii,jj = np.nonzero(ph.data < 0) - ph.data[ii,jj] += 360.0 + 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 - amp.error = np.sqrt(comp1**2 + comp2**2) + 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) - ph.error = (180.0/np.pi)*np.sqrt(comp1**2 + comp2**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,101 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + 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', 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] @@ -635,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 62be313b..842304ed 100755 --- a/scripts/regress_grace_maps.py +++ b/scripts/regress_grace_maps.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" regress_grace_maps.py -Written by Tyler Sutterley (06/2023) +Written by Tyler Sutterley (07/2026) Reads in GRACE/GRACE-FO spatial files and fits a regression model at each grid point @@ -60,6 +60,7 @@ utilities.py: download and management utilities for files UPDATE HISTORY: + Updated 07/2026: use np.hypot to calculate the sum of two squares Updated 06/2023: append amplitude and phase titles when creating flags more tidal aliasing periods using values from Ray and Luthcke (2006) Updated 05/2023: split S2 tidal aliasing terms into GRACE and GRACE-FO eras @@ -95,6 +96,7 @@ Updated 06/2015: added output_files for log files Written 09/2013 """ + from __future__ import print_function, division import sys @@ -107,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) @@ -116,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, @@ -134,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(): @@ -166,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 @@ -214,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 = [] @@ -242,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') @@ -271,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') @@ -279,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 @@ -287,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 @@ -295,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]}' @@ -339,41 +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 - ii,jj = np.nonzero(ph.data < 0) - ph.data[ii,jj] += 360.0 + 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 - amp.error = np.sqrt(comp1**2 + comp2**2) + 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) - ph.error = (180.0/np.pi)*np.sqrt(comp1**2 + comp2**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,104 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + 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', 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] @@ -630,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 e4a0e27d..ad2af07e 100644 --- a/scripts/run_sea_level_equation.py +++ b/scripts/run_sea_level_equation.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" run_sea_level_equation.py (06/2025) Solves the sea level equation with the option of including polar motion feedback Uses a Clenshaw summation to calculate the spherical harmonic summation @@ -117,6 +117,7 @@ Updated 04/2017: set the permissions mode of the output files with --mode Written 09/2016 """ + from __future__ import print_function import sys @@ -131,6 +132,7 @@ import collections import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -140,8 +142,11 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: Computes Sea Level Fingerprints including polar motion feedback -def run_sea_level_equation(INPUT_FILE, OUTPUT_FILE, +def run_sea_level_equation( + INPUT_FILE, + OUTPUT_FILE, LANDMASK=None, LMAX=0, LOVE_NUMBERS=0, @@ -155,8 +160,8 @@ def run_sea_level_equation(INPUT_FILE, OUTPUT_FILE, INPUT_TYPE=None, DATE=False, UNITS=None, - MODE=0o775): - + MODE=0o775, +): # set default paths INPUT_FILE = pathlib.Path(INPUT_FILE).expanduser().absolute() OUTPUT_FILE = pathlib.Path(OUTPUT_FILE).expanduser().absolute() @@ -172,36 +177,38 @@ def run_sea_level_equation(INPUT_FILE, OUTPUT_FILE, # Land-Sea Mask with Antarctica from Rignot (2017) and Greenland from GEUS # 0=Ocean, 1=Land, 2=Lake, 3=Small Island, 4=Ice Shelf # Open the land-sea NetCDF file for reading - landsea = gravtk.spatial().from_netCDF4(LANDMASK, date=False, - varname='LSMASK') + landsea = gravtk.spatial().from_netCDF4( + LANDMASK, date=False, varname='LSMASK' + ) # create land function - nth,nphi = landsea.shape + nth, nphi = landsea.shape land_function = np.zeros((nth, nphi), dtype=np.float64) # calculate colatitude in radians - th = (90.0 - landsea.lat)*np.pi/180.0 + 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 3f29117e..c6cc3ed7 100644 --- a/scripts/scale_grace_maps.py +++ b/scripts/scale_grace_maps.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" scale_grace_maps.py Written by Tyler Sutterley (05/2023) @@ -182,6 +182,7 @@ Updated 02/2021: changed remove index to files with specified formats Updated 02/2021: for public release """ + from __future__ import print_function, division import sys @@ -195,6 +196,7 @@ import traceback import gravity_toolkit as gravtk + # PURPOSE: keep track of threads def info(args): logging.info(pathlib.Path(sys.argv[0]).name) @@ -204,10 +206,17 @@ def info(args): logging.info(f'parent process: {os.getppid():d}') logging.info(f'process id: {os.getpid():d}') + # PURPOSE: import GRACE/GRACE-FO files for a given months range # Calculates monthly scaled spatial maps from GRACE/GRACE-FO # spherical harmonic coefficients -def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, +def scale_grace_maps( + base_dir, + PROC, + DREL, + DSET, + LMAX, + RAD, START=None, END=None, MISSING=None, @@ -242,8 +251,8 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, OUTPUT_DIRECTORY=None, FILE_PREFIX=None, VERBOSE=0, - MODE=0o775): - + MODE=0o775, +): # recursively create output Directory if not currently existing OUTPUT_DIRECTORY = pathlib.Path(OUTPUT_DIRECTORY).expanduser().absolute() if not OUTPUT_DIRECTORY.exists(): @@ -258,8 +267,9 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, file_format = '{0}{1}{2}_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}' # read arrays of kl, hl, and ll Love Numbers - LOVE = gravtk.load_love_numbers(LMAX, LOVE_NUMBERS=LOVE_NUMBERS, - REFERENCE=REFERENCE, FORMAT='class') + LOVE = gravtk.load_love_numbers( + LMAX, LOVE_NUMBERS=LOVE_NUMBERS, REFERENCE=REFERENCE, FORMAT='class' + ) # atmospheric ECMWF "jump" flag (if ATM) atm_str = '_wATM' if ATM else '' @@ -273,43 +283,46 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, fill_value = -9999.0 # Calculating the Gaussian smoothing for radius RAD - if (RAD != 0): - wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) + if RAD != 0: + wt = 2.0 * np.pi * gravtk.gauss_weights(RAD, LMAX) gw_str = f'_r{RAD:0.0f}km' else: # else = 1 - wt = np.ones((LMAX+1)) + wt = np.ones((LMAX + 1)) gw_str = '' # Read Ocean function and convert to Ylms for redistribution if REDISTRIBUTE_REMOVED: # read Land-Sea Mask and convert to spherical harmonics - ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, - LOVE=LOVE) + ocean_Ylms = gravtk.ocean_stokes(LANDMASK, LMAX, MMAX=MMAX, LOVE=LOVE) # Grid spacing - dlon,dlat = (DDEG[0],DDEG[0]) if (len(DDEG) == 1) else (DDEG[0],DDEG[1]) + dlon, dlat = (DDEG[0], DDEG[0]) if (len(DDEG) == 1) else (DDEG[0], DDEG[1]) # Grid dimensions - if (INTERVAL == 1):# (0:360, 90:-90) - nlon = np.int64((360.0/dlon)+1.0) - nlat = np.int64((180.0/dlat)+1.0) - elif (INTERVAL == 2):# degree spacing/2 - nlon = np.int64((360.0/dlon)) - nlat = np.int64((180.0/dlat)) + if INTERVAL == 1: # (0:360, 90:-90) + nlon = np.int64((360.0 / dlon) + 1.0) + nlat = np.int64((180.0 / dlat) + 1.0) + elif INTERVAL == 2: # degree spacing/2 + nlon = np.int64((360.0 / dlon)) + nlat = np.int64((180.0 / dlat)) # field mapping for input spatial variables - field_mapping = dict(lon='lon', lat='lat', data='kfactor', - error='error', magnitude='power') + field_mapping = dict( + lon='lon', lat='lat', data='kfactor', error='error', magnitude='power' + ) # read data for input scale files (ascii, netCDF4, HDF5) - if (DATAFORM == 'ascii'): - kfactor = gravtk.scaling_factors().from_ascii(SCALE_FILE, - spacing=[dlon,dlat], nlat=nlat, nlon=nlon) - elif (DATAFORM == 'netCDF4'): - kfactor = gravtk.scaling_factors().from_netCDF4(SCALE_FILE, - date=False, field_mapping=field_mapping) - elif (DATAFORM == 'HDF5'): - kfactor = gravtk.scaling_factors().from_HDF5(SCALE_FILE, - date=False, field_mapping=field_mapping) + if DATAFORM == 'ascii': + kfactor = gravtk.scaling_factors().from_ascii( + SCALE_FILE, spacing=[dlon, dlat], nlat=nlat, nlon=nlon + ) + elif DATAFORM == 'netCDF4': + kfactor = gravtk.scaling_factors().from_netCDF4( + SCALE_FILE, date=False, field_mapping=field_mapping + ) + elif DATAFORM == 'HDF5': + kfactor = gravtk.scaling_factors().from_HDF5( + SCALE_FILE, date=False, field_mapping=field_mapping + ) # input data shape nlat, nlon = kfactor.shape @@ -317,18 +330,36 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # replacing low-degree harmonics with SLR values if specified # include degree 1 (geocenter) harmonics if specified # correcting for Pole-Tide and Atmospheric Jumps if specified - Ylms = gravtk.grace_input_months(base_dir, PROC, DREL, DSET, LMAX, - START, END, MISSING, SLR_C20, DEG1, MMAX=MMAX, SLR_21=SLR_21, - SLR_22=SLR_22, SLR_C30=SLR_C30, SLR_C40=SLR_C40, SLR_C50=SLR_C50, - DEG1_FILE=DEG1_FILE, MODEL_DEG1=MODEL_DEG1, ATM=ATM, - POLE_TIDE=POLE_TIDE) + Ylms = gravtk.grace_input_months( + base_dir, + PROC, + DREL, + DSET, + LMAX, + START, + END, + MISSING, + SLR_C20, + DEG1, + MMAX=MMAX, + SLR_21=SLR_21, + SLR_22=SLR_22, + SLR_C30=SLR_C30, + SLR_C40=SLR_C40, + SLR_C50=SLR_C50, + DEG1_FILE=DEG1_FILE, + MODEL_DEG1=MODEL_DEG1, + ATM=ATM, + POLE_TIDE=POLE_TIDE, + ) # create harmonics object from GRACE/GRACE-FO data GRACE_Ylms = gravtk.harmonics().from_dict(Ylms) # use a mean file for the static field to remove if MEAN_FILE: # read data form for input mean file (ascii, netCDF4, HDF5, gfc) - mean_Ylms = gravtk.harmonics().from_file(MEAN_FILE, - format=MEANFORM, date=False) + mean_Ylms = gravtk.harmonics().from_file( + MEAN_FILE, format=MEANFORM, date=False + ) # remove the input mean GRACE_Ylms.subtract(mean_Ylms) else: @@ -355,7 +386,7 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # default file prefix if not FILE_PREFIX: - fargs = (PROC,DREL,DSET,Ylms['title'],gia_str) + fargs = (PROC, DREL, DSET, Ylms['title'], gia_str) FILE_PREFIX = '{0}_{1}_{2}{3}{4}_'.format(*fargs) # input spherical harmonic datafiles to be removed from the GRACE data @@ -366,35 +397,37 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, if REMOVE_FILES: # extend list if a single format was entered for all files if len(REMOVE_FORMAT) < len(REMOVE_FILES): - REMOVE_FORMAT = REMOVE_FORMAT*len(REMOVE_FILES) + REMOVE_FORMAT = REMOVE_FORMAT * len(REMOVE_FILES) # for each file to be removed - for REMOVE_FILE,REMOVEFORM in zip(REMOVE_FILES,REMOVE_FORMAT): - if REMOVEFORM in ('ascii','netCDF4','HDF5'): + for REMOVE_FILE, REMOVEFORM in zip(REMOVE_FILES, REMOVE_FORMAT): + if REMOVEFORM in ('ascii', 'netCDF4', 'HDF5'): # ascii (.txt) # netCDF4 (.nc) # HDF5 (.H5) - Ylms = gravtk.harmonics().from_file(REMOVE_FILE, - format=REMOVEFORM) - elif REMOVEFORM in ('index-ascii','index-netCDF4','index-HDF5'): + Ylms = gravtk.harmonics().from_file( + REMOVE_FILE, format=REMOVEFORM + ) + elif REMOVEFORM in ('index-ascii', 'index-netCDF4', 'index-HDF5'): # read from index file - _,removeform = REMOVEFORM.split('-') + _, removeform = REMOVEFORM.split('-') # index containing files in data format - Ylms = gravtk.harmonics().from_index(REMOVE_FILE, - format=removeform) + Ylms = gravtk.harmonics().from_index( + REMOVE_FILE, format=removeform + ) # reduce to GRACE/GRACE-FO months and truncate to degree and order - Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX,mmax=MMAX) + Ylms = Ylms.subset(GRACE_Ylms.month).truncate(lmax=LMAX, mmax=MMAX) # distribute removed Ylms uniformly over the ocean if REDISTRIBUTE_REMOVED: # calculate ratio between total removed mass and # a uniformly distributed cm of water over the ocean - ratio = Ylms.clm[0,0,:]/ocean_Ylms.clm[0,0] + ratio = Ylms.clm[0, 0, :] / ocean_Ylms.clm[0, 0] # for each spherical harmonic - for m in range(0,MMAX+1):# MMAX+1 to include MMAX - for l in range(m,LMAX+1):# LMAX+1 to include LMAX + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX # remove the ratio*ocean Ylms from Ylms # note: x -= y is equivalent to x = x - y - Ylms.clm[l,m,:] -= ratio*ocean_Ylms.clm[l,m] - Ylms.slm[l,m,:] -= ratio*ocean_Ylms.slm[l,m] + Ylms.clm[l, m, :] -= ratio * ocean_Ylms.clm[l, m] + Ylms.slm[l, m, :] -= ratio * ocean_Ylms.slm[l, m] # filter removed coefficients if DESTRIPE: Ylms = Ylms.destripe() @@ -405,10 +438,22 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # calculating GRACE/GRACE-FO error (Wahr et al. 2006) # output GRACE error file (for both LMAX==MMAX and LMAX != MMAX cases) - fargs = (PROC,DREL,DSET,LMAX,order_str,ds_str,atm_str,GRACE_Ylms.month[0], - GRACE_Ylms.month[-1], suffix[DATAFORM]) + fargs = ( + PROC, + DREL, + DSET, + LMAX, + order_str, + ds_str, + atm_str, + GRACE_Ylms.month[0], + GRACE_Ylms.month[-1], + suffix[DATAFORM], + ) delta_format = '{0}_{1}_{2}_DELTA_CLM_L{3:d}{4}{5}{6}_{7:03d}-{8:03d}.{9}' - GRACE_Ylms.directory = pathlib.Path(Ylms['directory']).expanduser().absolute() + GRACE_Ylms.directory = ( + pathlib.Path(Ylms['directory']).expanduser().absolute() + ) DELTA_FILE = GRACE_Ylms.directory.joinpath(delta_format.format(*fargs)) # check full path of the GRACE directory for delta file # if file was previously calculated: will read file @@ -419,38 +464,41 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, # Delta coefficients of GRACE time series (Error components) delta_Ylms = gravtk.harmonics(lmax=LMAX, mmax=MMAX) - delta_Ylms.clm = np.zeros((LMAX+1, MMAX+1)) - delta_Ylms.slm = np.zeros((LMAX+1, MMAX+1)) + delta_Ylms.clm = np.zeros((LMAX + 1, MMAX + 1)) + delta_Ylms.slm = np.zeros((LMAX + 1, MMAX + 1)) # Smoothing Half-Width (CNES is a 10-day solution) # All other solutions are monthly solutions (HFWTH for annual = 6) - if ((PROC == 'CNES') and (DREL in ('RL01','RL02'))): + if (PROC == 'CNES') and (DREL in ('RL01', 'RL02')): HFWTH = 19 else: HFWTH = 6 # Equal to the noise of the smoothed time-series # for each spherical harmonic order - for m in range(0,MMAX+1):# MMAX+1 to include MMAX + for m in range(0, MMAX + 1): # MMAX+1 to include MMAX # for each spherical harmonic degree - for l in range(m,LMAX+1):# LMAX+1 to include LMAX + for l in range(m, LMAX + 1): # LMAX+1 to include LMAX # Delta coefficients of GRACE time series - for cs,csharm in enumerate(['clm','slm']): + for cs, csharm in enumerate(['clm', 'slm']): # calculate GRACE Error (Noise of smoothed time-series) # With Annual and Semi-Annual Terms val1 = getattr(GRACE_Ylms, csharm) - smth = gravtk.time_series.smooth(GRACE_Ylms.time, - val1[l,m,:], HFWTH=HFWTH) + smth = gravtk.time_series.smooth( + GRACE_Ylms.time, val1[l, m, :], HFWTH=HFWTH + ) # number of smoothed points nsmth = len(smth['data']) tsmth = np.mean(smth['time']) # GRACE/GRACE-FO delta Ylms # variance of data-(smoothed+annual+semi) val2 = getattr(delta_Ylms, csharm) - val2[l,m] = np.sqrt(np.sum(smth['noise']**2)/nsmth) + val2[l, m] = np.sqrt(np.sum(smth['noise'] ** 2) / nsmth) # attributes for output files attributes = {} attributes['title'] = 'GRACE/GRACE-FO Spherical Harmonic Errors' - attributes['reference'] = f'Output from {pathlib.Path(sys.argv[0]).name}' + attributes['reference'] = ( + f'Output from {pathlib.Path(sys.argv[0]).name}' + ) # save GRACE/GRACE-FO delta harmonics to file delta_Ylms.time = np.copy(tsmth) delta_Ylms.month = np.int64(nsmth) @@ -461,8 +509,7 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, output_files.append(DELTA_FILE) else: # read GRACE/GRACE-FO delta harmonics from file - delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, - format=DATAFORM) + delta_Ylms = gravtk.harmonics().from_file(DELTA_FILE, format=DATAFORM) # truncate GRACE/GRACE-FO delta clm and slm to d/o LMAX/MMAX delta_Ylms = delta_Ylms.truncate(lmax=LMAX, mmax=MMAX) tsmth = np.squeeze(delta_Ylms.time) @@ -473,17 +520,17 @@ def scale_grace_maps(base_dir, PROC, DREL, DSET, LMAX, RAD, grid.lon = np.copy(kfactor.lon) grid.lat = np.copy(kfactor.lat) grid.time = np.zeros((nfiles)) - grid.month = np.zeros((nfiles),dtype=np.int64) + grid.month = np.zeros((nfiles), dtype=np.int64) grid.data = np.zeros((nlat, nlon, nfiles)) - grid.mask = np.zeros((nlat, nlon, nfiles),dtype=bool) + grid.mask = np.zeros((nlat, nlon, nfiles), dtype=bool) # Computing plms for converting to spatial domain - phi = grid.lon[np.newaxis,:]*np.pi/180.0 - theta = (90.0-grid.lat)*np.pi/180.0 + 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,90 +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', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') - parser.add_argument('--output-directory','-O', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Output directory for spatial files') - parser.add_argument('--file-prefix','-P', + 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', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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' @@ -762,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/ @@ -788,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] @@ -923,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 40ede658..97837a09 100644 --- a/setup.py +++ b/setup.py @@ -3,10 +3,19 @@ # list of all scripts to be included with package scripts = [] -for dir in ['access','dealiasing','geocenter','mapping','scripts','utilities']: - scripts.extend([os.path.join(dir,f) for f in os.listdir(dir) if f.endswith('.py')]) -scripts.append(os.path.join('gravity_toolkit','grace_date.py')) -scripts.append(os.path.join('gravity_toolkit','grace_months_index.py')) +for dir in [ + 'access', + 'dealiasing', + 'geocenter', + 'mapping', + 'scripts', + 'utilities', +]: + scripts.extend( + [os.path.join(dir, f) for f in os.listdir(dir) if f.endswith('.py')] + ) +scripts.append(os.path.join('gravity_toolkit', 'grace_date.py')) +scripts.append(os.path.join('gravity_toolkit', 'grace_months_index.py')) setup( name='gravity-toolkit', diff --git a/test/land.fcn.1_deg.gz b/test/land.fcn.1_deg.gz new file mode 100755 index 00000000..fc977c42 Binary files /dev/null and b/test/land.fcn.1_deg.gz differ diff --git a/test/out.slf.green_ice.1_deg.2008.60.gz b/test/out.slf.green_ice.1_deg.2008.60.gz new file mode 100644 index 00000000..0f13a933 Binary files /dev/null and b/test/out.slf.green_ice.1_deg.2008.60.gz differ diff --git a/test/test_download_and_read.py b/test/test_download_and_read.py index eb3a311e..73529047 100644 --- a/test/test_download_and_read.py +++ b/test/test_download_and_read.py @@ -3,6 +3,7 @@ test_download_and_read.py (11/2021) Tests the read program to verify that coefficients are being extracted """ +import pytest import pathlib import posixpath import gravity_toolkit as gravtk @@ -14,13 +15,20 @@ def test_podaac_cumulus_download_and_read(username,password): mission='grace', center='CSR', release='RL06', level='L2', product='GSM', start_date='2002-04-01', end_date='2002-04-30', provider='POCLOUD', endpoint='data') - # build opener for data client access - URS = 'urs.earthdata.nasa.gov' - opener = gravtk.utilities.attempt_login(URS, - username=username, password=password, - authorization_header=False, verbose=True) - # download and read as virtual file object - FILE = gravtk.utilities.from_http(urls[0], context=None, verbose=True) + # attempt to download the GRACE file + try: + # build opener for data client access + URS = 'urs.earthdata.nasa.gov' + opener = gravtk.utilities.attempt_login(URS, + username=username, password=password, + authorization_header=False, verbose=True) + # download and read as virtual file object + FILE = gravtk.utilities.from_http(urls[0], context=None, verbose=True) + except gravtk.utilities.urllib2.HTTPError as exc: + pytest.xfail(exc.reason) + except EOFError as exc: + pytest.xfail("NASA Earthdata Login Error") + # read as virtual file object Ylms = gravtk.read_GRACE_harmonics(FILE, 60) keys = ['time', 'start', 'end', 'clm', 'slm', 'eclm', 'eslm', 'header'] test = dict(start=2452369.5, end=2452394.5) @@ -32,8 +40,13 @@ def test_podaac_cumulus_download_and_read(username,password): def test_gfz_http_download_and_read(): HOST=['https://isdc-data.gfz.de','grace','Level-2','CSR','RL06', 'GSM-2_2002095-2002120_GRAC_UTCSR_BA01_0600.gz'] - # download and read as virtual file object - FILE = gravtk.utilities.from_http(HOST,verbose=True) + # attempt to download the GRACE file + try: + # download and read as virtual file object + FILE = gravtk.utilities.from_http(HOST,verbose=True) + except gravtk.utilities.urllib2.HTTPError as exc: + pytest.xfail(exc.reason) + # read as virtual file object Ylms = gravtk.read_GRACE_harmonics(FILE, 60) keys = ['time', 'start', 'end', 'clm', 'slm', 'eclm', 'eslm', 'header'] test = dict(start=2452369.5, end=2452394.5) @@ -42,11 +55,17 @@ def test_gfz_http_download_and_read(): assert (Ylms['clm'][2,0] == -0.484169355584e-03) # PURPOSE: Download a GRACE file from GFZ and check that read program runs +@pytest.mark.skip(reason="Deprecated GFZ FTP server") def test_gfz_ftp_download_and_read(): HOST=['isdcftp.gfz-potsdam.de','grace','Level-2','CSR','RL06', 'GSM-2_2002095-2002120_GRAC_UTCSR_BA01_0600.gz'] - # download and read as virtual file object - FILE = gravtk.utilities.from_ftp(HOST,verbose=True) + # attempt to download the GRACE file + try: + # download and read as virtual file object + FILE = gravtk.utilities.from_ftp(HOST, verbose=True) + except gravtk.utilities.urllib2.HTTPError as exc: + pytest.xfail(exc.reason) + # read as virtual file object Ylms = gravtk.read_GRACE_harmonics(FILE, 60) keys = ['time', 'start', 'end', 'clm', 'slm', 'eclm', 'eslm', 'header'] test = dict(start=2452369.5, end=2452394.5) @@ -58,7 +77,12 @@ def test_gfz_ftp_download_and_read(): def test_gfz_icgem_costg_download_and_read(): HOST=['https://icgem.gfz.de','getseries','02_COST-G_', 'Grace-FO_RL02','GSM-2_2018152-2018181_GRFO_COSTG_BF01_0200.gfc'] - FILE = gravtk.utilities.from_http(HOST,verbose=True) + # attempt to download the GRACE file + try: + # download and read as virtual file object + FILE = gravtk.utilities.from_http(HOST,verbose=True) + except gravtk.utilities.urllib2.HTTPError as exc: + pytest.xfail(exc.reason) # read as virtual file object Ylms = gravtk.read_GRACE_harmonics(FILE, 60) keys = ['time', 'start', 'end', 'clm', 'slm', 'eclm', 'eslm', 'header'] @@ -74,10 +98,15 @@ def test_esa_swarm_download_and_read(): swarm_file='SW_OPER_EGF_SHA_2__20131201T000000_20131231T235959_0101.ZIP' parameters = gravtk.utilities.urlencode({'file': posixpath.join('swarm','Level2longterm','EGF',swarm_file)}) - remote_file = [HOST,'?do=download&{0}'.format(parameters)] - # download and read as virtual file object - gravtk.utilities.from_http(remote_file, - local=swarm_file,verbose=True) + remote_file = [HOST,'?do=download&{0}'.format(parameters)] + # attempt to download the Swarm file + try: + # download as local file object + gravtk.utilities.from_http(remote_file, + local=swarm_file, verbose=True) + except gravtk.utilities.urllib2.HTTPError as exc: + pytest.xfail(exc.reason) + # read the local file swarm_file = pathlib.Path(swarm_file).absolute() Ylms = gravtk.read_gfc_harmonics(swarm_file) keys = ['time', 'start', 'end', 'clm', 'slm', 'eclm', 'eslm'] @@ -90,11 +119,16 @@ def test_esa_swarm_download_and_read(): # PURPOSE: Download a GRACE ITSG GRAZ file and check that read program runs def test_itsg_graz_download_and_read(): - HOST=['http://ftp.tugraz.at','outgoing','ITSG','GRACE', + HOST=['http://ftp.tugraz.at','pub','ITSG','GRACE', 'ITSG-Grace_operational','monthly','monthly_n60', 'ITSG-Grace_operational_n60_2018-06.gfc'] - # download and read as virtual file object - gravtk.utilities.from_http(HOST, local=HOST[-1], verbose=True) + # attempt to download the GRAZ file + try: + # download as local file object + gravtk.utilities.from_http(HOST, local=HOST[-1], verbose=True) + except gravtk.utilities.urllib2.HTTPError as exc: + pytest.xfail(exc.reason) + # read the local file itsg_file = pathlib.Path(HOST[-1]).absolute() Ylms = gravtk.read_gfc_harmonics(itsg_file) keys = ['time', 'start', 'end', 'clm', 'slm', 'eclm', 'eslm'] diff --git a/test/test_harmonics.py b/test/test_harmonics.py index 62c931b1..34de7e0d 100755 --- a/test/test_harmonics.py +++ b/test/test_harmonics.py @@ -39,7 +39,7 @@ def test_harmonics(): RAD = 250.0 # calculate colatitudes of input distribution - theta = (90.0 - input_distribution.lat)*np.pi/180.0 + theta = np.radians(90.0 - input_distribution.lat) # use fortran thresholds for colatitude bounds theta[theta > np.arccos(-0.9999999)] = np.arccos(-0.9999999) theta[theta < np.arccos(0.9999999)] = np.arccos(0.9999999) @@ -68,13 +68,14 @@ def test_harmonics(): dfactor = gravtk.units(lmax=LMAX).harmonic(*LOVE) wt = 2.0*np.pi*gravtk.gauss_weights(RAD,LMAX) smooth_Ylms.convolve(dfactor.cmwe*wt) + transform_Ylms = smooth_Ylms.copy() # convert harmonics back to spatial domain at same grid spacing test_distribution = gravtk.harmonic_summation(smooth_Ylms.clm, smooth_Ylms.slm, input_distribution.lon, input_distribution.lat, LMAX=LMAX, PLM=PLM).T # convert harmonics using fast-fourier transform method - test_transform = gravtk.harmonic_transform(smooth_Ylms.clm, - smooth_Ylms.slm, input_distribution.lon, input_distribution.lat, + test_transform = gravtk.harmonic_transform(transform_Ylms.clm, + transform_Ylms.slm, input_distribution.lon, input_distribution.lat, LMAX=LMAX, PLM=PLM).T # convert harmonics back to spatial domain using wrapper function test_combine = gravtk.stokes_summation(test_Ylms.clm, diff --git a/test/test_masks.py b/test/test_masks.py index 70c1f923..d04cb873 100644 --- a/test/test_masks.py +++ b/test/test_masks.py @@ -25,10 +25,10 @@ def test_lsmask(LANDMASK): nlat, nlon = landsea.shape # colatitude in radians gridlon, gridlat = np.meshgrid(landsea.lon, landsea.lat) - th = (90.0 - gridlat)*np.pi/180.0 + th = np.radians(90.0 - gridlat) # grid spacing in radians - dphi = np.pi*np.abs(dlon)/180.0 - dth = np.pi*np.abs(dlat)/180.0 + dphi = np.radians(np.abs(dlon)) + dth = np.radians(np.abs(dlat)) # create land function land_function = np.zeros((nlat, nlon), dtype=np.float64) # combine land and island levels for land function diff --git a/test/test_sea_level.py b/test/test_sea_level.py new file mode 100644 index 00000000..ebe093ad --- /dev/null +++ b/test/test_sea_level.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +u""" +test_sea_level.py (07/2026) +""" +import inspect +import pathlib +import numpy as np +import gravity_toolkit as gravtk + +# path to test files +filename = inspect.getframeinfo(inspect.currentframe()).filename +filepath = pathlib.Path(filename).absolute().parent + +# PURPOSE: test sea level equation programs +def test_sea_level(): + # path to load Love numbers file + love_numbers_file = gravtk.utilities.get_data_path( + ['data','love_numbers']) + # read load Love numbers + LOVE = gravtk.read_love_numbers(love_numbers_file, FORMAT='class') + + # read land function file + LANDMASK = filepath.joinpath('land.fcn.1_deg.gz') + landsea = gravtk.spatial().from_ascii(LANDMASK, + date=False, spacing=[1.0, 1.0], nlat=180, nlon=360, + extent=[0.5,359.5,-89.5,89.5], compression='gzip') + + # spherical harmonic parameters + # maximum spherical harmonic degree + LMAX = 60 + # read harmonics from file + harmonics_file = filepath.joinpath('out.geoid.green_ice.0.5.2008.60.gz') + Ylms = gravtk.harmonics(lmax=LMAX, mmax=LMAX).from_ascii( + harmonics_file, date=False, compression='gzip') + # calculate the legendre functions using Martin Mohlenkamp's relation + th = np.radians(90.0 - landsea.lat) + PLM, dPLM = gravtk.plm_mohlenkamp(LMAX, np.cos(th)) + + # run pseudo-spectral sea level equation solver + sea_level = gravtk.sea_level_equation(Ylms.clm, Ylms.slm, + landsea.lon, landsea.lat, landsea.data.T, LMAX=LMAX, + LOVE=LOVE, BODY_TIDE_LOVE=0, + FLUID_LOVE=0, DENSITY=1.0, POLAR=True, + PLM=PLM, ITERATIONS=2, FILL_VALUE=np.nan).T + + # check that sea level data is equal to file precision + valid_file = filepath.joinpath('out.slf.green_ice.1_deg.2008.60.gz') + validation = gravtk.spatial().from_ascii(valid_file, + date=False, spacing=[1.0, 1.0], nlat=180, nlon=360, + extent=[0.5,359.5,-89.5,89.5], compression='gzip') + # check differences + difference = validation.data - sea_level + valid_difference = difference[np.isfinite(difference)] + assert np.all(np.abs(valid_difference) < 1e-8) + +def test_harmonics(): + # read land function file + LANDMASK = filepath.joinpath('land.fcn.1_deg.gz') + landsea = gravtk.spatial().from_ascii(LANDMASK, + date=False, spacing=[1.0, 1.0], nlat=180, nlon=360, + extent=[0.5,359.5,-89.5,89.5], compression='gzip') + # calculate ocean function from land function + land_function = landsea.data.T + ocean_function = 1.0 - land_function + # maximum spherical harmonic degree + LMAX = 60 + # calculate spherical harmonics using integration and fourier methods + YlmI = gravtk.gen_harmonics(ocean_function, landsea.lon, landsea.lat, + LMAX=LMAX, METHOD="integration") + YlmF = gravtk.gen_harmonics(ocean_function, landsea.lon, landsea.lat, + LMAX=LMAX, METHOD="fourier") + # check that amplitudes of harmonic data are nearly equal + difference = YlmI.amplitude - YlmF.amplitude + harmonic_eps = np.finfo(np.float16).eps + assert np.all(np.abs(difference) < harmonic_eps) diff --git a/utilities/make_grace_index.py b/utilities/make_grace_index.py index 00aae0d2..7ee24987 100644 --- a/utilities/make_grace_index.py +++ b/utilities/make_grace_index.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" make_grace_index.py Written by Tyler Sutterley (10/2023) Creates index files of GRACE/GRACE-FO Level-2 data @@ -32,6 +32,7 @@ Updated 08/2022: make the data product optional Written 08/2022 """ + from __future__ import print_function import sys @@ -40,14 +41,15 @@ import pathlib import gravity_toolkit as gravtk -# PURPOSE: Creates index files of GRACE/GRACE-FO data -def make_grace_index(DIRECTORY, PROC=[], DREL=[], DSET=[], - VERSION=[], MODE=None): +# PURPOSE: Creates index files of GRACE/GRACE-FO data +def make_grace_index( + DIRECTORY, PROC=[], DREL=[], DSET=[], VERSION=[], MODE=None +): # input directory setup DIRECTORY = pathlib.Path(DIRECTORY).expanduser().absolute() # mission shortnames - shortname = {'grace':'GRAC', 'grace-fo':'GRFO'} + shortname = {'grace': 'GRAC', 'grace-fo': 'GRFO'} # GRACE/GRACE-FO level-2 spherical harmonic products logging.info('GRACE/GRACE-FO L2 Global Spherical Harmonics:') # for each processing center (CSR, GFZ, JPL) @@ -57,22 +59,24 @@ def make_grace_index(DIRECTORY, PROC=[], DREL=[], DSET=[], # for each level-2 product for ds in DSET: # local directory for exact data product - local_dir = DIRECTORY.joinpath( pr, rl, ds) + local_dir = DIRECTORY.joinpath(pr, rl, ds) # check if local directory exists if not local_dir.exists(): continue # list of GRACE/GRACE-FO files for index grace_files = [] # for each satellite mission (grace, grace-fo) - for i,mi in enumerate(['grace','grace-fo']): + for i, mi in enumerate(['grace', 'grace-fo']): # print string of exact data product logging.info(f'{mi} {pr}/{rl}/{ds}') # regular expression operator for data product - rx = gravtk.utilities.compile_regex_pattern(pr, rl, ds, - mission=shortname[mi], version=VERSION[i]) + rx = gravtk.utilities.compile_regex_pattern( + pr, rl, ds, mission=shortname[mi], version=VERSION[i] + ) # find local GRACE/GRACE-FO files to create index - granules = [f.name for f in local_dir.iterdir() - if rx.match(f.name)] + granules = [ + f.name for f in local_dir.iterdir() if rx.match(f.name) + ] # extend list of GRACE/GRACE-FO files grace_files.extend(granules) @@ -86,6 +90,7 @@ def make_grace_index(DIRECTORY, PROC=[], DREL=[], DSET=[], # change permissions of index file index_file.chmod(mode=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -95,54 +100,95 @@ def arguments(): ) # command line parameters # # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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 a2fc9336..ac136a90 100644 --- a/utilities/quick_mascon_plot.py +++ b/utilities/quick_mascon_plot.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" quick_mascon_plot.py Written by Tyler Sutterley (06/2024) Plots a mascon time series file for a particular format @@ -54,6 +54,7 @@ Updated 11/2019: using getopt to set parameters. use figure tight layout Written 10/2019 """ + import sys import pathlib import argparse @@ -63,19 +64,22 @@ # attempt imports plt = gravtk.utilities.import_dependency('matplotlib.pyplot') + # PURPOSE: read mascon time series file and create plot -def run_plot(i, input_file, - individual=False, - header=0, - marker=None, - zorder=None, - title=None, - time=None, - units=None, - legend=None, - error=False, - monthly=True - ): +def run_plot( + i, + input_file, + individual=False, + header=0, + marker=None, + zorder=None, + title=None, + time=None, + units=None, + legend=None, + error=False, + monthly=True, +): """ Plots a mascon time series file for a particular format @@ -99,68 +103,74 @@ def run_plot(i, input_file, """ # if creating individual plots if individual: - plt.figure(i+1) + plt.figure(i + 1) # read data input_file = pathlib.Path(input_file).expanduser().absolute() print(input_file.name) dinput = np.loadtxt(input_file, skiprows=header) # calculate regression - TERMS = gravtk.time_series.aliasing_terms(dinput[:,1]) - x1 = gravtk.time_series.regress(dinput[:,1],dinput[:,2],ORDER=1, - CYCLES=[0.5,1.0], TERMS=TERMS) - x2 = gravtk.time_series.regress(dinput[:,1],dinput[:,2],ORDER=2, - CYCLES=[0.5,1.0], TERMS=TERMS) + TERMS = gravtk.time_series.aliasing_terms(dinput[:, 1]) + x1 = gravtk.time_series.regress( + dinput[:, 1], dinput[:, 2], ORDER=1, CYCLES=[0.5, 1.0], TERMS=TERMS + ) + x2 = gravtk.time_series.regress( + dinput[:, 1], dinput[:, 2], ORDER=2, CYCLES=[0.5, 1.0], TERMS=TERMS + ) # print regression coefficients - args = ('x1',x1['beta'][1],x1['error'][1]) + args = ('x1', x1['beta'][1], x1['error'][1]) print('{0}: {1:0.4f} +/- {2:0.4f}'.format(*args)) - args = ('x2',2.0*x2['beta'][2],2.0*x2['error'][2]) + args = ('x2', 2.0 * x2['beta'][2], 2.0 * x2['error'][2]) print('{0}: {1:0.4f} +/- {2:0.4f}'.format(*args)) - args = ('AIC',x2['AIC']-x1['AIC']) + args = ('AIC', x2['AIC'] - x1['AIC']) print('{0}: {1:0.4f}'.format(*args)) # plot all data or monthly data with gap if monthly: # calculate months and remove GAP from missing - START_MON,END_MON = (dinput[0,0],dinput[-1,0]) - all_months = np.arange(START_MON,END_MON+1,dtype=np.int64) - GAP = [187,188,189,190,191,192,193,194,195,196,197] - MISSING = sorted(set(all_months) - set(dinput[:,0]) - set(GAP)) + START_MON, END_MON = (dinput[0, 0], dinput[-1, 0]) + all_months = np.arange(START_MON, END_MON + 1, dtype=np.int64) + GAP = [187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197] + MISSING = sorted(set(all_months) - set(dinput[:, 0]) - set(GAP)) months = sorted(set(all_months) - set(MISSING)) # create a time series with nans for missing months - tdec = np.full_like(months,np.nan,dtype=np.float64) - data = np.full_like(months,np.nan,dtype=np.float64) + tdec = np.full_like(months, np.nan, dtype=np.float64) + data = np.full_like(months, np.nan, dtype=np.float64) if error: - err = np.full_like(months,np.nan,dtype=np.float64) - for t,m in enumerate(months): - valid = np.count_nonzero(dinput[:,0] == m) + err = np.full_like(months, np.nan, dtype=np.float64) + for t, m in enumerate(months): + valid = np.count_nonzero(dinput[:, 0] == m) if valid: - mm, = np.nonzero(dinput[:,0] == m) - tdec[t] = dinput[mm,1] - data[t] = dinput[mm,2] - dinput[0,2] + (mm,) = np.nonzero(dinput[:, 0] == m) + tdec[t] = dinput[mm, 1] + data[t] = dinput[mm, 2] - dinput[0, 2] if error: - err[t] = dinput[mm,3] + err[t] = dinput[mm, 3] else: - tdec = np.copy(dinput[:,1]) - data = np.copy(dinput[:,2]) - dinput[0,2] + tdec = np.copy(dinput[:, 1]) + data = np.copy(dinput[:, 2]) - dinput[0, 2] if error: - err = np.copy(dinput[:,3]) + err = np.copy(dinput[:, 3]) # plot all dates - l, = plt.plot(tdec, data, - label=legend, - marker=marker, - markersize=5, - zorder=zorder + (l,) = plt.plot( + tdec, data, label=legend, marker=marker, markersize=5, zorder=zorder ) # add estimated errors if error: - plt.fill_between(tdec, data-err, y2=data+err, - color=l.get_color(), alpha=0.25, zorder=zorder) + plt.fill_between( + tdec, + data - err, + y2=data + err, + color=l.get_color(), + alpha=0.25, + zorder=zorder, + ) # vertical lines for end of the GRACE mission and start of GRACE-FO if monthly & ((i == 0) | individual): - jj, = np.flatnonzero(dinput[:,0] == 186) - kk, = np.flatnonzero(dinput[:,0] == 198) - vs = plt.gca().axvspan(dinput[jj,1],dinput[kk,1], - color='0.5',ls='dashed',alpha=0.15) - vs._dashes = (4,3) + (jj,) = np.flatnonzero(dinput[:, 0] == 186) + (kk,) = np.flatnonzero(dinput[:, 0] == 198) + vs = plt.gca().axvspan( + dinput[jj, 1], dinput[kk, 1], color='0.5', ls='dashed', alpha=0.15 + ) + vs._dashes = (4, 3) # if on the first axes or creating individual plots if (i == 0) | individual: # add labels @@ -170,6 +180,7 @@ def run_plot(i, input_file, # use a tight layout to minimize whitespace plt.tight_layout() + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -177,72 +188,130 @@ def arguments(): """ ) # command line parameters - parser.add_argument('file', - type=pathlib.Path, nargs='+', - help='Mascon data files') - parser.add_argument('--individual','-I', - default=False, action='store_true', - help='Create individual plots or combine into single') + parser.add_argument( + 'file', type=pathlib.Path, nargs='+', help='Mascon data files' + ) + parser.add_argument( + '--individual', + '-I', + default=False, + action='store_true', + help='Create individual plots or combine into single', + ) # output filename, format and dpi - parser.add_argument('--output-file','-O', - type=pathlib.Path, - help='Output figure file') - parser.add_argument('--figure-format','-f', - type=str, default='pdf', choices=('pdf','png','jpg','svg'), - help='Output figure format') - parser.add_argument('--figure-dpi','-d', - type=int, default=180, - help='Output figure resolution in dots per inch (dpi)') - parser.add_argument('--header','-H', - type=int, default=0, - help='Number of rows of header text to skip') - parser.add_argument('--marker','-m', - type=str, help='Plot marker') - parser.add_argument('--zorder','-z', - type=int, nargs='+', - help='Drawing order for each time series') - parser.add_argument('--title','-t', - type=lambda x: ' '.join(str.split(x,"_")), - help='Plot title') - parser.add_argument('--time','-T', - type=str, default='Time [Yr]', - help='Time label for x-axis') - parser.add_argument('--units','-U', - type=str, default='Mass [Gt]', - help='Units label for y-axis') - parser.add_argument('--legend','-L', - type=str, nargs='+', - help='Legend labels for each time series') - parser.add_argument('--error','-E', - default=False, action='store_true', - help='Plot mascon errors') - parser.add_argument('--all','-A', - default=True, action='store_false', - help='Plot all data without data gap') + parser.add_argument( + '--output-file', '-O', type=pathlib.Path, help='Output figure file' + ) + parser.add_argument( + '--figure-format', + '-f', + type=str, + default='pdf', + choices=('pdf', 'png', 'jpg', 'svg'), + help='Output figure format', + ) + parser.add_argument( + '--figure-dpi', + '-d', + type=int, + default=180, + help='Output figure resolution in dots per inch (dpi)', + ) + parser.add_argument( + '--header', + '-H', + type=int, + default=0, + help='Number of rows of header text to skip', + ) + parser.add_argument('--marker', '-m', type=str, help='Plot marker') + parser.add_argument( + '--zorder', + '-z', + type=int, + nargs='+', + help='Drawing order for each time series', + ) + parser.add_argument( + '--title', + '-t', + type=lambda x: ' '.join(str.split(x, '_')), + help='Plot title', + ) + parser.add_argument( + '--time', + '-T', + type=str, + default='Time [Yr]', + help='Time label for x-axis', + ) + parser.add_argument( + '--units', + '-U', + type=str, + default='Mass [Gt]', + help='Units label for y-axis', + ) + parser.add_argument( + '--legend', + '-L', + type=str, + nargs='+', + help='Legend labels for each time series', + ) + parser.add_argument( + '--error', + '-E', + default=False, + action='store_true', + help='Plot mascon errors', + ) + parser.add_argument( + '--all', + '-A', + default=True, + action='store_false', + help='Plot all data without data gap', + ) # permissions mode of the local directories and files (number in octal) - parser.add_argument('--mode','-M', - type=lambda x: int(x,base=8), default=0o775, - help='Permissions mode of output files') + parser.add_argument( + '--mode', + '-M', + type=lambda x: int(x, base=8), + default=0o775, + help='Permissions mode of output files', + ) # return the parser return parser + # This is the main part of the program that calls the individual functions def main(): # Read the system arguments listed after the program parser = arguments() - args,_ = parser.parse_known_args() + args, _ = parser.parse_known_args() # run plot program for each input file - for i,f in enumerate(args.file): + for i, f in enumerate(args.file): legend = args.legend[i] if args.legend else None zorder = args.zorder[i] if args.zorder else None - run_plot(i, f, individual=args.individual, header=args.header, - marker=args.marker, title=args.title, time=args.time, - units=args.units, legend=legend, error=args.error, - monthly=args.all, zorder=zorder) + run_plot( + i, + f, + individual=args.individual, + header=args.header, + marker=args.marker, + title=args.title, + time=args.time, + units=args.units, + legend=legend, + error=args.error, + monthly=args.all, + zorder=zorder, + ) # add legend if applicable if args.legend: - lgd = plt.legend(loc=3,frameon=False) + lgd = plt.legend(loc=3, frameon=False) lgd.get_frame().set_alpha(1.0) for line in lgd.get_lines(): line.set_linewidth(6) @@ -253,19 +322,21 @@ def main(): fig = plt.figure(num) output = f'{args.output_file.stem}_{num}{args.output_file.suffix}' # save the figure file - fig.savefig(output, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, + fig.savefig( + output, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, dpi=args.figure_dpi, - format=args.figure_format + format=args.figure_format, ) # change the permissions mode output.chmod(mode=args.mode) elif args.output_file: # save the figure file - plt.savefig(args.output_file, - metadata={'Title':pathlib.Path(sys.argv[0]).name}, + plt.savefig( + args.output_file, + metadata={'Title': pathlib.Path(sys.argv[0]).name}, dpi=args.figure_dpi, - format=args.figure_format + format=args.figure_format, ) # change the permissions mode args.output_file.chmod(mode=args.mode) @@ -277,6 +348,7 @@ def main(): plt.clf() plt.close() + # run main program if __name__ == '__main__': main() diff --git a/utilities/quick_mascon_regress.py b/utilities/quick_mascon_regress.py index 2fcad343..59dcbc71 100755 --- a/utilities/quick_mascon_regress.py +++ b/utilities/quick_mascon_regress.py @@ -1,7 +1,7 @@ #!/usr/bin/env python -u""" +""" quick_mascon_regress.py -Written by Tyler Sutterley (06/2023) +Written by Tyler Sutterley (07/2026) Creates a regression summary file for a mascon time series file COMMAND LINE OPTIONS: @@ -22,6 +22,7 @@ time_series.amplitude.py: calculates the amplitude and phase of a harmonic UPDATE HISTORY: + Updated 07/2026: use np.hypot to calculate the sum of two squares Updated 06/2023: can choose different tidal aliasing periods Updated 05/2023: split S2 tidal aliasing terms into GRACE and GRACE-FO eras allow fit to be piecewise using a known breakpoint GRACE/GRACE-FO month @@ -32,6 +33,7 @@ Updated 05/2022: use argparse descriptions within documentation Written 10/2021 """ + from __future__ import print_function import sys @@ -40,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 @@ -73,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'] @@ -82,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: @@ -114,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 @@ -145,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 @@ -159,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] - amp[err][i] = np.sqrt(comp1**2 + comp2**2) + 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) - ph[err][i] = (180.0/np.pi)*np.sqrt(comp1**2 + comp2**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( @@ -225,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 0a0fb12f..6d834295 100755 --- a/utilities/run_grace_date.py +++ b/utilities/run_grace_date.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -u""" +""" run_grace_date.py Written by Tyler Sutterley (05/2023) @@ -68,6 +68,7 @@ Updated 02/2014: minor update to if statements Written 07/2012 """ + from __future__ import print_function import sys @@ -76,6 +77,7 @@ import argparse import gravity_toolkit as gravtk + def run_grace_date(base_dir, PROC, DREL, VERBOSE=0, MODE=0o775): # create logger loglevels = [logging.CRITICAL, logging.INFO, logging.DEBUG] @@ -85,20 +87,27 @@ def run_grace_date(base_dir, PROC, DREL, VERBOSE=0, MODE=0o775): DSET = {} VALID = {} # CSR RL04/5/6 at LMAX 60 - DSET['CSR'] = {'RL04':['GAC', 'GAD', 'GSM'], 'RL05':['GAC', 'GAD', 'GSM'], - 'RL06':['GAC', 'GAD', 'GSM']} - VALID['CSR'] = ['RL04','RL05','RL06'] + DSET['CSR'] = { + 'RL04': ['GAC', 'GAD', 'GSM'], + 'RL05': ['GAC', 'GAD', 'GSM'], + 'RL06': ['GAC', 'GAD', 'GSM'], + } + VALID['CSR'] = ['RL04', 'RL05', 'RL06'] # GFZ RL04/5 at LMAX 90 # GFZ RL06 at LMAX 60 - DSET['GFZ'] = {'RL04':['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], - 'RL05':['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], - 'RL06':['GAA', 'GAB', 'GAC', 'GAD', 'GSM']} - VALID['GFZ'] = ['RL04','RL05','RL06'] + DSET['GFZ'] = { + 'RL04': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + 'RL05': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + 'RL06': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + } + VALID['GFZ'] = ['RL04', 'RL05', 'RL06'] # JPL RL04/5/6 at LMAX 60 - DSET['JPL'] = {'RL04':['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], - 'RL05':['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], - 'RL06':['GAA', 'GAB', 'GAC', 'GAD', 'GSM']} - VALID['JPL'] = ['RL04','RL05','RL06'] + DSET['JPL'] = { + 'RL04': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + 'RL05': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + 'RL06': ['GAA', 'GAB', 'GAC', 'GAD', 'GSM'], + } + VALID['JPL'] = ['RL04', 'RL05', 'RL06'] # for each processing center for p in PROC: @@ -109,13 +118,15 @@ def run_grace_date(base_dir, PROC, DREL, VERBOSE=0, MODE=0o775): for d in DSET[p][r]: logging.info(f'GRACE Date Program: {p} {r} {d}') # create GRACE/GRACE-FO date index file - gravtk.grace_date(base_dir, PROC=p, DREL=r, DSET=d, - OUTPUT=True, MODE=MODE) + gravtk.grace_date( + base_dir, PROC=p, DREL=r, DSET=d, OUTPUT=True, MODE=MODE + ) # run GRACE/GRACE-FO months program for data releases logging.info('GRACE Months Program') gravtk.grace_months_index(base_dir, DREL=DREL, MODE=MODE) + # PURPOSE: create argument parser def arguments(): parser = argparse.ArgumentParser( @@ -125,40 +136,69 @@ def arguments(): ) # command line parameters # working data directory - parser.add_argument('--directory','-D', - type=pathlib.Path, default=pathlib.Path.cwd(), - help='Working data directory') + parser.add_argument( + '--directory', + '-D', + type=pathlib.Path, + default=gravtk.utilities.get_cache_path(ensure_exists=False), + 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__':