├── docs ├── modules.rst ├── _static │ ├── R06_change.png │ ├── science_cover.jpg │ ├── 15.03733_profile_2100_ssps.png │ ├── excess_meltwater_diagram.png │ ├── analyze_glacier_change_region11.png │ ├── analyze_glacier_change_watershed11.png │ └── elev_change_1d │ │ ├── README.txt │ │ ├── 01.00570_elev_change_1d.json │ │ └── 01.00570_elev_change_1d.csv ├── pygem_environment.yml ├── Makefile ├── dynamics_oggm.md ├── citing.md ├── pygem.tests.rst ├── scripts_overview.md ├── environment.yml ├── make.bat ├── initial_conditions.md ├── dynamics_parameterizations.md ├── test_pygem.md ├── pygem.rst ├── run_inversion_overview.md ├── mb_frontalablation.md ├── conf.py ├── publications.md ├── run_simulation_overview.md ├── run_calibration_overview.md ├── run_calibration_frontalablation_overview.md ├── mb_ablation.md ├── mb_refreezing.md ├── bias_corrections.md ├── index.rst ├── runoff.md ├── model_output.md ├── mb_parameterizations.md ├── mb_accumulation.md ├── limitations.md ├── install_pygem.md ├── contributing.md ├── dynamics_massredistributioncurves.md ├── faqs.md └── model_structure.md ├── .gitignore ├── pygem ├── bin │ ├── run │ │ └── __init__.py │ ├── op │ │ ├── duplicate_gdirs.py │ │ ├── compress_gdirs.py │ │ ├── initialize.py │ │ └── list_failed_simulations.py │ ├── preproc │ │ └── preproc_fetch_mbdata.py │ └── postproc │ │ ├── postproc_distribute_ice.py │ │ └── postproc_subannual_mass.py ├── setup │ └── __init__.py ├── tests │ ├── __init__.py │ ├── test_01_basics.py │ ├── test_03_notebooks.py │ ├── test_04_auxiliary.py │ ├── test_05_postproc.py │ └── test_02_config.py ├── __init__.py ├── scraps │ ├── dummy_task_module.py │ └── run.py ├── utils │ ├── _funcs_selectglaciers.py │ ├── stats.py │ └── _funcs.py └── shop │ ├── icethickness.py │ ├── debris.py │ ├── meltextent_and_snowline_1d.py │ └── mbdata.py ├── .readthedocs.yaml ├── setup.py ├── package.sh ├── docker └── Dockerfile ├── LICENSE.txt ├── .github └── workflows │ ├── docker_pygem.yml │ └── test_suite.yml ├── README.md └── pyproject.toml /docs/modules.rst: -------------------------------------------------------------------------------- 1 | pygem 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pygem 8 | -------------------------------------------------------------------------------- /docs/_static/R06_change.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyGEM-Community/PyGEM/HEAD/docs/_static/R06_change.png -------------------------------------------------------------------------------- /docs/_static/science_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyGEM-Community/PyGEM/HEAD/docs/_static/science_cover.jpg -------------------------------------------------------------------------------- /docs/pygem_environment.yml: -------------------------------------------------------------------------------- 1 | name: pygem 2 | dependencies: 3 | - python>=3.10,<3.13 4 | - pip 5 | - pip: 6 | - pygem -------------------------------------------------------------------------------- /docs/_static/15.03733_profile_2100_ssps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyGEM-Community/PyGEM/HEAD/docs/_static/15.03733_profile_2100_ssps.png -------------------------------------------------------------------------------- /docs/_static/excess_meltwater_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyGEM-Community/PyGEM/HEAD/docs/_static/excess_meltwater_diagram.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Subdirectories 2 | __pycache__/ 3 | sample_data/ 4 | .vscode/ 5 | 6 | # python bytecode 7 | *.pyc 8 | 9 | *.DS_Store 10 | -------------------------------------------------------------------------------- /docs/_static/analyze_glacier_change_region11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyGEM-Community/PyGEM/HEAD/docs/_static/analyze_glacier_change_region11.png -------------------------------------------------------------------------------- /docs/_static/analyze_glacier_change_watershed11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyGEM-Community/PyGEM/HEAD/docs/_static/analyze_glacier_change_watershed11.png -------------------------------------------------------------------------------- /pygem/bin/run/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2018 David Rounce 5 | 6 | Distributed under the MIT license 7 | """ 8 | -------------------------------------------------------------------------------- /pygem/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2018 David Rounce 5 | 6 | Distrubted under the MIT lisence 7 | """ 8 | 9 | from setuptools import setup 10 | 11 | if __name__ == '__main__': 12 | setup() 13 | -------------------------------------------------------------------------------- /pygem/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2018 David Rounce , David Rounce 5 | 6 | Distributed under the MIT license 7 | """ 8 | 9 | This directory contains example 1d elevation change data following the format specifications of PyGEM. -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | # build: 2 | # python setup.py sdist bdist_wheel 3 | poetry lock --no-update 4 | poetry build 5 | 6 | # upload: 7 | python -m twine upload dist/* 8 | # poetry publish # was not able to publish with poetry for some reason... 9 | 10 | # cleanup: 11 | rm -r eggs/ 12 | rm -r dist/ 13 | rm -rf pygem.egg-info 14 | rm -rf build/ 15 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | 3 | SPHINXOPTS ?= 4 | SPHINXBUILD ?= sphinx-build 5 | SOURCEDIR = . 6 | BUILDDIR = _build 7 | 8 | # Help target 9 | help: 10 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) 11 | 12 | .PHONY: help Makefile 13 | 14 | # Catch-all target for all unknown targets 15 | %: Makefile 16 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) 17 | -------------------------------------------------------------------------------- /docs/dynamics_oggm.md: -------------------------------------------------------------------------------- 1 | ## OGGM Flowline Model 2 | PyGEM has been developed to be compatible with OGGM thereby enabling the use of their ice dynamics flowline model ([Maussion et al. 2019](https://gmd.copernicus.org/articles/12/909/2019/)). The model uses a shallow ice approximation with a depth-integrated flowline model to explicitly compute the flux of ice along the glacier centerline. Model details are fully documented in OGGM’s manual found [here](https://docs.oggm.org/en/latest/ice-dynamics.html). -------------------------------------------------------------------------------- /docs/citing.md: -------------------------------------------------------------------------------- 1 | # Citing PyGEM 2 | The most recent version of PyGEM was published in [Science](https://www.science.org/doi/10.1126/science.abo1324). Therefore, if using PyGEM, please cite: 3 |

Rounce, D.R., Hock, R., Maussion, F., Hugonnet, R., Kochtitzky, W., Huss, M., Berthier, E., Brinkerhoff, D., Compagno, L., Copland, L., Farinotti, D., Menounos, B., and McNabb, R.W. (2023). “Global glacier change in the 21st century: Every increase in temperature matters”, Science, 379(6627), pp. 78-83, doi:10.1126/science.abo1324. 4 | 5 | ```{figure} _static/science_cover.jpg 6 | --- 7 | width: 50% 8 | --- 9 | ``` -------------------------------------------------------------------------------- /docs/pygem.tests.rst: -------------------------------------------------------------------------------- 1 | pygem.tests package 2 | =================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | pygem.tests.test\_basics module 8 | ------------------------------- 9 | 10 | .. automodule:: pygem.tests.test_basics 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pygem.tests.test\_oggm\_compat module 16 | ------------------------------------- 17 | 18 | .. automodule:: pygem.tests.test_oggm_compat 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: pygem.tests 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /pygem/scraps/dummy_task_module.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import xarray as xr 4 | from oggm import cfg 5 | from oggm.utils import entity_task 6 | 7 | # Module logger 8 | log = logging.getLogger(__name__) 9 | 10 | # Add the new name "my_netcdf_file" to the list of things that the GlacierDirectory understands 11 | cfg.BASENAMES['my_netcdf_file'] = ( 12 | 'somefilename.nc', 13 | 'This is just a documentation string', 14 | ) 15 | 16 | 17 | @entity_task(log, writes=[]) 18 | def dummy_task(gdir, some_param=None): 19 | """Very dummy""" 20 | 21 | fpath = gdir.get_filepath('my_netcdf_file') 22 | da = xr.DataArray([1, 2, 3]) 23 | da.to_netcdf(fpath) 24 | -------------------------------------------------------------------------------- /docs/scripts_overview.md: -------------------------------------------------------------------------------- 1 | (scripts_overview_target)= 2 | # Script Details 3 | The [Model Workflow](model_workflow_target) and [Test Model](test_model_target) sections provide a general overview of the workflows and how to use the scripts. Here we provide more detail of the primary scripts used to run PyGEM. This information can help support your understanding of the inner workings of the code and also provide an overview for developers who may want to fork the codes and create new content. The following scripts are described: 4 | 5 | ```{toctree} 6 | --- 7 | caption: Primary Scripts: 8 | maxdepth: 1 9 | --- 10 | 11 | run_calibration_frontalablation_overview 12 | run_inversion_overview 13 | run_calibration_overview 14 | run_simulation_overview 15 | ``` -------------------------------------------------------------------------------- /docs/_static/elev_change_1d/01.00570_elev_change_1d.json: -------------------------------------------------------------------------------- 1 | {"ref_dem": "COP30", "ref_dem_year": 2013, "dates": [["2016-06-01", "2017-06-01"]], "bin_edges": [1300.0, 1400.0, 1500.0, 1600.0, 1700.0, 1800.0, 1900.0, 2000.0, 2100.0, 2200.0, 2300.0], "bin_centers": [1350.0, 1450.0, 1550.0, 1650.0, 1750.0, 1850.0, 1950.0, 2050.0, 2150.0, 2250.0], "bin_area": [79778.125, 107009.375, 96590.625, 138362.5, 176418.75, 231671.875, 270537.5, 218462.5, 137959.375, 100803.125], "dh": [[-4.50164794921875, -3.091949462890625, -2.728790283203125, -1.95526123046875, -2.159393310546875, -1.4642333984375, -1.630615234375, -1.223602294921875, -1.411376953125, -1.162353515625]], "dh_sigma": [[2.37652587890625, 1.5350341796875, 1.53717041015625, 3.421600341796875, 7.1052093505859375, 1.008819580078125, 1.78204345703125, 1.511993408203125, 1.4329833984375, 2.39971923828125]]} -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | name: pygem_rtd 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python>=3.10,<3.13 6 | - jupyter 7 | - jupyterlab 8 | - numpy 9 | - scipy 10 | - pandas 11 | - shapely 12 | - matplotlib 13 | - Pillow 14 | - netcdf4 15 | - scikit-image 16 | - scikit-learn 17 | - configobj 18 | - xarray 19 | - pytest 20 | - dask 21 | - bottleneck 22 | - pyproj 23 | - cartopy 24 | - geopandas 25 | - rasterio 26 | - rioxarray 27 | - xarray 28 | - seaborn 29 | - pytables 30 | - salem 31 | - motionless 32 | - sphinx 33 | - sphinx-book-theme>=0.3.3 34 | - ipython 35 | - numpydoc 36 | - seaborn 37 | - sphinx-intl 38 | - sphinx-reredirects 39 | - pip 40 | - pip: 41 | - joblib 42 | - progressbar2 43 | - sphinx-togglebutton 44 | - sphinx-book-theme 45 | - myst-parser 46 | - oggm 47 | - pygem 48 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | # Install system dependencies 4 | RUN apt-get update && apt-get install -y --no-install-recommends \ 5 | sudo curl vim git tree python3-pip python3-venv python3-dev build-essential \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | # Add non-root user 'ubuntu' to sudo group 9 | RUN usermod -aG sudo ubuntu && \ 10 | echo "ubuntu ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ubuntu 11 | 12 | # Switch to non-root user 13 | USER ubuntu 14 | WORKDIR /home/ubuntu 15 | 16 | # Add .local/bin to PATH 17 | ENV PATH="/home/ubuntu/.local/bin:${PATH}" 18 | 19 | # What PyGEM branch to clone (either master or dev; see docker_pygem.yml) 20 | ARG PYGEM_BRANCH=master 21 | 22 | RUN git clone --branch ${PYGEM_BRANCH} https://github.com/PyGEM-Community/PyGEM.git && \ 23 | pip install --break-system-packages -e PyGEM 24 | 25 | # Clone the PyGEM notebooks repository, which are used for testing 26 | RUN git clone https://github.com/PyGEM-Community/PyGEM-notebooks.git -------------------------------------------------------------------------------- /docs/_static/elev_change_1d/01.00570_elev_change_1d.csv: -------------------------------------------------------------------------------- 1 | bin_start,bin_stop,bin_area,date_start,date_end,dh,dh_sigma,ref_dem,ref_dem_year 2 | 1300.0,1400.0,79778.125,2016-06-01,2017-06-01,-4.50164794921875,2.37652587890625,COP30,2013 3 | 1400.0,1500.0,107009.375,2016-06-01,2017-06-01,-3.091949462890625,1.5350341796875,COP30,2013 4 | 1500.0,1600.0,96590.625,2016-06-01,2017-06-01,-2.728790283203125,1.53717041015625,COP30,2013 5 | 1600.0,1700.0,138362.5,2016-06-01,2017-06-01,-1.95526123046875,3.421600341796875,COP30,2013 6 | 1700.0,1800.0,176418.75,2016-06-01,2017-06-01,-2.159393310546875,7.1052093505859375,COP30,2013 7 | 1800.0,1900.0,231671.875,2016-06-01,2017-06-01,-1.4642333984375,1.008819580078125,COP30,2013 8 | 1900.0,2000.0,270537.5,2016-06-01,2017-06-01,-1.630615234375,1.78204345703125,COP30,2013 9 | 2000.0,2100.0,218462.5,2016-06-01,2017-06-01,-1.223602294921875,1.511993408203125,COP30,2013 10 | 2100.0,2200.0,137959.375,2016-06-01,2017-06-01,-1.411376953125,1.4329833984375,COP30,2013 11 | 2200.0,2300.0,100803.125,2016-06-01,2017-06-01,-1.162353515625,2.39971923828125,COP30,2013 12 | -------------------------------------------------------------------------------- /docs/initial_conditions.md: -------------------------------------------------------------------------------- 1 | # Initial Conditions and Surface Type 2 | Initial glacier area is based on the RGI and assumed to represent the year 2000. The initial surface type is based on the glacier’s median elevation, with the higher elevation being classified as firn and the lower classified as ice (or debris-covered). The surface type evolves based on the five-year running average of the glacier bin’s annual climatic mass balance ([Huss and Hock, 2015](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full)). If the five running average is positive, then the surface is classified as firn, while if it is negative, the surface is classified as ice. The surface type is classified as snow when snow accumulates on the surface. 3 | 4 | During the ice thickness inversion, which is done using OGGM ([Maussion et al. 2019](https://gmd.copernicus.org/articles/12/909/2019/)), the glacier is assumed to be in steady state. As a result, at the start of the simulation, there is typically a dynamical adjustment as the ice thickness is redistributed in response to the climatic mass balance forcing. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 David Rounce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pygem/scraps/run.py: -------------------------------------------------------------------------------- 1 | # Libs 2 | import geopandas as gpd 3 | from oggm import cfg, utils, workflow 4 | 5 | # Initialize OGGM and set up the default run parameters 6 | cfg.initialize() 7 | 8 | # How many grid points around the glacier? 9 | cfg.PARAMS['border'] = 10 10 | 11 | # Make it robust 12 | cfg.PARAMS['use_intersects'] = False 13 | cfg.PARAMS['continue_on_error'] = True 14 | 15 | # Local working directory (where OGGM will write its output) 16 | cfg.PATHS['working_dir'] = utils.get_temp_dir('some_wd') 17 | 18 | # RGI file 19 | path = utils.get_rgi_region_file('11') 20 | rgidf = gpd.read_file(path) 21 | 22 | # Select only 2 glaciers 23 | rgidf = rgidf.iloc[:2] 24 | 25 | # Sort for more efficient parallel computing 26 | rgidf = rgidf.sort_values('Area', ascending=False) 27 | 28 | # Go - create the pre-processed glacier directories 29 | gdirs = workflow.init_glacier_directories(rgidf) 30 | 31 | # Our task now 32 | from dummy_task_module import dummy_task 33 | 34 | workflow.execute_entity_task(dummy_task, gdirs) 35 | 36 | # See that we can read the new dummy data: 37 | import xarray as xr 38 | 39 | fpath = gdirs[0].get_filepath('my_netcdf_file') 40 | print(xr.open_dataset(fpath)) 41 | -------------------------------------------------------------------------------- /docs/dynamics_parameterizations.md: -------------------------------------------------------------------------------- 1 | # Glacier Dynamics Models 2 | Glacier dynamics in large-scale glacier evolution models typically rely on geometry changes like volume-area-length scaling (e.g., [Radić and Hock, 2011](https://www.nature.com/articles/ngeo1052)), mass redistribution using empirical equations (e.g., [Huss and Hock, 2015](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full)), or simplified glacier dynamics (e.g., [Maussion et al., 2019](https://gmd.copernicus.org/articles/12/909/2019/)). [Zekollari et al. (2022)](https://agupubs.onlinelibrary.wiley.com/doi/full/10.1029/2021RG000754) provide a comprehensive review of ice dynamics for mountain glaciers. These methods all allow the glacier to evolve over time in response to the total glacier-wide mass balance. The benefit of volume-area-length scaling and mass redistribution is they are computationally inexpensive compared to simplified glacier dynamics methods. The two options available in PyGEM are OGGM’s flowline model using the shallow ice approximation ([Maussion et al. 2019](https://gmd.copernicus.org/articles/12/909/2019/)) and mass redistribution curves ([Rounce et al. 2020](https://www.frontiersin.org/articles/10.3389/feart.2019.00331/full)). 3 | 4 | ```{toctree} 5 | --- 6 | caption: Mass Balance Components: 7 | maxdepth: 2 8 | --- 9 | 10 | dynamics_oggm 11 | dynamics_massredistributioncurves 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/test_pygem.md: -------------------------------------------------------------------------------- 1 | (test_model_target)= 2 | # Test Model 3 | Once the conda environment is [properly installed](install_pygem_target) and you have an understanding of the model components, you are ready to run the model. Various Jupyter Notebooks are provided for testing PyGEM in a separate [GitHub repository](https://github.com/PyGEM-Community/PyGEM-notebooks). 4 | 5 | The following notebooks are intended to allow for introduction and testing of PyGEM and may be run using sample data that should have been downloaded during model installation and setup (see [here](https://pygem.readthedocs.io/en/latest/install_pygem.html)), but can also be downloaded directly [here](https://drive.google.com/file/d/1Wu4ZqpOKxnc4EYhcRHQbwGq95FoOxMfZ/view?usp=drive_link).
6 | - [simple_test](https://github.com/PyGEM-Community/PyGEM-notebooks/blob/main/simple_test.ipynb): simple introductory PyGEM test run using provided sample data for Khumbu Glacier
7 | - [advanced_test](https://github.com/PyGEM-Community/PyGEM-notebooks/blob/main/advanced_test.ipynb): a more advanced PyGEM test run, demonstrating Bayesian inference calibration and simulation, using provided sample data for Khumbu Glacier
8 | - [advanced_test_tw](https://github.com/PyGEM-Community/PyGEM-notebooks/blob/main/advanced_test_tw.ipynb): demonstrates calibration of the frontal ablation parameterization, using provided sample data for LeConte Glacier
-------------------------------------------------------------------------------- /docs/pygem.rst: -------------------------------------------------------------------------------- 1 | pygem package 2 | ============= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | pygem.tests 11 | 12 | Submodules 13 | ---------- 14 | 15 | pygem.class\_climate module 16 | --------------------------- 17 | 18 | .. automodule:: pygem.class_climate 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | pygem.gcmbiasadj module 24 | ----------------------- 25 | 26 | .. automodule:: pygem.gcmbiasadj 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | pygem.glacierdynamics module 32 | ---------------------------- 33 | 34 | .. automodule:: pygem.glacierdynamics 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | pygem.massbalance module 40 | ------------------------ 41 | 42 | .. automodule:: pygem.massbalance 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | pygem.oggm\_compat module 48 | ------------------------- 49 | 50 | .. automodule:: pygem.oggm_compat 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | pygem.pygem\_modelsetup module 56 | ------------------------------ 57 | 58 | .. automodule:: pygem.pygem_modelsetup 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | Module contents 64 | --------------- 65 | 66 | .. automodule:: pygem 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | -------------------------------------------------------------------------------- /pygem/tests/test_03_notebooks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | import pytest 5 | 6 | from pygem.setup.config import ConfigManager 7 | 8 | # instantiate ConfigManager 9 | config_manager = ConfigManager() 10 | # update export_extra_vars to True before running tests 11 | config_manager.update_config({'sim.out.export_extra_vars': True}) 12 | 13 | 14 | # Get all notebooks in the PyGEM-notebooks repository 15 | nb_dir = os.environ.get('PYGEM_NOTEBOOKS_DIRPATH') or os.path.join(os.path.expanduser('~'), 'PyGEM-notebooks') 16 | # TODO #54: Test all notebooks 17 | # notebooks = [f for f in os.listdir(nb_dir) if f.endswith('.ipynb')] 18 | 19 | # list of notebooks to test, in the desired order (failures may occur if order is changed) 20 | notebooks = [ 21 | 'simple_test.ipynb', # runs with sample_data 22 | 'simple_test_daily.ipynb', # runs with sample_data 23 | 'advanced_test.ipynb', # runs with sample_data 24 | 'dhdt_processing.ipynb', # runs with sample_data 25 | 'advanced_test_spinup_elev_change_calib.ipynb', # runs with sample_data, depends on dhdt_processing.ipynb results 26 | 'advanced_test_tw.ipynb', # runs with sample_data_tw 27 | ] 28 | 29 | 30 | @pytest.mark.parametrize('notebook', notebooks) 31 | def test_notebook(notebook): 32 | """ 33 | Run pytest with nbmake on the specified notebook. 34 | 35 | This test is parameterized to run each notebook individually, 36 | preserving the order defined in the `notebooks` list. 37 | """ 38 | subprocess.check_call(['pytest', '--nbmake', os.path.join(nb_dir, notebook)]) 39 | -------------------------------------------------------------------------------- /docs/run_inversion_overview.md: -------------------------------------------------------------------------------- 1 | (run_inversion_overview_target)= 2 | # run_inversion.py 3 | This script will perform ice thickness inversion while calibrating the ice viscosity ("Glen A") model parameter such that the modeled ice volume roughly matches the ice volume estimates from [Farinotti et al. (2019)](https://www.nature.com/articles/s41561-019-0300-3) for each RGI region. Run the script as follows: 4 | 5 | ``` 6 | run_inversion -rgi_region01 7 | ``` 8 | 9 | ## Script Structure 10 | 11 | Broadly speaking, the script follows: 12 | * Load glaciers 13 | * Load climate data 14 | * Compute apparent mass balance and invert for initial ice thickness 15 | * Use minimization to find agreement between our modeled and [Farinotti et al. (2019)](https://www.nature.com/articles/s41561-019-0300-3) modeled ice thickness estimates for each RGI region 16 | * Export the calibrated parameters 17 | 18 | ## Special Considerations 19 | The regional Glen A value is calibrated by inverting for the ice thickness of all glaciers in a given region without considering calving (all glaciers are considered land-terminating). After the "best" Glen A value is determined, a final round of ice thickness inversion is performed for tidewater glaciers with calving turned **on**. Running this script will by default export the regionally calibrated Glen A values to the path specfied by `sim.oggm_dynamics.glen_a_regional_relpath` in *~/PyGEM/config.yaml'*. The calibrated inversion parameters also get stored within a given glacier directories *diagnostics.json* file, e.g.: 20 | ``` 21 | {"dem_source": "COPDEM90", "flowline_type": "elevation_band", "apparent_mb_from_any_mb_residual": 2893.2237556771674, "inversion_glen_a": 3.784593106855888e-24, "inversion_fs": 0} 22 | ``` -------------------------------------------------------------------------------- /docs/mb_frontalablation.md: -------------------------------------------------------------------------------- 1 | ## Frontal Ablation 2 | For marine-terminating glaciers, frontal ablation is modeled using a frontal ablation parameterization coupled to the ice dynamical model (i.e., the glacier dynamics parameterization). Given the coupling to the dynamical model, frontal ablation is accounted for on an annual timestep and the code for the frontal ablation parameterization is located with the dynamical model. OGGM provides a nice overview of the frontal ablation parameterization in one of their advanced tutorials: 3 | 4 | https://oggm.org/tutorials/stable/notebooks/kcalving_parameterization.html 5 | 6 | The same parameterization is included for mass redistribution curves in PyGEM. 7 | 8 | Frontal ablation ($A_{f}$) computes the mass that is removed at the glacier front when the bedrock is below sea level using an empirical formula following [Oerlemans and Nick (2005)](https://www.cambridge.org/core/journals/annals-of-glaciology/article/minimal-model-of-a-tidewater-glacier/C6B72F547D8C44CDAAAD337E1F2FC97F): 9 | ```{math} 10 | A_{f} = k \cdot d \cdot H_{f} \cdot w 11 | ``` 12 | where $k$ is the frontal ablation scaling parameter (yr$^{-1}$), $d$ is the water depth at the calving front (m), $H_{f}$ is the ice thickness at the calving front, and $w$ is the glacier width at the calving front. Over the next century, many marine-terminating glaciers are projected to retreat onto land based on present-day frontal ablation rates ([Rounce et al. 2023](https://www.science.org/doi/10.1126/science.abo1324)); hence, the maximum frontal ablation rate is constrained by the mass of ice where the bed elevation of the bin is located below sea level. The user is also able to specify the water level (default is 0), which supports the application of the parameterization to lake-terminating glaciers in the future. -------------------------------------------------------------------------------- /.github/workflows/docker_pygem.yml: -------------------------------------------------------------------------------- 1 | name: 'Build bespoke PyGEM Docker container' 2 | 3 | on: 4 | # Trigger when these files change in an open PR 5 | pull_request: 6 | paths: 7 | - '.github/workflows/docker_pygem.yml' 8 | - 'docker/Dockerfile' 9 | 10 | # Trigger when these files change on the master or dev branches 11 | push: 12 | branches: 13 | - master 14 | - dev 15 | paths: 16 | - '.github/workflows/docker_pygem.yml' 17 | - 'docker/Dockerfile' 18 | 19 | # Trigger every Saturday at 12AM GMT 20 | schedule: 21 | - cron: '0 0 * * 6' 22 | 23 | # Manually trigger the workflow 24 | workflow_dispatch: 25 | 26 | # Stop the workflow if a new one is started 27 | concurrency: 28 | group: ${{ github.workflow }}-${{ github.ref }} 29 | cancel-in-progress: true 30 | 31 | permissions: 32 | contents: read 33 | packages: write 34 | 35 | jobs: 36 | docker: 37 | name: 'Build and push Docker container' 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - name: 'Check out the repo' 42 | uses: actions/checkout@v4 43 | 44 | - name: 'Set up Docker buildx' 45 | uses: docker/setup-buildx-action@v3 46 | 47 | - name: 'Log into GitHub Container Repository' 48 | uses: docker/login-action@v3 49 | with: 50 | registry: ghcr.io 51 | username: ${{ github.actor }} 52 | password: ${{ secrets.GITHUB_TOKEN }} 53 | logout: true 54 | 55 | - name: 'Build and Push Docker Container' 56 | uses: docker/build-push-action@v5 57 | with: 58 | push: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev' }} 59 | no-cache: true 60 | file: 'docker/Dockerfile' 61 | build-args: | 62 | PYGEM_BRANCH=${{ github.ref == 'refs/heads/master' && 'master' || 'dev' }} 63 | tags: | 64 | ghcr.io/pygem-community/pygem:${{ github.ref == 'refs/heads/master' && 'latest' || github.ref == 'refs/heads/dev' && 'dev' }} -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | import os 10 | import sys 11 | 12 | import tomllib 13 | 14 | sys.path.insert(0, os.path.abspath('../pygem/')) 15 | 16 | # source pyproject.toml to get release 17 | with open('../pyproject.toml', 'rb') as f: 18 | pyproject = tomllib.load(f) 19 | 20 | project = 'PyGEM' 21 | copyright = '2023, David Rounce' 22 | author = 'David Rounce' 23 | release = pyproject['tool']['poetry']['version'] 24 | 25 | # -- General configuration --------------------------------------------------- 26 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 27 | 28 | extensions = [ 29 | 'sphinx_book_theme', 30 | 'myst_parser', 31 | 'sphinx.ext.autodoc', 32 | 'sphinx.ext.autosummary', 33 | 'sphinx.ext.intersphinx', 34 | 'numpydoc', 35 | 'sphinx.ext.viewcode', 36 | 'sphinx_togglebutton', 37 | ] 38 | 39 | myst_enable_extensions = [ 40 | 'amsmath', 41 | 'attrs_inline', 42 | 'colon_fence', 43 | 'deflist', 44 | 'dollarmath', 45 | 'fieldlist', 46 | 'html_admonition', 47 | 'html_image', 48 | ] 49 | 50 | # templates_path = ['_templates'] 51 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 52 | 53 | 54 | # -- Options for HTML output ------------------------------------------------- 55 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 56 | 57 | 58 | html_theme = 'sphinx_book_theme' 59 | 60 | html_theme_options = { 61 | 'repository_url': 'https://github.com/PyGEM-Community/PyGEM', 62 | 'use_repository_button': True, 63 | 'show_nav_level': 1, 64 | 'navigation_depth': 4, 65 | 'toc_title': 'On this page', 66 | } 67 | -------------------------------------------------------------------------------- /docs/publications.md: -------------------------------------------------------------------------------- 1 | (publications_target)= 2 | # Publications 3 | ## Publications describing PyGEM 4 | * Rounce, D.R., Hock, R., Maussion, F., Hugonnet, R., Kochtitzky, W., Huss, M., Berthier, E., Brinkerhoff, D., Compagno, L., Copland, L., Farinotti, D., Menounos, B., and McNabb, R.W. (2023). “[Global glacier change in the 21st century: Every increase in temperature matters](https://www.science.org/doi/10.1126/science.abo1324)”, Science, 379(6627), pp. 78-83, doi:10.1126/science.abo1324 5 | * Rounce, D.R., Hock, R., and Shean, D.E. (2020). “[Glacier mass change in High Mountain Asia through 2100 using the open-source Python Glacier Evolution Model (PyGEM)](https://www.frontiersin.org/articles/10.3389/feart.2019.00331/full)”, Frontiers in Earth Science, 7(331), pp. 1-20, doi:10.3389/feart.2019.00331 6 | * Rounce, D.R., Khurana, T., Short, M.B., Hock, R., Shean, D.E., and Brinkherhoff, D.J. (2020). “[Quantifying parameter uncertainty in a large-scale glacier evolution model using Bayesian inference – Application to High Mountain Asia](https://www.cambridge.org/core/journals/journal-of-glaciology/article/quantifying-parameter-uncertainty-in-a-largescale-glacier-evolution-model-using-bayesian-inference-application-to-high-mountain-asia/61D8956E9A6C27CC1A5AEBFCDADC0432)”, Journal of Glaciology, 66(256), pp. 175-187, doi:10.1017/jog.2019.91 7 | 8 | ## Publications using PyGEM 9 | * Yang, R, Hock, R., Rounce, D., Kang, S. (2024). "[Regional-scale response of glacier speed to seasonal runoff variations on the Kenai Peninsula, Alaska.](https://doi.org/10.1029/2025GL115248)", Geophysical Research Letters. 52, e2025GL115248, doi:10.1029/2025GL115248. 10 | * Kochtitzky W., Copland, L., Van Wychen, W., Hock, R., Rounce, D.R., Jiskoot, H., Scambos, T.A., Morlighem, M., King, M., Cha, L., Gould, L., Merrill,P-M., Glazovsky, A., Hugonnet, R., Strozzi, T., Noël, B., Navarro, F., Millan, R., Dowdeswell, J.A., Cook, A., Dalton, A., Khan, S., Jania, J. (2023). "[Progress towards globally complete frontal ablation estimates of marine-terminating glaciers](https://doi.org/10.1017/aog.2023.35)". Annals of Glaciology. doi:10.1017/aog.2023.35. -------------------------------------------------------------------------------- /pygem/bin/op/duplicate_gdirs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2024 Brandon Tober David Rounce 5 | 6 | Distributed under the MIT license 7 | 8 | duplicate OGGM glacier directories 9 | """ 10 | 11 | import argparse 12 | import os 13 | import shutil 14 | 15 | # pygem imports 16 | from pygem.setup.config import ConfigManager 17 | 18 | # instantiate ConfigManager 19 | config_manager = ConfigManager() 20 | # read the config 21 | pygem_prms = config_manager.read_config() 22 | 23 | 24 | def main(): 25 | parser = argparse.ArgumentParser( 26 | description='Script to make duplicate oggm glacier directories - primarily to avoid corruption if parellelizing runs on a single glacier' 27 | ) 28 | # add arguments 29 | parser.add_argument( 30 | '-rgi_glac_number', 31 | type=str, 32 | default=None, 33 | help='Randoph Glacier Inventory region', 34 | ) 35 | parser.add_argument( 36 | '-num_copies', 37 | type=int, 38 | default=1, 39 | help='Number of copies to create of the glacier directory data', 40 | ) 41 | args = parser.parse_args() 42 | num_copies = args.num_copies 43 | glac_num = args.rgi_glac_number 44 | 45 | if (glac_num is not None) and (num_copies) > 1: 46 | reg, id = glac_num.split('.') 47 | reg = reg.zfill(2) 48 | thous = id[:2] 49 | 50 | root = pygem_prms['root'] + '/' + pygem_prms['oggm']['oggm_gdir_relpath'] 51 | sfix = '/per_glacier/' + f'RGI60-{reg}/' + f'RGI60-{reg}.{thous}/' 52 | 53 | for n in range(num_copies): 54 | nroot = os.path.abspath(root.replace('gdirs', f'gdirs_{n + 1}')) 55 | # duplicate structure 56 | os.makedirs(nroot + sfix + f'RGI60-{reg}.{id}', exist_ok=True) 57 | # copy directory data 58 | shutil.copytree( 59 | root + sfix + f'RGI60-{reg}.{id}', 60 | nroot + sfix + f'RGI60-{reg}.{id}', 61 | dirs_exist_ok=True, 62 | ) 63 | 64 | return 65 | 66 | 67 | if __name__ == '__main__': 68 | main() 69 | -------------------------------------------------------------------------------- /docs/run_simulation_overview.md: -------------------------------------------------------------------------------- 1 | (run_simulation_target)= 2 | # run_simulation.py 3 | This script will run the glacier evolution model for the reference climate data or for future climate scenarios. If successful, the script will run without errors and generate one or more netcdf files. The user has the option to export essential statistics (e.g., area, mass, runoff, mass balance components)and/or binned data (e.g., ice thickness, area, mass, mass balance). The output general output will be: 4 | * ../Output/simulations/\[gcm_name\]/\[scenario\]/stats/\[glac_no\]...-all.nc 5 | 6 | When running the script, the GCM and scenario can be specified in the *~/PyGEM/config.yaml* configuration file or passed via the command line as follows: 7 | ``` 8 | run_simulation -gcm_name -scenario 9 | ``` 10 | 11 | ## Script Structure 12 | While most users may just want to run the model, those interested in developing new calibration schemes should be aware of the general structure of the script. Broadly speaking, the script follows: 13 | * Load glaciers 14 | * Load climate data 15 | * Bias correct the climate data 16 | * Load glacier data (area, etc.) 17 | * Load model parameters 18 | * Estimate ice thickness 19 | * Run simulation 20 | * Export model results 21 | 22 | ## View Output 23 | Various netcdf files may be generated. To view the results, we recommend using xarray as follows: 24 | 25 | ``` 26 | ds = xr.open_dataset(filename) 27 | print(ds) 28 | ``` 29 | 30 | ## Special Considerations 31 | There currently exist a series of try/except statements to ensure model runs are successful. The most common causes of failure are (i) advancing glacier exceeds the "borders" defined in OGGM's pre-processing and (ii) numerical instabilities within OGGM's dynamical model. The latter is more common for tidewater glaciers. If you want to avoid these issues, we suggest removing the try/except statements. 32 | 33 | ```{warning} 34 | sim_iters in the pygem_input.py specifies the number of iterations. If using MCMC as the calibration option, this becomes important. If you set sim_iters=1, the simulation will run using the median value of each model parameter. 35 | ``` -------------------------------------------------------------------------------- /docs/run_calibration_overview.md: -------------------------------------------------------------------------------- 1 | (run_calibration_target)= 2 | # run_calibration.py 3 | This script will calibrate the mass balance model parameters (degree-day factor of snow, precipitation factor, and temperature bias). 4 | 5 | When running the script, the calibration option can be specified in the *~/PyGEM/config.yaml* configuration file or passed via the command line as follows: 6 | ``` 7 | run_calibration -option_calibration 8 | ``` 9 | 10 | If successful, the script will run without errors and the following will be generated: 11 | * ../Output/calibration/\[glacno\]-modelprms_dict.json 12 | 13 | This is a JSON file that contains the calibration data. If the file already exists, the calibrated model option will be added to the existing .json file. 14 | 15 | ## Script Structure 16 | While most users may just want to run the model, those interested in developing new calibration schemes should be aware of the general structure of the script. Broadly speaking, the script follows: 17 | * Load glaciers 18 | * Load reference climate data 19 | * Load glacier data (area, etc.) 20 | * Load mass balance data 21 | * Calibrate the model parameters 22 | - "Modular" calibration options are included as an if/elif/else statement. 23 | * Export model parameters 24 | 25 | ## View Output 26 | The .json file stores a dictionary and each calibration option is a key to the dictionary. The model parameters are also stored in a dictionary (i.e., a dictionary within a dictionary) with each model parameter being a key to the dictionary that provides access to a list of values for that specific model parameter. The following shows an example of how to print a list of the precipitation factors ($k_{p}$) for the calibration option specified in the input file: 27 | 28 | ``` 29 | with open(modelprms_fullfn, 'r') as f: 30 | modelprms_dict = json.load(f) 31 | print(modelprms_dict[pygem_prms.option_calibration][‘kp’]) 32 | ``` 33 | 34 | ## Special Considerations 35 | Typically, the glacier area is assumed to be constant (option_dynamics=None), i.e., the glacier geometry is not updated, to reduce computational expense. 36 | 37 | Current calibration options rely solely on glacier-wide geodetic mass balance estimates. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Python Glacier Evolution Model (PyGEM) 2 | 3 | The Python Glacier Evolution Model (PyGEM) is an open-source glacier evolution model coded in Python that models the transient evolution of glaciers. Each glacier is modeled independently using a monthly timestep. PyGEM has a modular framework that allows different schemes to be used for model calibration or model physics (e.g., climatic mass balance, glacier dynamics). 4 | 5 | Details concerning the model installation, physics, and more may be found at [pygem.readthedocs.io](https://pygem.readthedocs.io/en/latest/). 6 | 7 | PyGEM versions prior to 1.0.0 are no longer actively supported. 8 | 9 | *** 10 | 11 | ### Model Testing 12 | 13 | To support model testing and demonstration, a suite of Jupyter notebooks can be found within a separate [PyGEM-notebooks](https://github.com/PyGEM-Community/PyGEM-notebooks) repository. 14 | 15 | *** 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 48 | 49 |
Version 21 | 22 |   23 | 24 |
Citation 29 | 30 |   31 | DOI 32 |
License 37 | 38 |
Systems 43 | - Ubuntu 20.04, 22.04
44 | - Red Hat Enterprise Linux (RHEL) 8.8
45 | - macOS (Intel & Apple Silicon)
46 | - Note, we suggest that Windows users install
PyGEM using either the Windows Subsystem
for Linux or Oracle VirtualBox 47 |
50 | -------------------------------------------------------------------------------- /pygem/bin/op/compress_gdirs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2024 Brandon Tober David Rounce 5 | 6 | Distributed under the MIT license 7 | 8 | compress OGGM glacier directories 9 | """ 10 | 11 | import argparse 12 | 13 | import geopandas as gpd 14 | import oggm.cfg as cfg 15 | from oggm import utils, workflow 16 | 17 | # pygem imports 18 | from pygem.setup.config import ConfigManager 19 | 20 | # instantiate ConfigManager 21 | config_manager = ConfigManager() 22 | # read the config 23 | pygem_prms = config_manager.read_config() 24 | 25 | # Initialize OGGM subprocess 26 | cfg.initialize(logging_level='WARNING') 27 | cfg.PATHS['working_dir'] = f'{pygem_prms["root"]}/{pygem_prms["oggm"]["oggm_gdir_relpath"]}' 28 | cfg.PARAMS['border'] = pygem_prms['oggm']['border'] 29 | cfg.PARAMS['use_multiprocessing'] = True 30 | 31 | 32 | def compress_region(region): 33 | print(f'\n=== Compressing glacier directories for RGI Region: {region} ===') 34 | # Get glacier IDs from the RGI shapefile 35 | rgi_ids = gpd.read_file(utils.get_rgi_region_file(str(region).zfill(2), version='62'))['RGIId'].tolist() 36 | 37 | # Initialize glacier directories 38 | gdirs = workflow.init_glacier_directories(rgi_ids) 39 | 40 | # Tar the individual glacier directories 41 | workflow.execute_entity_task(utils.gdir_to_tar, gdirs, delete=True) 42 | 43 | # Tar the bundles 44 | utils.base_dir_to_tar( 45 | f'{cfg.PATHS["working_dir"]}/per_glacier/RGI60-{str(region).zfill(2)}', 46 | delete=True, 47 | ) 48 | 49 | 50 | def main(): 51 | parser = argparse.ArgumentParser(description='Script to compress and store OGGM glacier directories') 52 | # add arguments 53 | parser.add_argument( 54 | '-rgi_region01', 55 | type=int, 56 | default=pygem_prms['setup']['rgi_region01'], 57 | help='Randoph Glacier Inventory region (can take multiple, e.g. `-run_region01 1 2 3`)', 58 | nargs='+', 59 | ) 60 | parser.add_argument( 61 | '-ncores', 62 | action='store', 63 | type=int, 64 | default=1, 65 | help='number of simultaneous processes (cores) to use', 66 | ) 67 | args = parser.parse_args() 68 | 69 | cfg.PARAMS['mp_processes'] = args.ncores 70 | 71 | for reg in args.rgi_region01: 72 | compress_region(reg) 73 | 74 | 75 | if __name__ == '__main__': 76 | main() 77 | -------------------------------------------------------------------------------- /pygem/tests/test_04_auxiliary.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import subprocess 4 | 5 | import pytest 6 | 7 | from pygem.setup.config import ConfigManager 8 | 9 | """ 10 | Test suite to any necessary aux""" 11 | 12 | 13 | @pytest.fixture(scope='module') 14 | def rootdir(): 15 | config_manager = ConfigManager() 16 | pygem_prms = config_manager.read_config() 17 | return pygem_prms['root'] 18 | 19 | 20 | def test_simulation_massredistribution_dynamics(rootdir): 21 | """ 22 | Test the run_simulation CLI script with the "MassRedistributionCurves" dynamical option. 23 | """ 24 | 25 | # Run run_simulation CLI script 26 | subprocess.run( 27 | [ 28 | 'run_simulation', 29 | '-rgi_glac_number', 30 | '1.03622', 31 | '-option_calibration', 32 | 'MCMC', 33 | '-sim_climate_name', 34 | 'ERA5', 35 | '-sim_startyear', 36 | '2000', 37 | '-sim_endyear', 38 | '2019', 39 | '-nsims', 40 | '1', 41 | '-option_dynamics', 42 | 'MassRedistributionCurves', 43 | '-outputfn_sfix', 44 | 'mrcdynamics_', 45 | ], 46 | check=True, 47 | ) 48 | 49 | # Check if output files were created 50 | outdir = os.path.join(rootdir, 'Output', 'simulations', '01', 'ERA5') 51 | output_files = glob.glob(os.path.join(outdir, '**', '*_mrcdynamics_all.nc'), recursive=True) 52 | assert output_files, f'Simulation output file not found in {outdir}' 53 | 54 | 55 | def test_simulation_no_dynamics(rootdir): 56 | """ 57 | Test the run_simulation CLI script with no dynamics option. 58 | """ 59 | 60 | # Run run_simulation CLI script 61 | subprocess.run( 62 | [ 63 | 'run_simulation', 64 | '-rgi_glac_number', 65 | '1.03622', 66 | '-option_calibration', 67 | 'MCMC', 68 | '-sim_climate_name', 69 | 'ERA5', 70 | '-sim_startyear', 71 | '2000', 72 | '-sim_endyear', 73 | '2019', 74 | '-nsims', 75 | '1', 76 | '-option_dynamics', 77 | 'None', 78 | '-outputfn_sfix', 79 | 'nodynamics_', 80 | ], 81 | check=True, 82 | ) 83 | 84 | # Check if output files were created 85 | outdir = os.path.join(rootdir, 'Output', 'simulations', '01', 'ERA5') 86 | output_files = glob.glob(os.path.join(outdir, '**', '*_nodynamics_all.nc'), recursive=True) 87 | assert output_files, f'Simulation output file not found in {outdir}' 88 | -------------------------------------------------------------------------------- /docs/run_calibration_frontalablation_overview.md: -------------------------------------------------------------------------------- 1 | (run_calibration_fa_overview_target)= 2 | # run_calibration_frontalablation.py 3 | This script will perform all pre-processing and calibration required to calibrate the frontal ablation parameterization for marine-terminating glaciers. If successful, the script will run without errors, generate numerous diagnostic plots, and most importantly, produce a .csv file with the calibration outputs. 4 | 5 | ## Script Structure 6 | The frontal ablation calibration runs through several steps on a regional basis. First, the frontal ablation data are merged together into a single dataset. 7 | 8 | Next, the frontal ablation parameter is calibrated for each marine-terminating glacier. The geodetic mass balance dataset is used to ensure that the frontal ablation estimates do not produce unrealistic climatic mass balance estimates. In the event that they do, a correction is performed to limit the frontal ablation based on a lower bound for the climatic mass balance based on regional mass balance data. This exports a .csv file for each region that includes the calibrated model parameters as well as numerous other columns to be able to compare the observed and modeled frontal ablation as well as the climatic mass balance. 9 | 10 | All the frontal ablation parameters for each marine-terminating glacier in a given region are then merged together into a single file. 11 | 12 | Lastly, the climatic-basal mass balance data is updated by removing the frontal ablation from the total mass change. This is an important step since the geodetic mass balance data do not account for the mass loss below sea level; however, some of the observed mass change is due to frontal ablation that is above the water level, which is accounted for by this script. This script will export a new "corrected" .csv file to replace the previous mass balance .csv file. 13 | 14 | 15 | To perform the frontal ablation calibration steps outlined above, simply call the run_calibration_frontalablation python script (by default all regions are calibrated): 16 | ``` 17 | run_calibration_frontalablation #optionally pass -rgi_region01 18 | ``` 19 | 20 | ## Special Considerations 21 | Circularity issues exist in calibrating the frontal ablation parameter as the mass balance model parameters are required to estimate the ice thickness, but the frontal ablation will affect the mass balance estimates and thus the mass balance model parameters. We suggest taking an iterative approach: calibrate the mass balance model parameters, calibrate the frontal ablation parameter, update the glacier-wide climatic mass balance, and recalibrate the mass balance model parameters. 22 | 23 | Currently only one iteration has been used, but this could be investigated further in the future. -------------------------------------------------------------------------------- /docs/mb_ablation.md: -------------------------------------------------------------------------------- 1 | ## Ablation 2 | There are currently two model options for ablation. Both model options use a degree-day model ($f$). 3 | 4 | ### Option 1: monthly temperatures 5 | The first calculates ablation ($a$) using the mean monthly temperature: 6 | $a=f_{snow/ice/firn/debris} \cdot T_{m}^{+} \cdot n$ 7 | 8 | where $f$ is the degree-day factor of snow, ice, firn, or debris (m w.e. d$^{-1}$ °C$^{-1}$), $T_{m}^{+}$ is the positive monthly mean temperature (°C), and $n$ is the number of days per month. 9 | 10 | ### Option 2: monthly temperatures with daily variance 11 | The second option incorporates the daily variance associated with the temperature for each month according to [Huss and Hock (2015)](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full): 12 | $a=f_{snow/ice/firn/debris} \cdot \sum_{i=1}^{ndays} T_{d,i}^{+} $ 13 | 14 | where $T_{d}$ is the daily positive mean air temperature and is estimated by superimposing random variability from the standard deviation of the daily temperature for each month. 15 | 16 | The degree-day factors for snow, ice, firn, and debris depend on the surface type option that is chosen by the user (see Section 5). The values of $f$ for these various surface types are assumed to be related to one another to reduce the number of model parameters. The default ratio of the $f_{snow}$ to the $f_{ice}$ is 0.7, and $f_{firn}$ is assumed to be the mean of the $f_{snow}$ and $f_{ice}$; however, the user may change these values in the input file if desired. The values for $f_{debris}$ are equal to $f_{ice}$ multiplied by the mean sub-debris melt enhancement factor [(Rounce et al. 2021)](https://agupubs.onlinelibrary.wiley.com/doi/full/10.1029/2020GL091311) for the given elevation bin. 17 | 18 | ### Temperature at elevation bins 19 | Temperature for each elevation bin ($T_{bin}$) is determined by selecting the temperature from the gridded climate data ($T_{gcm}$) based on the nearest neighbor, which is then downscaled to the elevation bins on the glacier according to: 20 | $T_{bin} = T_{gcm} + lr_{gcm} \cdot (z_{ref} - z_{gcm}) + lr_{glac} \cdot (z_{bin} - z_{ref}) + T_{bias}$ 21 | 22 | where $lr_{gcm}$ and $lr_{glac}$ are lapse rates (°C m-1) associated with downscaling the climate data to the glacier and then over the glacier elevation bins, respectively; $z_{ref}$, $z_{gcm}$, and $z_{bin}$ are the elevations from the glacier’s reference point (median or mean elevation), the climate data, and the elevation bin, respectively; and $T_{bias}$ is the temperature bias. The temperature bias is one of three model parameters that is calibrated and serves to account for any biases resulting from the use of coarse climate data that is unable to capture local topographic variations. By default, the $lr_{gcm}$ and $lr_{glac}$ are assumed to be equal. -------------------------------------------------------------------------------- /.github/workflows/test_suite.yml: -------------------------------------------------------------------------------- 1 | name: 'Install PyGEM and Run Test Suite' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - dev 8 | paths: 9 | - '**.py' 10 | - '.github/workflows/test_suite.yml' 11 | - 'pyproject.toml' 12 | 13 | pull_request: 14 | paths: 15 | - '**.py' 16 | - '.github/workflows/test_suite.yml' 17 | - 'pyproject.toml' 18 | 19 | # Run test suite every Saturday at 1AM GMT (1 hour after the Docker image is updated) 20 | schedule: 21 | - cron: '0 1 * * 6' 22 | 23 | # Stop the workflow if a new one is started 24 | concurrency: 25 | group: ${{ github.workflow }}-${{ github.ref }} 26 | cancel-in-progress: true 27 | 28 | jobs: 29 | test_suite: 30 | name: 'Test suite' 31 | runs-on: ubuntu-latest 32 | container: 33 | # Use pygem:latest for master branch and pygem:dev otherwise 34 | image: ghcr.io/pygem-community/pygem:${{ github.ref == 'refs/heads/master' && 'latest' || 'dev' }} 35 | options: --user root 36 | env: 37 | # Since we are root we need to set PYTHONPATH to be able to find the installed packages 38 | PYTHONPATH: /home/ubuntu/.local/lib/python3.12/site-packages 39 | 40 | steps: 41 | - name: 'Checkout the PyGEM repo' 42 | id: checkout 43 | uses: actions/checkout@v4 44 | 45 | - name: 'Reinstall PyGEM' 46 | run: pip install --break-system-packages -e . 47 | 48 | - name: 'Run ruff linting check' 49 | run: ruff check . 50 | 51 | - name: 'Run ruff formatting check' 52 | if: ${{ !cancelled() }} 53 | run: ruff format . --check 54 | 55 | - name: 'Initialize PyGEM' 56 | run: initialize 57 | 58 | - name: 'Clone the PyGEM-notebooks repo' 59 | run: | 60 | BRANCH=${GITHUB_REF#refs/heads/} 61 | if [ "$BRANCH" = "main" ]; then 62 | NOTEBOOK_BRANCH="main" 63 | else 64 | NOTEBOOK_BRANCH="dev" 65 | fi 66 | git clone --depth 1 --branch "$NOTEBOOK_BRANCH" https://github.com/pygem-community/PyGEM-notebooks.git 67 | echo "PYGEM_NOTEBOOKS_DIRPATH=$(pwd)/PyGEM-notebooks" >> "$GITHUB_ENV" 68 | 69 | - name: 'Run tests' 70 | run: | 71 | python3 -m coverage erase 72 | # run each test file explicitly in the desired order 73 | python3 -m pytest --cov=pygem -v --durations=20 pygem/tests/test_01_basics.py 74 | python3 -m pytest --cov=pygem -v --durations=20 pygem/tests/test_02_config.py 75 | python3 -m pytest --cov=pygem -v --durations=20 pygem/tests/test_03_notebooks.py 76 | python3 -m pytest --cov=pygem -v --durations=20 pygem/tests/test_04_auxiliary.py 77 | python3 -m pytest --cov=pygem -v --durations=20 pygem/tests/test_05_postproc.py 78 | -------------------------------------------------------------------------------- /docs/mb_refreezing.md: -------------------------------------------------------------------------------- 1 | ## Refreezing 2 | There are two model options for computing refreezing. The default option estimates refreezing based on the mean annual air temperature ([Woodward et al. 1997](https://www.cambridge.org/core/journals/annals-of-glaciology/article/influence-of-superimposedice-formation-on-the-sensitivity-of-glacier-mass-balance-to-climate-change/84DFA08E9CC8F28BE0729F1EBF4DA4E1)), while the alternative is based on heat conduction ([Huss and Hock, 2015](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full)). 3 | 4 | ### Option 1: Mean annual air temperature 5 | For the default option, refreezing (R) is calculated for each elevation bin as a function of its weighted annual mean air temperature (Ta) following [Woodward et al. (1997)](https://www.cambridge.org/core/journals/annals-of-glaciology/article/influence-of-superimposedice-formation-on-the-sensitivity-of-glacier-mass-balance-to-climate-change/84DFA08E9CC8F28BE0729F1EBF4DA4E1)): 6 | $R = -0.0069 \cdot T_{a} + 0.000096$ 7 | The weighted annual mean accounts for the number of days in each month. Refreezing cannot be negative. The model assumes that refreezing occurs in the snowpack as opposed to being superimposed ice, so in the ablation zone the refreezing cannot exceed the snow depth. Since this option estimates annual refreezing, the equation above provides the maximum amount of potential refreezing. Each year, the potential refreezing is reset in October as this is the transition season from predominantly summer melt to winter accumulation. Therefore, it is possible that accumulated snow may melt and refreeze diurnally. The model replicates this physical behavior by first determining the amount of snow that has melted in that month. If the refreezing potential is greater than zero, the model assumes the snow melts and refreezes in the snow pack. Refreezing cannot exceed the amount of snow melt in any given month. The amount of refreezing is then subtracted from the potential refreezing until the potential refreezing is fully depleted or reset. Once the snow and refreezing completely melts, the model can melt the underlying ice/firn/snow. This model is used as the default because it is computationally cheap compared to the alternative. 8 | 9 | ### Option 2: Heat conduction 10 | The alternative option is to estimate refreezing using modeled snow and firn temperatures based on heat conduction as described by [Huss and Hock (2015)](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full). This option is significantly more computationally expensive. The code for this section was translated from the IDL code used in [Huss and Hock (2015)](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full), which potentially has an error in the heat conduction equation where the temperature in each layer is divided by a factor of 2 that Lilian Schuster identified is not physically-derived (as of 2021 - this error has not been fixed). This option should thus be used with caution until further developed. -------------------------------------------------------------------------------- /docs/bias_corrections.md: -------------------------------------------------------------------------------- 1 | # Bias Corrections 2 | Bias corrections can be applied to ensure the temperature and precipitation associated with the future climate data is roughly consistent with the reference climate data. 3 | 4 | ## Delta change 5 | The temperature bias corrections follow [Huss and Hock (2015)](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full) and use an additive factor to ensure the mean monthly temperature and interannual monthly variability are consistent between the reference and future climate data. 6 | 7 | There are two options for the precipitation bias correction: 8 | 9 | **Option 2** follows [Huss and Hock (2015)](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full) and uses a multiplicative factor to ensure the monthly mean precipitation are consistent between the reference and future climate data. 10 | 11 | **Option 1** modifies the approach of [Huss and Hock (2015)](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full), since in dry regions, the adjusted precipitation was found to be unrealistically high when using multiplicative values. This option uses the interannual monthly variability and quality controls the bias corrected precipitation by replacing any values that exceed the monthly maximum with the monthly average adjusted for potentially wetter years in the future using the normalized annual variation. Note that [Marzeion et al. (2020)](https://agupubs.onlinelibrary.wiley.com/doi/full/10.1029/2019EF001470) suggests that [Huss and Hock (2015)](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full) have updated their approach to also adjust the precipitation bias correction to account for monthly interannual variability as well. 12 | 13 | ## Quantile delta mapping 14 | The temperature and precipitation bias corrections follow the approach below: 15 | 16 | **Option 3** follows [Lader et al. (2017)](https://journals.ametsoc.org/view/journals/apme/56/9/jamc-d-16-0415.1.xml) and uses a quantile delta mapping approach to capture both the future mean climate and future climatic extremes. This option determines the relative change between the future and historical simulated climate data at each percentile in the distribution and overlays the difference onto the reference dataset. For example, the 70th-percentile precipitation from a simulated future distribution is divided by the 70th-percentile precipitation from the simulated historic distribution to calculate the modeled ratio of change. This ratio is then multiplied by the 70th-percentile precipitation from the reference dataset to calculate the bias-corrected future value. This bias correction is applied in 20-year increments (e.g., 2020-2040, 2040-2060) to better represent the changing climate over time. 17 | 18 | ```{note} 19 | These bias corrections significantly impact projections for all glacier evolution models. Current work is thus investigating different bias corrections and how they affect projections. We hope to add additional bias correction options in the near future. 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pygem documentation master file, created by 2 | sphinx-quickstart on Sat Jun 10 23:30:41 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to PyGEM's documentation! 7 | ================================= 8 | The Python Glacier Evolution Model (PyGEM) is an open-source glacier evolution model coded in Python that is designed to model the transient evolution of glaciers on regional 9 | and global scales. Each glacier is modeled independently using a given time step and elevation bins. The model computes the climatic mass balance (i.e., snow accumulation minus 10 | melt plus refreezing) for each elevation bin and each monthly time step. Glacier geometry is updated annually. The model outputs a variety of data including monthly mass balance 11 | and its components (accumulation, melt, refreezing, frontal ablation, glacier runoff), and annual volume, volume below sea level, and area. 12 | 13 | PyGEM has a modular framework that allows different schemes to be used for model calibration or model physics (e.g., ablation, accumulation, refreezing, glacier dynamics). 14 | The most recent version of PyGEM, published in *Science* `(Rounce et al., 2023) `_, has been made compatible with the 15 | Open Global Glacier Model `(OGGM) `_ to both leverage the pre-processing tools (e.g., digital elevation models, glacier characteristics) and their advances 16 | with respect to modeling glacier dynamics and ice thickness inversions. 17 | 18 | .. admonition:: Note for new users: 19 | 20 | - Looking for a quick overview? Check out the :doc:`model structure and workflow `. 21 | - Want to read some studies? Check out our :doc:`publications `. 22 | - Want to see what PyGEM can do? Check out this presentation about PyGEM's latest developments: 23 | 24 | .. raw:: html 25 | 26 | 27 | 28 | 29 | .. toctree:: 30 | :maxdepth: 1 31 | :caption: Overview: 32 | 33 | introduction 34 | model_structure 35 | model_inputs 36 | mb_parameterizations 37 | dynamics_parameterizations 38 | runoff 39 | calibration_options 40 | bias_corrections 41 | initial_conditions 42 | limitations 43 | publications 44 | faqs 45 | 46 | .. toctree:: 47 | :maxdepth: 1 48 | :caption: Getting Started: 49 | 50 | install_pygem 51 | configuration 52 | scripts_overview 53 | test_pygem 54 | model_output 55 | 56 | .. toctree:: 57 | :maxdepth: 1 58 | :caption: Contributing: 59 | 60 | contributing 61 | 62 | .. toctree:: 63 | :maxdepth: 1 64 | :caption: Citing: 65 | 66 | citing 67 | .. Indices and tables 68 | .. ================== 69 | 70 | .. * :ref:`genindex` 71 | .. * :ref:`modindex` 72 | .. * :ref:`search` 73 | -------------------------------------------------------------------------------- /docs/runoff.md: -------------------------------------------------------------------------------- 1 | # Glacier Runoff 2 | Following [Huss and Hock (2018)](https://www.nature.com/articles/s41558-017-0049-x), glacier runoff ($Q$) is defined as all water that leaves the initial glacierized area, which includes rain ($P_{liquid}$), ablation ($a$), and refreezing ($R$) as follows: 3 | 4 | ```{math} 5 | Q = P_{liquid} + a - R 6 | ``` 7 | 8 | In the case of glacier retreat, rain, snow melt, and refreezing are computed for the non-glaciated portion of the initial glacier area and this runoff is referred to as “off-glacier” runoff. No other processes, e.g., evapotranspiration or groundwater recharge, are accounted for in these deglaciated areas. 9 | 10 | ```{warning} 11 | In the case of glacier advance, runoff is computed over the current year’s glacier area, which may exceed the initial glacierized area. Given that most glaciers are retreating, the increase in glacier runoff due to the additional glacier area is considered to be negligible. 12 | ``` 13 | 14 | ```{note} 15 | The model will also compute runoff assuming a moving-gauge, i.e., glacier runoff that is only computed from the glacierized areas. For some users who may want to use their models to compute off-glacier runoff, this is preferable. 16 | ``` 17 | 18 | 19 | ## Excess meltwater 20 | Excess meltwater is defined as the runoff caused by glacier mass loss that the glacier does not regain over the duration of the entire simulated period (**Figure 1**). For example, a glacier that melts completely contributes its entire mass as excess meltwater, while a glacier in equilibrium produces no excess meltwater. Since interannual glacier mass change is highly variable, i.e., a glacier can lose, gain, and then lose mass again, excess meltwater is computed retroactively as the last time that the glacier mass is lost. 21 | 22 | ```{figure} _static/excess_meltwater_diagram.png 23 | --- 24 | width: 100% 25 | --- 26 | ``` 27 | 28 | **Figure 1.** Diagram exemplifying how excess meltwater (blue dashed line) is calculated retroactively based on annual glacier mass balance (black line) over time. Cumulative (top subplot) and annual (bottom subplot) mass balance and excess meltwater are shown. Years refer to mass-balance years and values of cumulative mass balances and excess meltwater refer to the end of each mass-balance year. The total excess meltwater is equivalent to the total mass loss over the entire period, and therefore is not equal to the absolute sum of all annual negative mass balances if positive mass balances have occurred in the period. Excess meltwater is distributed retroactively over all mass-balance years that are negative and where the lost mass is not regained in the future. For example, annual mass balances in year 6, 7 and 9 are negative, but all mass loss lost between year 6 and 10 is regained by the end of year 10; thus, excess meltwater is zero in years 6 to 10 despite negative annual mass balances. Excess meltwater for any year with negative mass balance cannot exceed the annual net mass loss of that year. The figure is copied from [Rounce et al. (2020)](https://www.frontiersin.org/articles/10.3389/feart.2019.00331/full). 29 | -------------------------------------------------------------------------------- /docs/model_output.md: -------------------------------------------------------------------------------- 1 | (model_output_overview_target)= 2 | # Model Output 3 | The model outputs a variety of data including monthly mass balance and its components (accumulation, melt, refreezing, frontal ablation, glacier runoff), and annual mass, mass below sea level, and area. Results are written as a netcdf file (.nc) for each glacier. If multiple simulations are performed (e.g., for Monte Carlo simulations), then statistics related to the median and median absolute deviation are output for each parameter. 4 | 5 | In addition to this standard glacier-wide output, binned outputs are also available (by setting `sim["out"]["export_binned_data"]` to `true` in the [PyGEM configuration file](contributing_pygem_target)), which include each bins initial surface elevation, annual area, annual mass, annual thickness, annual climatic mass balance, and monthly climatic mass balance. Monthly climatic mass balance components can also be stored by setting `sim["out"]["export_binned_components"]` to `true`. 6 | 7 | ## Post-processing Data 8 | PyGEM simulations are output for each glacier individually. For most analyses, it is useful to aggregate or merge analyses to a regional or global scale. PyGEM's *postproc.compile_simulations.py* is designed to do just so. 9 | 10 | This script is designed to aggregate by region, scenario, and variable. For example the following call will result in 8 output files, the annual glacier mass and the annual glacier area for each specified scenario, for each specified region 11 | 12 | ``` 13 | compile_simulations -rgi_region 01 02 -scenario ssp245 ssp585 -gcm_startyear2000 -gcm_endyear 2100 -vars glac_mass_annual glac_area_annual 14 | ``` 15 | See below for more information. 16 | 17 | ## Analyzing Results 18 | Various Jupyter Notebooks are provided for analyzing PyGEM results in a separate [GitHub repository](https://github.com/PyGEM-Community/PyGEM-notebooks). 19 | 20 | - **analyze_regional_change.ipynb.ipynb**
This notebook demonstrates how to aggregate simulations by region and plot the glacier mass, area, and runoff changes. 21 | ```{figure} _static/analyze_glacier_change_region11.png 22 | --- 23 | width: 100% 24 | --- 25 | ``` 26 | - **analyze_glacier_change_byWatershed.ipynb**
This notebook can be used to aggregate glacier mass, area, and runoff into watersheds; specifically, it will create new netcdf files per watershed such that after the initial aggregation, analyses can be performed much more rapidly. The notebook continues to show an example plot of glacier mass, area, and runoff changes for each watershed in an example region: 27 | ```{figure} _static/R06_change.png 28 | --- 29 | width: 50% 30 | --- 31 | ``` 32 | ```{note} 33 | This notebook assumes that you have a "dictionary", i.e., a .csv file, that has each glacier of interest and the watershed (or other grouping) name associated with each glacier. 34 | ``` 35 | - **analyze_glacier_change_CrossSection.ipynb**
This notebook can be used to plot cross sections of an individual glacier's ice thickness over time for an ensemble of GCMs: 36 | ```{figure} _static/15.03733_profile_2100_ssps.png 37 | --- 38 | width: 100% 39 | --- 40 | ``` -------------------------------------------------------------------------------- /docs/mb_parameterizations.md: -------------------------------------------------------------------------------- 1 | # Mass Balance Models 2 | 3 | PyGEM computes the climatic mass balance for each elevation bin and timestep, estimates frontal ablation for marine-terminating glaciers at the end of each year (if this process is included), and updates the glacier geometry annually. The convention below follows [Cogley et al. (2011)](https://wgms.ch/downloads/Cogley_etal_2011.pdf). The total glacier-wide mass balance ($\Delta M$) is thus estimated as: 4 | 5 | ```{math} 6 | \Delta M = B_{clim} + A_{f}/S 7 | ``` 8 | 9 | where $B_{clim}$ is the climatic mass balance in specific units, i.e. mass change per unit area (m w.e.), $A_{f}$ is frontal ablation, and $S$ is the glacier area. The basal mass balance is assumed to be zero. 10 | 11 | The climatic mass balance for each elevation bin ($b_{clim}$) is computed according to: 12 | ```{math} 13 | b_{clim} = a + c + R 14 | ``` 15 | 16 | where $a$ is the ablation, $c$ is accumulation, and $R$ is refreezing (all in units of m w.e.). Mass loss is negative and mass gain is positive. The glacier-wide specific climatic mass balance ($B_{clim}$) is thus calculated by: 17 | ```{math} 18 | \sum_{i=1}^{nbins} b_{clim,i} 19 | ``` 20 | 21 | The model offers alternative methods for calculating the mass balance components and accounting for glacier geometry changes (i.e., representing glacier dynamics). These vary in level of complexity and computational expense. The current options for each component are described below: 22 | 23 | ```{toctree} 24 | --- 25 | caption: Mass Balance Components: 26 | maxdepth: 2 27 | --- 28 | 29 | mb_ablation 30 | mb_accumulation 31 | mb_refreezing 32 | mb_frontalablation 33 | ``` 34 | 35 | ## Summary of model parameters 36 | Below is a summary of some of the key mass balance model parameters, their symbols, units, and the values used in PyGEM. Note that some parameters are calculated, others are calibrated, and others may be specified by the user in the input file. 37 | 38 | | Parameter | Symbol | Unit | Value | 39 | | :--- | :--- | :--- | :--- | 40 | | Ablation | $a$ | m w.e. | calculated | 41 | | Accumulation | $c$ | m w.e. | calculated | 42 | | Refreeze | $R$ | m w.e. | calculated | 43 | | Frontal ablation | $A_{f}$ | m w.e. | calculated | 44 | | Degree-day factor of snow | $f_{snow}$ | mm w.e. d$^{-1}$ K$^{-1}$ | calibrated | 45 | | Degree-day factor of ice | $f_{ice}$ | mm w.e. d$^{-1}$ K$^{-1}$ | $f_{snow}$/0.7
(user-specified) | 46 | | Degree-day factor of firn | $f_{firn}$ | mm w.e. d$^{-1}$ K$^{-1}$ | $\frac{f_{snow}+f_{ice}}{2}$ | 47 | | Degree-day factor of debris | $f_{debris}$ | mm w.e. d$^{-1}$ K$^{-1}$ | $E_{d} \cdot f_{ice}$ | 48 | | Sub-debris melt enhancement factor | $E_{d}$ | - | 1 if no debris;
otherwise from [Rounce et al. (2021)](https://agupubs.onlinelibrary.wiley.com/doi/full/10.1029/2020GL091311) | 49 | | Temperature bias correction | $T_{bias}$ | K | calibrated | 50 | | Threshold temperature (rain/snow) | $T_{snow}$ | $^{\circ}$C | 1
(user-specified) | 51 | | Precipitation correction factor | $k_{p}$ | - | calibrated | 52 | | Precipitation gradient | $d_{prec}$ | m$^{-1}$ | 0.0001
(user-specified) | 53 | | Frontal ablation scaling parameter | $k$ | yr$^{-1}$ | calibrated | 54 | -------------------------------------------------------------------------------- /pygem/utils/_funcs_selectglaciers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2018 David Rounce 5 | 6 | Distributed under the MIT license 7 | 8 | Functions of different ways to select glaciers 9 | """ 10 | 11 | # Built-in libraries 12 | import os 13 | import pickle 14 | 15 | # External libraries 16 | import numpy as np 17 | import pandas as pd 18 | 19 | 20 | # %% ----- Functions to select specific glacier numbers ----- 21 | def get_same_glaciers(glac_fp, ending): 22 | """ 23 | Get same glaciers for testing of priors 24 | 25 | Parameters 26 | ---------- 27 | glac_fp : str 28 | filepath to where netcdf files of individual glaciers are held 29 | ending : str 30 | ending of the string that you want to get (ex. '.nc') 31 | 32 | Returns 33 | ------- 34 | glac_list : list 35 | list of rgi glacier numbers 36 | """ 37 | glac_list = [] 38 | for i in os.listdir(glac_fp): 39 | if i.endswith(ending): 40 | glac_list.append(i.split(ending)[0]) 41 | glac_list = sorted(glac_list) 42 | 43 | return glac_list 44 | 45 | 46 | def glac_num_fromrange(int_low, int_high): 47 | """ 48 | Generate list of glaciers for all numbers between two integers. 49 | 50 | Parameters 51 | ---------- 52 | int_low : int64 53 | low value of range 54 | int_high : int64 55 | high value of range 56 | 57 | Returns 58 | ------- 59 | y : list 60 | list of rgi glacier numbers 61 | """ 62 | x = (np.arange(int_low, int_high + 1)).tolist() 63 | y = [str(i).zfill(5) for i in x] 64 | return y 65 | 66 | 67 | def glac_fromcsv(csv_fullfn, cn='RGIId'): 68 | """ 69 | Generate list of glaciers from csv file 70 | 71 | Parameters 72 | ---------- 73 | csv_fp, csv_fn : str 74 | csv filepath and filename 75 | 76 | Returns 77 | ------- 78 | y : list 79 | list of glacier numbers, e.g., ['14.00001', 15.00001'] 80 | """ 81 | df = pd.read_csv(csv_fullfn) 82 | return [x.split('-')[1] for x in df[cn].values] 83 | 84 | 85 | def glac_wo_cal(regions, prms_fp_sub=None, cal_option='MCMC'): 86 | """ 87 | Glacier list of glaciers that still need to be calibrated 88 | """ 89 | todo_list = [] 90 | for reg in regions: 91 | prms_fns = [] 92 | prms_fp = prms_fp_sub + str(reg).zfill(2) + '/' 93 | for i in os.listdir(prms_fp): 94 | if i.endswith('-modelprms_dict.pkl'): 95 | prms_fns.append(i) 96 | 97 | prms_fns = sorted(prms_fns) 98 | 99 | for nfn, prms_fn in enumerate(prms_fns): 100 | glac_str = prms_fn.split('-')[0] 101 | 102 | if nfn % 500 == 0: 103 | print(glac_str) 104 | 105 | # Load model parameters 106 | with open(prms_fp + prms_fn, 'rb') as f: 107 | modelprms_dict = pickle.load(f) 108 | 109 | # Check if 'MCMC' is in the modelprms_dict 110 | if cal_option not in modelprms_dict.keys(): 111 | todo_list.append(glac_str) 112 | 113 | return todo_list 114 | -------------------------------------------------------------------------------- /docs/mb_accumulation.md: -------------------------------------------------------------------------------- 1 | ## Accumulation 2 | Accumulation ($c$) is calculated for each elevation bin as a function of the precipitation ($P_{bin}$), air temperature ($T_{bin}$), and the snow temperature threshold ($T_{snow}$). There are two options for estimating accumulation based on how to classify precipitation as liquid or solid. 3 | 4 | ### Option 1: Threshold +/- 1$^{\circ}$C 5 | The first (default) option is to estimate the ratio of liquid and solid precipitation based on the air temperature: 6 | $c = \delta \cdot P_{bin}$ 7 | 8 | where $\delta=1$; if $T_{bin} \leq T_{snow}-1$ 9 | 10 |           $\delta=0$; if $T_{bin} \geq T_{snow}+1$ 11 | 12 |           $\delta=0.5-(T_{bin}-T_{snow})/2$; if $T_{snow}-1 < T_{bin} < T_{snow}+1$ 13 | 14 |
where $P_{bin}$ is the monthly precipitation and $\delta$ is the fraction of solid precipitation each month. $T_{snow}$ typically ranges from 0 – 2 $^{\circ}$C ([Radić and Hock, 2011](https://www.nature.com/articles/ngeo1052); [Huss and Hock, 2015](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full)) and is typically assumed to be 1$^{\circ}$C. 15 | 16 | ### Option 2: Single threshold 17 | The alternative option is to classify precipitation as snow or rain based on a single threshold. 18 | 19 | ### Precipitation at elevation bins 20 | Precipitation at each elevation bin of the glacier ($P_{bin}$) is determined by selecting the precipitation from the gridded climate data ($P_{gcm}$) based on the nearest neighbor, which is then downscaled to the elevation bins on the glacier: 21 | $P_{bin} = P_{GCM} \cdot k_{p} \cdot (1 + d_{prec} \cdot (z_{bin} - z_{ref}))$ 22 |
where $k_{p}$ is the precipitation factor and $d_{prec}$ is the precipitation gradient. The precipitation factor is a model parameter that is used to adjust from the climate data to the glacier, which could be caused by local topographic effects due to differences in elevation, rain shadow effects, etc. The precipitation gradient is another model parameter, which is used to redistribute the precipitation along the glacier and can be thought of as a precipitation lapse rate. Typical values for the precipitation gradient vary from 0.01 – 0.025% m$^{-1}$([Huss and Hock, 2015](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full) who cited [WGMS, 2012](https://wgms.ch/products_fog/)). The default assumes a precipitation gradient of 0.01% m$^{-1}$ to reduce the number of model parameters. 23 | 24 | Additionally, for glaciers with high relief (> 1000 m), the precipitation in the uppermost 25% of the glacier’s elevation is reduced using an exponential function ([Huss and Hock, 2015](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full)): 25 | $P_{bin,exp} = P_{bin} \cdot exp(\frac{z_{bin} - z_{75\%}}{z_{max} - z_{75\%}}) $ 26 | where $P_{bin,exp}$ is the adjusted precipitation, and $z_{max}$ and $z_{75\%}$ are the elevation of the glacier maximum and the glacier’s 75th percentile elevation, respectively. The adjusted precipitation cannot be lower than 87.5% of the maximum precipitation on the glacier. This adjustment accounts for the reduced air moisture and increased wind erosion at higher elevations ([Benn and Lehmkuhl, 2000](https://risweb.st-andrews.ac.uk/portal/en/researchoutput/mass-balance-and-equilibriumline-altitudes-of-glaciers-in-highmountain-environments(080f17fc-33dd-4805-bc97-a5aaa018a457)/export.html)). -------------------------------------------------------------------------------- /docs/limitations.md: -------------------------------------------------------------------------------- 1 | # Limitations 2 | While PyGEM has a number of strengths, we also want to be transparent of several limitations that the current version has: 3 | * **Monthly timestep**: while we plan to add the option to use daily data in the future, the model is currently only set up for monthly timesteps. 4 | * **ERA5 climate data**: we currently use ERA5 data as our reference data and this is the only dataset that PyGEM currently supports. We are aware of recent studies that have shown reanalysis precipitation data have issues compared to other data (Alexander et al. 2020) and may consider alternative precipitation data in the future. The class_climate.py script can be modified to incorporate others. 5 | * **Geodetic mass balance data**: while we envision adding options to incorporate other datasets (e.g., multiple mass balance datasets, mass balance gradients, snowline altitudes) for model calibration, the current framework is set up for a single glacier-wide mass balance observation for each glacier. Note that the geodetic mass balance data requires the time period to be specific with each observation; however, the time period does not have to be the same for each glacier. 6 | * **Glacier-specific workflow**: the model requires mass balance data for each glacier for model calibration. We hope to incorporate regional data as well in the future. 7 | * **Off-glacier snow accumulation**: when the glacier retreats, the model continues to compute accumulation, melt, and refreeze of snow over the deglaciated area. Glacier retreat may create off-glacier areas at high altitudes. At high altitudes, it’s possible for the temperature to be negative year-round and thereby cause off-glacier snow to accumulate unrealistically over time. Potential solutions could include removing the snow each year assuming that it sublimates, developing an avalanche parameterization to add the snow onto the glacier, or an alternative we have not yet considered. It is good to be aware of this since snow at this elevation will not contribute to off-glacier runoff since it doesn’t melt. 8 | * **Runoff for advancing glaciers**: when the glacier advances, the model continues to compute the runoff over the entire glacier area, which may exceed the initial glacierized area. 9 | * **Initialization at 2000**: the RGI is used to initialize the glacier areas. While the RGI targets all glacier extents to be from 2000, this may significantly vary depending on the source of the data. We assume the glacier extents represent 2000 and do not correct for these issues. 10 | * **Mass balance-ice thickness circularity issues**: circular issues exist regarding the derivation of the mass balance gradient and the ice thickness, i.e., a mass balance gradient is needed to estimate the ice thickness and yet the ice thickness will determine how the glacier evolves. To avoid these circularity issues, we calibrate the model assuming the glacier area is constant. 11 | * **Frontal ablation for marine-terminating glaciers**: currently the frontal ablation parameterization is only set up for marine-terminating glaciers based on the terminus type specific by the RGI. Theoretically, the same framework could be applied to lake-terminating glaciers, but to our knowledge, datasets for calibration are not yet available and the formation of lakes is not yet included in the model. 12 | * **Continual development**: PyGEM is constantly evolving. We will do our best to keep documents updated, but it's always helpful to let us know if you're using PyGEM so we can ensure you are aware of the latest and/or upcoming changes. -------------------------------------------------------------------------------- /docs/install_pygem.md: -------------------------------------------------------------------------------- 1 | (install_pygem_target)= 2 | # Installation 3 | PyGEM has been tested successfully on Linux and macOS systems. For Windows users (Windows 10/11), we recommend installing the [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl) (WSL), and then installing and running PyGEM from there. 4 | 5 | PyGEM has been packaged using [Poetry](https://python-poetry.org/) and is hosted on the Python Package Index ([PyPI](https://pypi.org/project/pygem/)), to ensure that all dependencies install seamlessly. It is recommended that users create a [conda](https://docs.conda.io/projects/conda/en/latest/user-guide/index.html) environment from which to install the model dependencies and core code. If you do not yet have conda installed, see [conda's documentation](https://docs.conda.io/projects/conda/en/latest/user-guide/install) for instructions. 6 | 7 | Next, choose your preferred PyGEM installation option:
8 | - [**stable**](stable_install_target): this is the latest version that has been officially released to PyPI, with a fixed version number (e.g. v1.0.1). It is intended for general use. 9 | - [**development**](dev_install_target): this is the development version of PyGEM hosted on [GitHub](https://github.com/PyGEM-Community/PyGEM/tree/dev). It might contain new features and bug fixes, but is also likely to continue to change until a new release is made. This is the recommended option if you want to work with the latest changes to the code. Note, this installation options require [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) software to be installed on your computer. 10 | 11 | **Copyright note**: PyGEM's installation instructions are modified from that of [OGGM](https://docs.oggm.org/en/stable/installing-oggm.html) 12 | 13 | (stable_install_target)= 14 | ## Stable install 15 | The simplest **stable** installation method is to use an environment file. Right-click and save PyGEM's recommended environment file from [this link](https://raw.githubusercontent.com/PyGEM-Community/PyGEM/refs/heads/main/docs/pygem_environment.yml). 16 | 17 | From the folder where you saved the file, run `conda env create -f pygem_environment.yml`. 18 | ```{note} 19 | By default the environment will be named `pygem`. A different name can be specified in the environment file. 20 | ``` 21 | 22 | (dev_install_target)= 23 | ## Development install 24 | Install the [development version](https://github.com/PyGEM-Community/PyGEM/tree/dev) of PyGEM in your conda environment using pip: 25 | ``` 26 | pip uninstall pygem 27 | pip install git+https://github.com/PyGEM-Community/pygem/@dev 28 | ``` 29 | 30 | If you intend to access and make your own edits to the model's source code, see the [contribution guide](contributing_pygem_target). 31 | 32 | (setup_target)= 33 | # Setup 34 | Following installation, an initialization script should be executed. 35 | 36 | The initialization script accomplishes two things: 37 | 1. Initializes the PyGEM configuration file *~/PyGEM/config.yaml*. If this file already exists, an overwrite prompt will appear. 38 | 2. Downloads and unzips a series of sample data files to *~/PyGEM/*, which can also be manually downloaded [here](https://drive.google.com/drive/folders/1jbQvp0jKXkBZYi47EYhnPCPBcn3We068?usp=sharing). 39 | 40 | Run the initialization script by entering the following in the terminal: 41 | ``` 42 | initialize 43 | ``` 44 | 45 | # Demonstration Notebooks 46 | A series of accompanying Jupyter notebooks has been produced for demonstrating the functionality of PyGEM. These are hosted in the [PyGEM-notebooks repository](https://github.com/PyGEM-Community/PyGEM-notebooks). 47 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pygem" 3 | version = "1.1.0-beta" 4 | description = "Python Glacier Evolution Model (PyGEM)" 5 | authors = ["David Rounce ,Brandon Tober "] 6 | license = "MIT License" 7 | readme = "README.md" 8 | packages = [ 9 | { include = "pygem" } 10 | ] 11 | 12 | [tool.poetry.urls] 13 | Documentation = "https://pygem.readthedocs.io/" 14 | Repository = "https://github.com/PyGEM-Community/PyGEM" 15 | 16 | [tool.poetry.dependencies] 17 | python = ">=3.10,<3.13" 18 | PyYAML = "^6.0.2" 19 | salem = "^0.3.11" 20 | tables = "^3.10.1" 21 | geopandas = "^1.0.1" 22 | xarray = "^2024.10.0" 23 | pandas = "^2.2.3" 24 | numpy = "<2.0" 25 | scipy = "^1.14.1" 26 | matplotlib = "^3.9.2" 27 | rasterio = "^1.4.2" 28 | pyproj = "^3.7.0" 29 | torch = ">=2.0.0,<=2.2.2" 30 | gpytorch = "^1.13" 31 | scikit-learn = "^1.5.2" 32 | tqdm = "^4.66.6" 33 | jupyter = "^1.1.1" 34 | arviz = "^0.20.0" 35 | oggm = "^1.6.2" 36 | ruamel-yaml = "^0.18.10" 37 | ruff = ">=0.9.6" 38 | pytest = ">=8.3.4" 39 | pytest-cov = ">=6.0.0" 40 | nbmake = ">=1.5.5" 41 | 42 | [tool.poetry.scripts] 43 | initialize = "pygem.bin.op.initialize:main" 44 | preproc_fetch_mbdata = "pygem.bin.preproc.preproc_fetch_mbdata:main" 45 | preproc_wgms_estimate_kp = "pygem.bin.preproc.preproc_wgms_estimate_kp:main" 46 | run_spinup = "pygem.bin.run.run_spinup:main" 47 | run_inversion = "pygem.bin.run.run_inversion:main" 48 | run_calibration_frontalablation = "pygem.bin.run.run_calibration_frontalablation:main" 49 | run_calibration = "pygem.bin.run.run_calibration:main" 50 | run_mcmc_priors = "pygem.bin.run.run_mcmc_priors:main" 51 | run_simulation = "pygem.bin.run.run_simulation:main" 52 | postproc_subannual_mass = "pygem.bin.postproc.postproc_subannual_mass:main" 53 | postproc_binned_subannual_thick = "pygem.bin.postproc.postproc_binned_subannual_thick:main" 54 | postproc_distribute_ice = "pygem.bin.postproc.postproc_distribute_ice:main" 55 | postproc_compile_simulations = "pygem.bin.postproc.postproc_compile_simulations:main" 56 | list_failed_simulations = "pygem.bin.op.list_failed_simulations:main" 57 | duplicate_gdirs = "pygem.bin.op.duplicate_gdirs:main" 58 | compress_gdirs = "pygem.bin.op.compress_gdirs:main" 59 | 60 | [build-system] 61 | requires = ["poetry-core"] 62 | build-backend = "poetry.core.masonry.api" 63 | 64 | [tool.ruff] 65 | line-length = 120 66 | 67 | [tool.ruff.format] 68 | quote-style = "single" 69 | 70 | [tool.ruff.lint] 71 | select = [ 72 | "B", # flake8-bugbear 73 | "C", # mccabe complexity 74 | "E", "W", # Pycodestyle 75 | "F", # Pyflakes 76 | "I", # isort 77 | ] 78 | ignore = [ 79 | "B006", # Mutable data structures in argument defaults 80 | "B007", # Loop control variable not used within loop body 81 | "B008", # Function call `range` in argument defaults 82 | "B023", # Function definition does not bind loop variable 83 | "B905", # Missing explicit `strict=` parameter in `zip()` call 84 | "C405", # Unnecessary list literal 85 | "C408", # Unnecessary `dict()` call 86 | "C414", # Unnecessary `list()` call 87 | "C416", # Unnecessary list comprehension 88 | "C901", # Function too complex 89 | "E402", # Module level import not at top of file 90 | "E501", # Line too long 91 | "E712", # Avoid equality comparisons to `False` 92 | "E721", # Use `is` and `is not` for type comparisons, or `isinstance()` 93 | "E722", # Using bare `except` 94 | "F841", # Local variable assigned to but never used 95 | ] 96 | 97 | [tool.coverage.report] 98 | omit = ["pygem/tests/*"] 99 | show_missing = true 100 | skip_empty = true -------------------------------------------------------------------------------- /pygem/bin/preproc/preproc_fetch_mbdata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2018 David Rounce 5 | 6 | Distributed under the MIT license 7 | 8 | Fetch filled Hugonnet reference mass balance data 9 | """ 10 | 11 | # Built-in libraries 12 | import argparse 13 | import os 14 | 15 | # External libraries 16 | # oggm 17 | from oggm import utils 18 | 19 | # pygem imports 20 | from pygem.setup.config import ConfigManager 21 | 22 | # instantiate ConfigManager 23 | config_manager = ConfigManager() 24 | # read the config 25 | pygem_prms = config_manager.read_config() 26 | 27 | 28 | def run(fp='', debug=False, overwrite=False): 29 | """ 30 | pull geodetic mass balance data from OGGM 31 | The original 'raw' were acquired and combined from https://doi.org/10.6096/13 (time series/dh__rgi60_pergla_rates) 32 | The combined global data have been modified in three ways (code): 33 | 1. the glaciers in RGI region 12 (Caucasus) had to be manually linked to the product by Hugonnet because of large errors in the RGI outlines. The resulting product used by OGGM in region 12 has large uncertainties. 34 | 2. outliers have been filtered as following: all glaciers with an error estimate larger than 3 at the RGI region level are filtered out 35 | 3. all missing data (including outliers) are attributed with the regional average. 36 | 37 | See https://docs.oggm.org/en/latest/reference-mass-balance-data.html and https://nbviewer.org/urls/cluster.klima.uni-bremen.de/~oggm/geodetic_ref_mb/convert.ipynb for further information. 38 | 39 | dmdtda represents the average climatic mass balance (in units meters water-equivalent per year) over a given period \\frac{\\partial mass}{\\partial time \\partial area} 40 | """ 41 | mbdf = utils.get_geodetic_mb_dataframe() 42 | if debug: 43 | print('MB data loaded from OGGM:') 44 | print(mbdf.head()) 45 | 46 | # pull only 2000-2020 period 47 | mbdf_subset = mbdf[mbdf.period == '2000-01-01_2020-01-01'] 48 | 49 | # reset the index 50 | mbdf_subset = mbdf_subset.reset_index() 51 | 52 | # sort by the rgiid column 53 | mbdf_subset = mbdf_subset.sort_values(by='rgiid') 54 | 55 | # rename some keys to work with what other scripts/functions expect 56 | mbdf_subset = mbdf_subset.rename(columns={'dmdtda': 'mb_mwea', 'err_dmdtda': 'mb_mwea_err'}) 57 | 58 | if fp[-4:] != '.csv': 59 | fp += '.csv' 60 | 61 | if os.path.isfile(fp) and not overwrite: 62 | raise FileExistsError( 63 | f'The filled global geodetic mass balance file already exists, pass `-o` to overwrite, or pass a different file name: {fp}' 64 | ) 65 | 66 | mbdf_subset.to_csv(fp, index=False) 67 | if debug: 68 | print(f'Filled global geodetic mass balance data saved to: {fp}') 69 | print(mbdf_subset.head()) 70 | 71 | 72 | def main(): 73 | parser = argparse.ArgumentParser( 74 | description='grab filled Hugonnet et al. 2021 geodetic mass balance data from OGGM and converts to a format PyGEM utilizes' 75 | ) 76 | # add arguments 77 | parser.add_argument( 78 | '-fname', 79 | action='store', 80 | type=str, 81 | default=f'{pygem_prms["calib"]["data"]["massbalance"]["hugonnet2021_fn"]}', 82 | help='Reference mass balance data file name (default: df_pergla_global_20yr-filled.csv)', 83 | ) 84 | parser.add_argument( 85 | '-o', 86 | '--overwrite', 87 | action='store_true', 88 | help='Flag to overwrite existing geodetic mass balance data', 89 | ) 90 | parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') 91 | args = parser.parse_args() 92 | 93 | # hugonnet filepath 94 | fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["massbalance"]["hugonnet2021_relpath"]}/{args.fname}' 95 | 96 | run(fp, args.debug, args.overwrite) 97 | 98 | 99 | if __name__ == '__main__': 100 | main() 101 | -------------------------------------------------------------------------------- /pygem/tests/test_05_postproc.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import subprocess 4 | 5 | import numpy as np 6 | import pytest 7 | import xarray as xr 8 | 9 | from pygem.setup.config import ConfigManager 10 | 11 | 12 | @pytest.fixture(scope='module') 13 | def rootdir(): 14 | config_manager = ConfigManager() 15 | pygem_prms = config_manager.read_config() 16 | return pygem_prms['root'] 17 | 18 | 19 | def test_postproc_subannual_mass(rootdir): 20 | """ 21 | Test the postproc_subannual_mass CLI script. 22 | """ 23 | simdir = os.path.join(rootdir, 'Output', 'simulations', '01', 'CESM2', 'ssp245', 'stats') 24 | 25 | # Run postproc_monthyl_mass CLI script 26 | subprocess.run(['postproc_subannual_mass', '-simdir', simdir], check=True) 27 | 28 | 29 | def test_postproc_binned_subannual_thick(rootdir): 30 | """ 31 | Test the postproc_binned_subannual_thick CLI script. 32 | """ 33 | simdir = os.path.join(rootdir, 'Output', 'simulations', '01', 'CESM2', 'ssp245', 'binned') 34 | 35 | # Run postproc_monthyl_mass CLI script 36 | subprocess.run(['postproc_binned_subannual_thick', '-simdir', simdir], check=True) 37 | 38 | 39 | def test_postproc_compile_simulations(rootdir): 40 | """ 41 | Test the postproc_compile_simulations CLI script. 42 | """ 43 | 44 | # Run postproc_compile_simulations CLI script 45 | subprocess.run( 46 | [ 47 | 'postproc_compile_simulations', 48 | '-rgi_region01', 49 | '01', 50 | '-option_calibration', 51 | 'MCMC', 52 | '-sim_climate_name', 53 | 'CESM2', 54 | '-sim_climate_scenario', 55 | 'ssp245', 56 | '-sim_startyear', 57 | '2000', 58 | '-sim_endyear', 59 | '2100', 60 | ], 61 | check=True, 62 | ) 63 | 64 | # Check if output files were created 65 | compdir = os.path.join(rootdir, 'Output', 'simulations', 'compile', 'glacier_stats') 66 | output_files = glob.glob(os.path.join(compdir, '**', '*.nc'), recursive=True) 67 | assert output_files, f'No output files found in {compdir}' 68 | 69 | 70 | def test_check_compiled_product(rootdir): 71 | """ 72 | Verify the contents of the files created by postproc_compile_simulations. 73 | """ 74 | # skip variables that are not in the compiled products 75 | vars_to_skip = [ 76 | 'glac_temp', 77 | 'glac_mass_change_ignored_annual', 78 | 'offglac_prec', 79 | 'offglac_refreeze', 80 | 'offglac_melt', 81 | 'offglac_snowpack', 82 | ] 83 | 84 | simpath = os.path.join( 85 | rootdir, 86 | 'Output', 87 | 'simulations', 88 | '01', 89 | 'CESM2', 90 | 'ssp245', 91 | 'stats', 92 | '1.03622_CESM2_ssp245_MCMC_ba1_10sets_2000_2100_all.nc', 93 | ) 94 | compdir = os.path.join(rootdir, 'Output', 'simulations', 'compile', 'glacier_stats') 95 | 96 | with xr.open_dataset(simpath) as simds: 97 | # loop through vars 98 | vars_to_check = [name for name, var in simds.variables.items() if len(var.dims) > 1] 99 | vars_to_check = [item for item in vars_to_check if item not in vars_to_skip] 100 | 101 | for var in vars_to_check: 102 | # skip mad 103 | if 'mad' in var: 104 | continue 105 | simvar = simds[var] 106 | comppath = os.path.join(compdir, var, '01') 107 | comppath = glob.glob(f'{comppath}/R01_{var}*.nc')[0] 108 | assert os.path.isfile(comppath), f'Compiled product not found for {var} at {comppath}' 109 | with xr.open_dataset(comppath) as compds: 110 | compvar = compds[var] 111 | 112 | # verify coords (compiled product has one more dimension for the `model`) 113 | assert compvar.ndim == simvar.ndim + 1 114 | 115 | # pull data values 116 | simvals = simvar.values 117 | compvals = compvar.values[0, :, :] # 0th index is the glacier index 118 | 119 | # check that compiled product has same shape as original data 120 | assert simvals.shape == compvals.shape, ( 121 | f'Compiled product shape {compvals.shape} does not match original data shape {simvals.shape}' 122 | ) 123 | # check that compiled product matches original data 124 | assert np.allclose(simvals, compvals, rtol=1e-8, atol=1e-12, equal_nan=True), ( 125 | f'Compiled product for {var} does not match original data' 126 | ) 127 | -------------------------------------------------------------------------------- /pygem/utils/stats.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2018 David Rounce 5 | 6 | Distributed under the MIT license 7 | 8 | Model statistics module 9 | """ 10 | 11 | import arviz as az 12 | import numpy as np 13 | 14 | 15 | def effective_n(x): 16 | """ 17 | Compute the effective sample size of a trace. 18 | 19 | Takes the trace and computes the effective sample size 20 | according to its detrended autocorrelation. 21 | 22 | Parameters 23 | ---------- 24 | x : list or array of chain samples 25 | 26 | Returns 27 | ------- 28 | effective_n : int 29 | effective sample size 30 | """ 31 | if len(set(x)) == 1: 32 | return 1 33 | try: 34 | # detrend trace using mean to be consistent with statistics 35 | # definition of autocorrelation 36 | x = np.asarray(x) 37 | x = x - x.mean() 38 | # compute autocorrelation (note: only need second half since 39 | # they are symmetric) 40 | rho = np.correlate(x, x, mode='full') 41 | rho = rho[len(rho) // 2 :] 42 | # normalize the autocorrelation values 43 | # note: rho[0] is the variance * n_samples, so this is consistent 44 | # with the statistics definition of autocorrelation on wikipedia 45 | # (dividing by n_samples gives you the expected value). 46 | rho_norm = rho / rho[0] 47 | # Iterate until sum of consecutive estimates of autocorrelation is 48 | # negative to avoid issues with the sum being -0.5, which returns an 49 | # effective_n of infinity 50 | negative_autocorr = False 51 | t = 1 52 | n = len(x) 53 | while not negative_autocorr and (t < n): 54 | if not t % 2: 55 | negative_autocorr = sum(rho_norm[t - 1 : t + 1]) < 0 56 | t += 1 57 | return int(n / (1 + 2 * rho_norm[1:t].sum())) 58 | except: 59 | return None 60 | 61 | 62 | def mcmc_stats( 63 | chains_dict, 64 | params=['tbias', 'kp', 'ddfsnow', 'ddfice', 'rhoabl', 'rhoacc', 'mb_mwea'], 65 | ): 66 | """ 67 | Compute per-chain and overall summary stats for MCMC samples. 68 | 69 | Parameters 70 | ---------- 71 | chains_dict : dict 72 | Dictionary with structure: 73 | { 74 | "param1": { 75 | "chain1": [...], 76 | "chain2": [...], 77 | ... 78 | }, 79 | ... 80 | } 81 | 82 | Returns 83 | ------- 84 | summary_stats : dict 85 | Dictionary with structure: 86 | { 87 | "param1": { 88 | "mean": [...], # per chain 89 | "std": [...], 90 | "median": [...], 91 | "q025": [...], 92 | "q975": [...], 93 | "ess": ..., # overall 94 | "r_hat": ... # overall 95 | }, 96 | ... 97 | } 98 | """ 99 | summary_stats = {} 100 | 101 | for param, chains in chains_dict.items(): 102 | if param not in params: 103 | continue 104 | 105 | # Stack chains into array: shape (n_chains, n_samples) 106 | chain_names = sorted(chains) # ensure consistent order 107 | samples = np.array([chains[c] for c in chain_names]) 108 | 109 | # Per-chain stats 110 | means = np.mean(samples, axis=1).tolist() 111 | stds = np.std(samples, axis=1, ddof=1).tolist() 112 | medians = np.median(samples, axis=1).tolist() 113 | q25 = np.quantile(samples, 0.25, axis=1).tolist() 114 | q75 = np.quantile(samples, 0.75, axis=1).tolist() 115 | ess = [effective_n(x) for x in samples] 116 | # Overall stats (R-hat) 117 | if samples.shape[0] > 1: 118 | # calculate the gelman-rubin stat for each variable across all chains 119 | # pass chains as 2d array to arviz using the from_dict() method 120 | # convert the chains into an InferenceData object 121 | idata = az.from_dict(posterior={param: samples}) 122 | # calculate the Gelman-Rubin statistic (rhat) 123 | r_hat = float(az.rhat(idata).to_array().values[0]) 124 | else: 125 | r_hat = None 126 | 127 | summary_stats[param] = { 128 | 'mean': means, 129 | 'std': stds, 130 | 'median': medians, 131 | 'q25': q25, 132 | 'q75': q75, 133 | 'ess': ess, 134 | 'r_hat': r_hat, 135 | } 136 | 137 | chains_dict['_summary_stats_'] = summary_stats 138 | 139 | return chains_dict 140 | -------------------------------------------------------------------------------- /pygem/bin/op/initialize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2024 Brandon Tober David Rounce 5 | 6 | Distributed under the MIT license 7 | 8 | initialization script (ensure config.yaml and get sample datasets) 9 | """ 10 | 11 | import os 12 | import shutil 13 | import zipfile 14 | 15 | import requests 16 | 17 | from pygem.setup.config import ConfigManager 18 | 19 | # instantiate ConfigManager - store new config.yaml file 20 | config_manager = ConfigManager(overwrite=True) 21 | 22 | 23 | def print_file_tree(start_path, indent=''): 24 | # Loop through all files and directories in the current directory 25 | for item in os.listdir(start_path): 26 | path = os.path.join(start_path, item) 27 | 28 | # Print the current item with indentation 29 | print(indent + '|-- ' + item) 30 | 31 | # Recursively call this function if the item is a directory 32 | if os.path.isdir(path): 33 | print_file_tree(path, indent + ' ') 34 | 35 | 36 | def get_confirm_token(response): 37 | """Extract confirmation token for Google Drive large file download.""" 38 | for key, value in response.cookies.items(): 39 | if key.startswith('download_warning'): 40 | return value 41 | return None 42 | 43 | 44 | def save_response_content(response, destination): 45 | """Save the response content to a file.""" 46 | chunk_size = 32768 47 | with open(destination, 'wb') as file: 48 | for chunk in response.iter_content(chunk_size): 49 | if chunk: # Filter out keep-alive chunks 50 | file.write(chunk) 51 | 52 | 53 | def get_unique_folder_name(dir): 54 | """Generate a unique folder name by appending a suffix if the folder already exists.""" 55 | counter = 1 56 | unique_dir = dir 57 | while os.path.exists(unique_dir): 58 | unique_dir = f'{dir}_{counter}' 59 | counter += 1 60 | return unique_dir 61 | 62 | 63 | def download_and_unzip_from_google_drive(file_id, output_dir): 64 | """ 65 | Download a ZIP file from Google Drive and extract its contents. 66 | 67 | Args: 68 | file_id (str): The Google Drive file ID. 69 | output_dir (str): The directory to save and extract the contents of the ZIP file. 70 | 71 | Returns: 72 | int: 1 if the ZIP file was successfully downloaded and extracted, 0 otherwise. 73 | """ 74 | # Google Drive URL template 75 | base_url = 'https://drive.google.com/uc?export=download' 76 | 77 | # Make sure the output directory exists 78 | os.makedirs(output_dir, exist_ok=True) 79 | 80 | # Path to save the downloaded file 81 | zip_path = os.path.join(output_dir, 'tmp_download.zip') 82 | 83 | try: 84 | # Start the download process 85 | with requests.Session() as session: 86 | response = session.get(base_url, params={'id': file_id}, stream=True) 87 | token = get_confirm_token(response) 88 | if token: 89 | response = session.get(base_url, params={'id': file_id, 'confirm': token}, stream=True) 90 | save_response_content(response, zip_path) 91 | 92 | # Unzip the file 93 | tmppath = os.path.join(output_dir, 'tmp') 94 | with zipfile.ZipFile(zip_path, 'r') as zip_ref: 95 | zip_ref.extractall(tmppath) 96 | 97 | # get root dir name of zipped files 98 | dir = [item for item in os.listdir(tmppath) if os.path.isdir(os.path.join(tmppath, item))][0] 99 | unzip_dir = os.path.join(tmppath, dir) 100 | # get unique name if root dir name already exists in output_dir 101 | output_dir = get_unique_folder_name(os.path.join(output_dir, dir)) 102 | # move data and cleanup 103 | shutil.move(unzip_dir, output_dir) 104 | shutil.rmtree(tmppath) 105 | os.remove(zip_path) 106 | return output_dir # Success 107 | 108 | except (requests.RequestException, zipfile.BadZipFile, Exception) as e: 109 | return None # Failure 110 | 111 | 112 | def main(): 113 | # Define the base directory 114 | basedir = os.path.join(os.path.expanduser('~'), 'PyGEM') 115 | # Google Drive file id for sample dataset 116 | file_id = '1BCkhuX37CZ5Iz4wBFq4Oy06lf57WGMLE' 117 | # download and unzip 118 | out = download_and_unzip_from_google_drive(file_id, basedir) 119 | 120 | if out: 121 | print('Downloaded PyGEM sample dataset:') 122 | print(os.path.abspath(out)) 123 | try: 124 | print_file_tree(out) 125 | except: 126 | pass 127 | 128 | else: 129 | print('Error downloading PyGEM sample dataset.') 130 | 131 | # update root path in config.yaml 132 | try: 133 | config_manager.update_config(updates={'root': f'{out}/sample_data'}) 134 | except: 135 | pass 136 | 137 | 138 | if __name__ == '__main__': 139 | main() 140 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | (contributing_pygem_target)= 2 | # PyGEM Contribution Guide 3 | 4 | Before contributing to PyGEM, it is recommended that you either clone [PyGEM's GitHub repository](https://github.com/PyGEM-Community/PyGEM) directly, or initiate your own fork (as described [here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo)) to then clone. 5 | 6 | If PyGEM was already installed in your conda environment (as outlined [here](install_pygem_target)), it is recommended that you first uninstall: 7 | ``` 8 | pip uninstall pygem 9 | ``` 10 | 11 | Next, clone PyGEM. This will place the code at your current directory, so you may wish to navigate to a desired location in your terminal before cloning: 12 | ``` 13 | git clone https://github.com/PyGEM-Community/PyGEM.git 14 | ``` 15 | If you opted to create your own fork, clone using appropriate repo URL: `git clone https://github.com//PyGEM.git` 16 | 17 | Navigate to root project directory: 18 | ``` 19 | cd PyGEM 20 | ``` 21 | 22 | Install PyGEM in 'editable' mode: 23 | ``` 24 | pip install -e . 25 | ``` 26 | 27 | Installing a package in editable mode creates a symbolic link to your source code directory (*/path/to/your/PyGEM/clone*), rather than copying the package files into the site-packages directory. This allows you to modify the package code without reinstalling it.
28 | 29 | ## General 30 | - The `dev` branch is the repository's working branch and should almost always be the base branch for Pull Requests (PRs). Exceptions include hotfixes that need to be pushed to the `master` branch immediately, or updates to the `README`. 31 | - Do not push to other people's branches. Instead create a new branch and open a PR that merges your new branch into the branch you want to modify. 32 | 33 | ## Issues 34 | - Check whether an issue describing your problem already exists [here](https://github.com/PyGEM-Community/PyGEM/issues). 35 | - Keep issues simple: try to describe only one problem per issue. Open multiple issues or sub-issues when appropriate. 36 | - Label the issue with the appropriate label (e.g., bug, documentation, etc.). 37 | - If you start working on an issue, assign it to yourself. There is no need to ask for permission unless someone is already assigned to it. 38 | 39 | ## Pull requests (PRs) 40 | - PRs should be submitted [here](https://github.com/PyGEM-Community/PyGEM/pulls). 41 | - PRs should be linked to issues they address (unless it's a minor fix that doesn't warrant a new issue). Think of Issues like a ticketing system. 42 | - PRs should generally address only one issue. This helps PRs stay shorter, which in turn makes the review process easier. 43 | - Concisely describe what your PR does. Avoid repeating what was already said in the issue. 44 | - Assign the PR to yourself. 45 | - First, open a Draft PR. Then consider: 46 | - Have you finished making changes? 47 | - Have you added tests for all new functionalities you introduced? 48 | - Have you run the ruff linter and formatter? See [the linting and formatting section below](ruff_target) on how to do that. 49 | - Have all tests passed in the CI? (Check the progress in the Checks tab of the PR.) 50 | 51 | If the answer to all of the above is "yes", mark the PR as "Ready for review" and request a review from an appropriate reviewer. If in doubt of which reviewer to assign, assign [drounce](https://github.com/drounce). 52 | - You will not be able to merge into `master` and `dev` branches without a reviewer's approval. 53 | 54 | ### Reviewing PRs and responding to a review 55 | - Reviewers should leave comments on appropriate lines. Then: 56 | - The original author of the PR should address all comments, specifying what was done and in which commit. For example, a short response like "Fixed in [link to commit]." is often sufficient. 57 | - After responding to a reviewer's comment, do not mark it as resolved. 58 | - Once all comments are addressed, request a new review from the same reviewer. The reviewer should then resolve the comments they are satisfied with. 59 | - After approving someone else's PR, do not merge it. Let the original author of the PR merge it when they are ready, as they might notice necessary last-minute changes. 60 | 61 | (ruff_target)= 62 | ## Code linting and formatting 63 | PyGEM **requires** all code to be linted and formatted using [ruff](https://docs.astral.sh/ruff/formatter). Ruff enforces a consistent coding style (based on [Black](https://black.readthedocs.io/en/stable/the_black_code_style/index.html)) and helps prevent potential errors, stylistic issues, or deviations from coding standards. The configuration for Ruff can be found in the `pyproject.toml` file. 64 | 65 | ⚠️ **Both linting and formatting must be completed before code is merged.** These checks are run automatically in the CI pipeline. If any issues are detected, the pipeline will fail. 66 | 67 | ### Lint the codebase 68 | To lint the codebase using Ruff, run the following command: 69 | ``` 70 | ruff check /path/to/code 71 | ``` 72 | Please address all reported errors. Many errors may be automatically and safely fixed by passing `--fix` to the above command. Other errors will need to be manually addressed. 73 | 74 | ### Format the codebase 75 | To automatically format the codebase using Ruff, run the following command: 76 | ``` 77 | ruff format /path/to/code 78 | ``` 79 | -------------------------------------------------------------------------------- /docs/dynamics_massredistributioncurves.md: -------------------------------------------------------------------------------- 1 | (mass_redistribution_curves_target)= 2 | ## Mass Redistribution Curves 3 | The mass redistribution curves in PyGEM follow those developed by [Huss and Hock (2015)](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full) based on [Huss et al. (2010)](https://hess.copernicus.org/articles/14/815/2010/hess-14-815-2010.html) but explicitly solve for area and ice thickness changes simultaneously to conserve mass. The approach is only applied to glaciers that have at least three elevation bins. Each year the glacier-wide mass balance is computed (see Mass Balance Section) and the mass is redistributed over the glacier using empirical equations that set the normalized surface elevation change ($\Delta h$) as a function of the glacier’s elevation bins: 4 | ```{math} 5 | \Delta h = (h_{n} + a_{HH2015})^{\gamma} + b_{HH2015} \cdot (h_{n} + a_{HH2015}) + c_{HH2015} 6 | ``` 7 | where $h_{n}$ is the normalized elevation according to $\frac{z_{max} - z_{bin}}{z_{max} - z_{min}}$ and $a_{HH2015)$, $b_{HH2015)$, $c_{HH2015)$, and $\gamma$ are all calibrated coefficients based on 34 glaciers in the Swiss Alps. These coefficients vary depending on the size of the glacier ([Huss et al. (2010)](https://hess.copernicus.org/articles/14/815/2010/hess-14-815-2010.html)). In order to ensure that mass is conserved, i.e., the integration of the elevation change and glacier area (A) of each bin over all the elevation bins ($nbins$) is equal to the glacier-wide volume change ($\Delta V$), an ice thickness scaling factor ($f_{s,HH2015}$) must be computed: 8 | ```{math} 9 | f_{s,HH2015} = \frac{\Delta V}{\sum_{i=0}^{nbins} A_{i} \cdot \Delta h_{i}} 10 | ``` 11 | The volume change in each elevation bin ($\Delta V_{bin}$) is computed as: 12 | ```{math} 13 | \Delta V_{bin} = f_{s,HH2015} \cdot \Delta h_{bin} \cdot A_{bin} 14 | ``` 15 | Depending on the bed shape (parabolic, triangular or rectangular) of the glacier, the resulting area, ice thickness ($H$), and width ($W$) can be solved for explicitly based on mass conservation and the use of similar shapes: 16 | ```{math} 17 | H_{bin,t+1} \cdot A_{bin,t+1} = H_{bin,t} \cdot A_{bin,t} + \Delta V_{bin} 18 | ``` 19 | ```{math} 20 | \frac{H_{bin,t+1}}{H_{bin,t}} \alpha \frac{A_{bin,t+1}}{A_bin,t} 21 | ``` 22 | This is a marked improvement over previous studies that have not explicitly solved for the area and ice thickness changes simultaneously, which can lead to mass loss or gain that is then corrected ([Huss and Hock, 2015](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full)). 23 | 24 | ### Glacier retreat 25 | Glacier retreat occurs when the volume change in an elevation bin ($\Delta V_{bin}$) causes the ice thickness for the next time step to be less than zero. In this case, the ice thickness is set to zero and the remaining volume change is redistributed over the entire glacier according to the mass redistribution described above. 26 | 27 | ### Glacier advance 28 | Glacier advance occurs when the ice thickness change exceeds the ice thickness advance threshold (default: 5 m; [Huss and Hock, 2015](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full)). When this occurs, the ice thickness change is set to 5 m, the area and width of the bin are calculated accordingly, and the excess volume is recorded. The model then calculates the average area and thickness associated with the bins located in the glacier’s terminus, which is defined by the terminus percentage (default: 20%). However, this calculation excludes the bin actually located at the terminus because prior to adding a new elevation bin, the model checks that the bin located at the terminus is “full”. Specifically, the area and ice thickness of the lowermost bin are compared to the terminus’ average and if the area and ice thickness is less than the average, then the lowermost bin is first filled until it reaches the terminus average. This ensures that the lowermost bin is “full” and prevents adding new bins to a glacier that may only have a relatively small excess volume in consecutive years. In other words, if this criterion did not exist, then it would be possible to add new bins over multiple years that had small areas, which would appear as though the glacier was moving down a steep slope. 29 | 30 | If there is still excess volume remaining after filling the lowermost bin to the terminus average, then a new bin is added below the terminus. The ice thickness in this new bin is set to be equal to the terminus average and the area is computed based on the excess volume. If the area of this bin would be greater than the average area of the terminus, this indicates that an additional bin needs to be added. However, prior to adding an additional bin the excess volume is redistributed over the glacier again. This allows the glacier’s area and thickness to increase and prevents the glacier from having a thin layer of ice that advances down-valley without thickening. 31 | 32 | There are two exceptions for when a glacier is not allowed to advance to a particular bin. The first exception is if the added bin would be below sea-level, in which case the remaining excess volume is redistributed over the entire glacier. The second exception is if the bin is over a known discontinuous section of the glacier, which is determined based on the initial glacier area. For example, it is possible, albeit unlikely, that a glacier could retreat over a discontinuous section of a glacier and then advance in the future. This discontinuous area is assumed to be a steep vertical drop, hence why a glacier currently does not exist, so a glacier is not allowed to form there in the future. The glacier instead skips over this discontinuous bin and a new bin is added below it. -------------------------------------------------------------------------------- /pygem/tests/test_02_config.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import pytest 4 | import yaml 5 | 6 | from pygem.setup.config import ConfigManager 7 | 8 | 9 | class TestConfigManager: 10 | """Tests for the ConfigManager class.""" 11 | 12 | @pytest.fixture(autouse=True) 13 | def setup(self, tmp_path): 14 | """Setup a ConfigManager instance for each test.""" 15 | self.config_manager = ConfigManager( 16 | config_filename='config.yaml', base_dir=tmp_path, overwrite=True, check_paths=False 17 | ) 18 | 19 | def test_config_created(self, tmp_path): 20 | config_path = pathlib.Path(tmp_path) / 'config.yaml' 21 | assert config_path.is_file() 22 | 23 | def test_read_config(self): 24 | config = self.config_manager.read_config() 25 | assert isinstance(config, dict) 26 | assert 'sim' in config 27 | assert 'nsims' in config['sim'] 28 | 29 | def test_update_config_unrecognized_key_error(self): 30 | """Test that a KeyError is raised when updating a value with an unrecognized key.""" 31 | with pytest.raises(KeyError, match='Unrecognized configuration key: invalid_key'): 32 | self.config_manager.update_config({'invalid_key': None}) 33 | 34 | @pytest.mark.parametrize( 35 | 'key, invalid_value, expected_type, invalid_type', 36 | [ 37 | ('sim.nsims', [1, 2, 3], 'int', 'list'), 38 | ('calib.HH2015_params.kp_init', '0.5', 'float', 'str'), 39 | ('setup.include_landterm', -999, 'bool', 'int'), 40 | ('rgi.rgi_cols_drop', 'not-a-list', 'list', 'str'), 41 | ], 42 | ) 43 | def test_update_config_type_error(self, key, invalid_value, expected_type, invalid_type): 44 | """ 45 | Test that a TypeError is raised when updating a value with a new value of a 46 | wrong type. 47 | """ 48 | with pytest.raises( 49 | TypeError, 50 | match=f"Invalid type for '{key.replace('.', '\\.')}': expected.*{expected_type}.*, not.*{invalid_type}.*", 51 | ): 52 | self.config_manager.update_config({key: invalid_value}) 53 | 54 | def test_update_config_list_element_type_error(self): 55 | """ 56 | Test that a TypeError is raised when updating a value with a new list value 57 | containing elements of a different type than expected. 58 | """ 59 | key = 'rgi.rgi_cols_drop' 60 | invalid_value = ['a', 'b', 100] 61 | expected_type = 'str' 62 | 63 | with pytest.raises( 64 | TypeError, 65 | match=f"Invalid type for elements in '{key.replace('.', '\\.')}':" 66 | f' expected all elements to be .*{expected_type}.*, but got.*{invalid_value}.*', 67 | ): 68 | self.config_manager.update_config({key: invalid_value}) 69 | 70 | def test_compare_with_source(self): 71 | """Test that compare_with_source detects missing keys.""" 72 | # Remove a key from the config file 73 | with open(self.config_manager.config_path, 'r') as f: 74 | config = yaml.safe_load(f) 75 | del config['sim']['nsims'] 76 | with open(self.config_manager.config_path, 'w') as f: 77 | yaml.dump(config, f) 78 | 79 | with pytest.raises(KeyError, match=r'Missing required key in configuration: sim\.nsims'): 80 | self.config_manager.read_config(validate=True) 81 | 82 | def test_update_config(self): 83 | """Test that update_config updates the config file for all data types.""" 84 | updates = { 85 | 'sim.nsims': 5, # int 86 | 'calib.HH2015_params.kp_init': 0.5, # float 87 | 'user.email': 'updated@example.com', # str 88 | 'setup.include_landterm': False, # bool 89 | 'rgi.rgi_cols_drop': ['Item1', 'Item2'], # list 90 | } 91 | 92 | # Values before updating 93 | config = self.config_manager.read_config() 94 | assert config['sim']['nsims'] == 1 95 | assert config['calib']['HH2015_params']['kp_init'] == 1.5 96 | assert config['user']['email'] == 'drounce@cmu.edu' 97 | assert config['setup']['include_landterm'] == True 98 | assert config['rgi']['rgi_cols_drop'] == [ 99 | 'GLIMSId', 100 | 'BgnDate', 101 | 'EndDate', 102 | 'Status', 103 | 'Linkages', 104 | 'Name', 105 | ] 106 | 107 | self.config_manager.update_config(updates) 108 | config = self.config_manager.read_config() 109 | 110 | # Values after updating 111 | assert config['sim']['nsims'] == 5 112 | assert config['calib']['HH2015_params']['kp_init'] == 0.5 113 | assert config['setup']['include_landterm'] == False 114 | assert config['user']['email'] == 'updated@example.com' 115 | assert config['rgi']['rgi_cols_drop'] == ['Item1', 'Item2'] 116 | 117 | def test_update_config_dict(self): 118 | """Test that update_config updates the config file for nested dictionaries.""" 119 | # Values before updating 120 | config = self.config_manager.read_config() 121 | assert config['user']['name'] == 'David Rounce' 122 | assert config['user']['email'] == 'drounce@cmu.edu' 123 | assert config['user']['institution'] == 'Carnegie Mellon University, Pittsburgh PA' 124 | 125 | updates = { 126 | 'user': { 127 | 'name': 'New Name', 128 | 'email': 'New email', 129 | 'institution': 'New Institution', 130 | } 131 | } 132 | self.config_manager.update_config(updates) 133 | 134 | # Values after updating 135 | config = self.config_manager.read_config() 136 | assert config['user']['name'] == 'New Name' 137 | assert config['user']['email'] == 'New email' 138 | assert config['user']['institution'] == 'New Institution' 139 | -------------------------------------------------------------------------------- /pygem/shop/icethickness.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2018 David Rounce 5 | 6 | Distributed under the MIT license 7 | """ 8 | 9 | import logging 10 | import os 11 | import pickle 12 | 13 | import numpy as np 14 | import rasterio 15 | import xarray as xr 16 | from oggm import cfg 17 | from oggm.core.gis import rasterio_to_gdir 18 | from oggm.utils import entity_task, ncDataset 19 | 20 | # pygem imports 21 | from pygem.setup.config import ConfigManager 22 | 23 | # instantiate ConfigManager 24 | config_manager = ConfigManager() 25 | # read the config 26 | pygem_prms = config_manager.read_config() 27 | 28 | if 'consensus_mass' not in cfg.BASENAMES: 29 | cfg.BASENAMES['consensus_mass'] = ( 30 | 'consensus_mass.pkl', 31 | 'Glacier mass from consensus ice thickness data', 32 | ) 33 | if 'consensus_h' not in cfg.BASENAMES: 34 | cfg.BASENAMES['consensus_h'] = ( 35 | 'consensus_h.tif', 36 | 'Raster of consensus ice thickness data', 37 | ) 38 | 39 | # Module logger 40 | log = logging.getLogger(__name__) 41 | 42 | 43 | @entity_task(log, writes=['consensus_mass']) 44 | def consensus_gridded( 45 | gdir, 46 | h_consensus_fp=f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["icethickness"]["h_ref_relpath"]}', 47 | add_mass=True, 48 | add_to_gridded=True, 49 | ): 50 | """Bin consensus ice thickness and add total glacier mass to the given glacier directory 51 | 52 | Updates the 'inversion_flowlines' save file and creates new consensus_mass.pkl 53 | 54 | Parameters 55 | ---------- 56 | gdir : :py:class:`oggm.GlacierDirectory` 57 | where to write the data 58 | """ 59 | # If binned mb data exists, then write to glacier directory 60 | h_fn = h_consensus_fp + 'RGI60-' + gdir.rgi_region + '/' + gdir.rgi_id + '_thickness.tif' 61 | assert os.path.exists(h_fn), 'Error: h_consensus_fullfn for ' + gdir.rgi_id + ' does not exist.' 62 | 63 | # open consensus ice thickness estimate 64 | h_dr = rasterio.open(h_fn, 'r', driver='GTiff') 65 | h = h_dr.read(1).astype(rasterio.float32) 66 | 67 | # Glacier mass [kg] 68 | glacier_mass_raw = (h * h_dr.res[0] * h_dr.res[1]).sum() * pygem_prms['constants']['density_ice'] 69 | # print(glacier_mass_raw) 70 | 71 | if add_mass: 72 | # Pickle data 73 | consensus_fn = gdir.get_filepath('consensus_mass') 74 | with open(consensus_fn, 'wb') as f: 75 | pickle.dump(glacier_mass_raw, f) 76 | 77 | if add_to_gridded: 78 | rasterio_to_gdir(gdir, h_fn, 'consensus_h', resampling='bilinear') 79 | output_fn = gdir.get_filepath('consensus_h') 80 | # append the debris data to the gridded dataset 81 | with rasterio.open(output_fn) as src: 82 | grids_file = gdir.get_filepath('gridded_data') 83 | with ncDataset(grids_file, 'a') as nc: 84 | # Mask values 85 | glacier_mask = nc['glacier_mask'][:] 86 | data = src.read(1) * glacier_mask 87 | # Pixel area 88 | pixel_m2 = abs(gdir.grid.dx * gdir.grid.dy) 89 | # Glacier mass [kg] reprojoected (may lose or gain mass depending on resampling algorithm) 90 | glacier_mass_reprojected = (data * pixel_m2).sum() * pygem_prms['constants']['density_ice'] 91 | # Scale data to ensure conservation of mass during reprojection 92 | data_scaled = data * glacier_mass_raw / glacier_mass_reprojected 93 | # glacier_mass = (data_scaled * pixel_m2).sum() * pygem_prms['constants']['density_ice'] 94 | # print(glacier_mass) 95 | 96 | # Write data 97 | vn = 'consensus_h' 98 | if vn in nc.variables: 99 | v = nc.variables[vn] 100 | else: 101 | v = nc.createVariable( 102 | vn, 103 | 'f8', 104 | ( 105 | 'y', 106 | 'x', 107 | ), 108 | zlib=True, 109 | ) 110 | v.units = 'm' 111 | v.long_name = 'Consensus ice thicknness' 112 | v[:] = data_scaled 113 | 114 | 115 | @entity_task(log, writes=['inversion_flowlines']) 116 | def consensus_binned(gdir): 117 | """Bin consensus ice thickness ice estimates. 118 | 119 | Updates the 'inversion_flowlines' save file. 120 | 121 | Parameters 122 | ---------- 123 | gdir : :py:class:`oggm.GlacierDirectory` 124 | where to write the data 125 | """ 126 | flowlines = gdir.read_pickle('inversion_flowlines') 127 | fl = flowlines[0] 128 | 129 | assert len(flowlines) == 1, 'Error: binning debris data set up only for single flowlines at present' 130 | 131 | # Add binned debris thickness and enhancement factors to flowlines 132 | ds = xr.open_dataset(gdir.get_filepath('gridded_data')) 133 | glacier_mask = ds['glacier_mask'].values 134 | topo = ds['topo_smoothed'].values 135 | h = ds['consensus_h'].values 136 | 137 | # Only bin on-glacier values 138 | idx_glac = np.where(glacier_mask == 1) 139 | topo_onglac = topo[idx_glac] 140 | h_onglac = h[idx_glac] 141 | 142 | # Bin edges 143 | nbins = len(fl.dis_on_line) 144 | z_center = (fl.surface_h[0:-1] + fl.surface_h[1:]) / 2 145 | z_bin_edges = np.concatenate( 146 | ( 147 | np.array([topo[idx_glac].max() + 1]), 148 | z_center, 149 | np.array([topo[idx_glac].min() - 1]), 150 | ) 151 | ) 152 | # Loop over bins and calculate the mean debris thickness and enhancement factor for each bin 153 | h_binned = np.zeros(nbins) 154 | for nbin in np.arange(0, len(z_bin_edges) - 1): 155 | bin_max = z_bin_edges[nbin] 156 | bin_min = z_bin_edges[nbin + 1] 157 | bin_idx = np.where((topo_onglac < bin_max) & (topo_onglac >= bin_min)) 158 | try: 159 | h_binned[nbin] = h_onglac[bin_idx].mean() 160 | except: 161 | h_binned[nbin] = 0 162 | 163 | fl.consensus_h = h_binned 164 | 165 | # Overwrite pickle 166 | gdir.write_pickle(flowlines, 'inversion_flowlines') 167 | -------------------------------------------------------------------------------- /docs/faqs.md: -------------------------------------------------------------------------------- 1 | # FAQs 2 | ### Why does my simulation from run_simulation.py script run smoothly without any error, but the only output is an error folder with a failed information .txt file? 3 | The run_simulation.py script uses try/except statements to support large-scale applications, i.e., if a simulation for a single glacier fails (e.g., because it grows beyond the maximum size), we don’t want this error to stop all of the simulations. Hence, this error is caught in the try/except statement and the except statement generates a failed .txt file to let the individual know that the simulation did not run. 4 | 5 | Troubleshooting this failure needs to be improved now that there are many new users who will likely cause this to fail more frequently. At present, the best workaround is to replace the try/except statement. Specifically, there will be a commented line named “for batman in [0]:”, which I suggest you uncomment, and comment out the try statement above. You then need to go to towards the end of the statement and uncomment “print(‘\nADD BACK IN EXCEPTION\n\n’)” and comment out the except statement that is about 8 lines of code. When you run the simulation now, you should get whatever runtime error was causing the failure to begin with. 6 | 7 | In the future, we will seek to catch these errors and put them in the text file to make debugging easier. 8 | 9 | 10 | ### Why is the mass balance that I calculate from the calibration not the same as the simulation? 11 | There are three potential causes for this, which are all dependent on the calibration options: 12 | 1. The calibration is performed assuming the glacier area is constant. This is primarily to save computational time, but also enables the calibration to not be linked to a specific glacier dynamics option. Tests were performed in Fall 2020 that showed the impacts of this over the calibration period (2000-2019) was fairly minor. If you run the model with a dynamical option, then you will get a different mass balance. 13 | 2. Did you use the emulator? The emulator is a statistical representation of the mass balance. Tests were performed in Fall 2020 that showed the emulator performed quite well (typically within 0.01 - 0.02 mwea of the observed mass balance), which was considered acceptable given the uncertainty associated with the geodetic mass balance data. Hence, the mass balance you get from the emulator will be slightly different than one that you get from running a simulation. 14 | 3. Did you use the MCMC option? The MCMC calibration is performed using a certain number of steps. The simulations are performed for a subset of model parameter combinations from those steps; hence, they will differ. 15 | 16 | 17 | ### How can I export a different variable (e.g., binned glacier runoff)? 18 | There are two primary steps: (1) calculate the new variable you want to export, and (2) add the variable to the proper dataset. We recommend the following for this example: 19 | 20 | Calculate the binned glacier runoff. You'll see in the massbalance.py script that it automatically adds the glac_bin_melt, glac_bin_refreeze, and bin_prec. You'll need to create a new variable for glac_bin_runoff. 21 | Add the binned glacier runoff to be exported. For this you need to add the glac_bin_runoff to the create_xrdataset_binned_stats() function in the run_simulation.py script. This will add the glacier runoff as a binned result to the binned netcdf file that is exported. 22 | 23 | 24 | ### Why am I getting a 'has_internet'=False error? 25 | Part of the beauty of our use of OGGM is access to the OGGM Shop. When we initialize our glacier directories in PyGEM, we are downloading them from OGGM shop. OGGM has a cfg.PARAMS['has_internet'] variable that must be set to True in order to download; otherwise, it will throw this error. If you skipped over running OGGM's test when you downloaded OGGM, you may not have downloaded the sample datasets that OGGM requires to run tests and it'll likely throw an error. To correct this error, you have two options: (1) set has_internet=True in pygem_input.py or (2) manually modify your code to set cfg.PARAMS['has_internet']=True likely somewhere in your oggm_compat.py file, which will be located somewhere in your conda environment if you used pip install. 26 | 27 | 28 | ### The error message and line of code appears to be associated with code from OGGM. How do I troubleshoot OGGM's code from within PyGEM? 29 | While we're doing everything we can to minimize these issues, and OGGM developers are excellent at supporting this as well, from time to time errors may come up based on OGGM. This often occurs during changes between versions as it's challenging to document every tiny change. Nonetheless, here's a guide for troubleshooting your errors to at least identify the problem. We'll use a recent example where for some reason with the new update, we couldn't get tidewater glaciers to invert for ice thickness. Here's what we did to solve the issue: 30 | * Identify the source code associated with the error. 31 | - In our case, we couldn't get the inversion to work, so we knew it was associated with OGGM's core/inversion.py. If you're having trouble with this step, try copying a function from the error message and finding where that function exists in OGGM's source code on github. 32 | * Find where OGGM’s code is installed in your environment on your local computer. 33 | - A good way to do this is to go to your directory and find where "inversion.py" exists. Then open this file and show the enclosing directory. Note that this can be a bit of a pain, but once you know where your conda environments are stored it makes it a lot easier. This is essentially where the packages are stored on your computer. Cool! 34 | * Find where the error is coming from by putting print statements within the inversion.py script. 35 | - This is very basic troubleshooting. There are likely faster ways of doing so, but I prefer to dive into the code. In our case, the model_flowlines wasn’t being generated, so I started going through the find_inversion_calving_from_any_mb fxn. This identified that it was not even getting pass the first if statement, which is a clever thing that OGGM does - OGGM has built-in "off-ramps" within their functions such that if the input structure isn’t ideal (in this case if it’s not a tidewater glacier or if you don’t set cfg.PARAMS[‘use_kcalving_for_inversion’]=True, then it returns out of the function without an error. 36 | * Lastly, come up with a solution! 37 | - This is where our help breaks down a bit as you'll need to figure out what the fix is for your situation. However, if you're able to get to this point, stay at it! If you are trying hard and can't figure out what's going on (perhaps you've tried for a couple hours and are now getting super frustrated - it happens), this is where using the "support" Channel on OGGM's Slack is a great option. In your message, make sure to include all the details above. If it's something with PyGEM, then the PyGEM folks on OGGM's Slack will do our best to help. If it's something with OGGM, then you'll find plenty of support as well. -------------------------------------------------------------------------------- /pygem/bin/op/list_failed_simulations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2018 David Rounce 5 | 6 | Distributed under the MIT license 7 | 8 | script to check for failed glaciers for a given simulation and export a pickle file containing a list of said glacier numbers to be reprocessed 9 | """ 10 | 11 | # imports 12 | import argparse 13 | import glob 14 | import json 15 | import os 16 | import sys 17 | 18 | import numpy as np 19 | 20 | # pygem imports 21 | from pygem.setup.config import ConfigManager 22 | 23 | # instantiate ConfigManager 24 | config_manager = ConfigManager() 25 | # read the config 26 | pygem_prms = config_manager.read_config() 27 | import pygem.pygem_modelsetup as modelsetup 28 | 29 | 30 | def run( 31 | reg, 32 | simpath, 33 | gcm, 34 | sim_climate_scenario, 35 | calib_opt, 36 | bias_adj, 37 | sim_startyear, 38 | sim_endyear, 39 | ): 40 | # define base directory 41 | base_dir = simpath + '/' + str(reg).zfill(2) + '/' 42 | 43 | # get all glaciers in region to see which fraction ran successfully 44 | main_glac_rgi_all = modelsetup.selectglaciersrgitable( 45 | rgi_regionsO1=[reg], 46 | rgi_regionsO2='all', 47 | rgi_glac_number='all', 48 | glac_no=None, 49 | debug=True, 50 | ) 51 | 52 | glacno_list_all = list(main_glac_rgi_all['rgino_str'].values) 53 | 54 | # get list of glacier simulation files 55 | if sim_climate_scenario: 56 | sim_dir = base_dir + gcm + '/' + sim_climate_scenario + '/stats/' 57 | else: 58 | sim_dir = base_dir + gcm + '/stats/' 59 | 60 | # check if gcm has given sim_climate_scenario 61 | assert os.path.isdir(sim_dir), f'Error: simulation path not found, {sim_dir}' 62 | 63 | # instantiate list of galcnos that are not in sim_dir 64 | failed_glacnos = [] 65 | 66 | fps = glob.glob(sim_dir + f'*_{calib_opt}_ba{bias_adj}_*_{sim_startyear}_{sim_endyear}_all.nc') 67 | 68 | # Glaciers with successful runs to process 69 | glacno_ran = [x.split('/')[-1].split('_')[0] for x in fps] 70 | glacno_ran = [x.split('.')[0].zfill(2) + '.' + x[-5:] for x in glacno_ran] 71 | 72 | # print stats of successfully simualated glaciers 73 | main_glac_rgi = main_glac_rgi_all.loc[main_glac_rgi_all.apply(lambda x: x.rgino_str in glacno_ran, axis=1)] 74 | print( 75 | f'{gcm} {str(sim_climate_scenario).replace("None", "")} glaciers successfully simulated:\n - {main_glac_rgi.shape[0]} of {main_glac_rgi_all.shape[0]} glaciers ({np.round(main_glac_rgi.shape[0] / main_glac_rgi_all.shape[0] * 100, 3)}%)' 76 | ) 77 | print( 78 | f' - {np.round(main_glac_rgi.Area.sum(), 0)} km2 of {np.round(main_glac_rgi_all.Area.sum(), 0)} km2 ({np.round(main_glac_rgi.Area.sum() / main_glac_rgi_all.Area.sum() * 100, 3)}%)' 79 | ) 80 | 81 | glacno_ran = ['{0:0.5f}'.format(float(x)) for x in glacno_ran] 82 | 83 | # loop through each glacier in batch list 84 | for i, glacno in enumerate(glacno_list_all): 85 | # gat glacier string and file name 86 | glacier_str = '{0:0.5f}'.format(float(glacno)) 87 | 88 | if glacier_str not in glacno_ran: 89 | failed_glacnos.append(glacier_str) 90 | return failed_glacnos 91 | 92 | 93 | def main(): 94 | # Set up CLI 95 | parser = argparse.ArgumentParser( 96 | description="""description: script to check for failed PyGEM glacier simulations\n\nexample call: $python list_failed_simulations.py -rgi_region01=1 -sim_climate_name=CanESM5 -scenrio=ssp585 -outdir=/path/to/output/failed/glaciers/""", 97 | formatter_class=argparse.RawTextHelpFormatter, 98 | ) 99 | requiredNamed = parser.add_argument_group('required named arguments') 100 | requiredNamed.add_argument( 101 | '-rgi_region01', 102 | type=int, 103 | default=pygem_prms['setup']['rgi_region01'], 104 | help='Randoph Glacier Inventory region (can take multiple, e.g. `-run_region01 1 2 3`)', 105 | nargs='+', 106 | ) 107 | parser.add_argument( 108 | '-sim_climate_name', 109 | type=str, 110 | default=None, 111 | help='GCM name to compile results from (ex. ERA5 or CESM2)', 112 | ) 113 | parser.add_argument( 114 | '-sim_climate_scenario', 115 | action='store', 116 | type=str, 117 | default=None, 118 | help='rcp or ssp sim_climate_scenario used for model run (ex. rcp26 or ssp585)', 119 | ) 120 | parser.add_argument( 121 | '-sim_startyear', 122 | action='store', 123 | type=int, 124 | default=pygem_prms['climate']['sim_startyear'], 125 | help='start year for the model run', 126 | ) 127 | parser.add_argument( 128 | '-sim_endyear', 129 | action='store', 130 | type=int, 131 | default=pygem_prms['climate']['sim_endyear'], 132 | help='start year for the model run', 133 | ) 134 | parser.add_argument( 135 | '-option_calibration', 136 | action='store', 137 | type=str, 138 | default=pygem_prms['calib']['option_calibration'], 139 | help='calibration option ("emulator", "MCMC", "HH2015", "HH2015mod", "None")', 140 | ) 141 | parser.add_argument( 142 | '-option_bias_adjustment', 143 | action='store', 144 | type=int, 145 | default=pygem_prms['sim']['option_bias_adjustment'], 146 | help='Bias adjustment option (options: `0`, `1`, `2`, `3`. 0: no adjustment, \ 147 | 1: new prec scheme and temp building on HH2015, \ 148 | 2: HH2015 methods, 3: quantile delta mapping)', 149 | ) 150 | parser.add_argument( 151 | '-outdir', 152 | type=str, 153 | default=None, 154 | help='directory to output json file containing list of failed glaciers in each RGI region', 155 | ) 156 | parser.add_argument('-v', '--verbose', action='store_true', help='verbose flag') 157 | args = parser.parse_args() 158 | 159 | region = args.rgi_region01 160 | sim_climate_scenario = args.sim_climate_scenario 161 | sim_climate_name = args.sim_climate_name 162 | bias_adj = args.option_bias_adjustment 163 | simpath = pygem_prms['root'] + '/Output/simulations/' 164 | 165 | if sim_climate_name in ['ERA5', 'ERA-Interim', 'COAWST']: 166 | sim_climate_scenario = None 167 | bias_adj = 0 168 | 169 | if not isinstance(region, list): 170 | region = [region] 171 | 172 | if args.outdir and not os.path.isdir(args.outdir): 173 | print(f'Specified output path does not exist: {args.outdir}') 174 | sys.exit(1) 175 | 176 | for reg in region: 177 | failed_glacs = run( 178 | reg, 179 | simpath, 180 | args.sim_climate_name, 181 | sim_climate_scenario, 182 | args.option_calibration, 183 | bias_adj, 184 | args.sim_startyear, 185 | args.sim_endyear, 186 | ) 187 | if len(failed_glacs) > 0: 188 | if args.outdir: 189 | fout = os.path.join( 190 | args.outdir, 191 | f'R{str(reg).zfill(2)}_{args.sim_climate_name}_{sim_climate_scenario}_{args.sim_startyear}_{args.sim_endyear}_failed_rgiids.json', 192 | ).replace('None_', '') 193 | with open(fout, 'w') as f: 194 | json.dump(failed_glacs, f) 195 | print( 196 | f'List of failed glaciers for {sim_climate_name} {str(sim_climate_scenario).replace("None", "")} exported to: {fout}' 197 | ) 198 | if args.verbose: 199 | print( 200 | f'Failed glaciers for RGI region R{str(reg).zfill(2)} {args.sim_climate_name} {str(sim_climate_scenario).replace("None", "")} {args.sim_startyear}-{args.sim_endyear}:' 201 | ) 202 | print(failed_glacs) 203 | 204 | else: 205 | print( 206 | f'No glaciers failed from R{region}, for {sim_climate_name} {sim_climate_scenario.replace("None", "")}' 207 | ) 208 | -------------------------------------------------------------------------------- /pygem/bin/postproc/postproc_distribute_ice.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2024 Brandon Tober David Rounce 5 | 6 | Distributed under the MIT license 7 | """ 8 | 9 | # Built-in libraries 10 | import argparse 11 | import glob 12 | import multiprocessing 13 | import os 14 | import time 15 | from functools import partial 16 | 17 | import matplotlib.pyplot as plt 18 | 19 | # External libraries 20 | import numpy as np 21 | import xarray as xr 22 | 23 | # oggm 24 | from oggm import tasks, workflow 25 | from oggm.sandbox import distribute_2d 26 | 27 | # pygem imports 28 | from pygem.setup.config import ConfigManager 29 | 30 | # instantiate ConfigManager 31 | config_manager = ConfigManager() 32 | # read the config 33 | pygem_prms = config_manager.read_config() 34 | import pygem.pygem_modelsetup as modelsetup 35 | from pygem.oggm_compat import ( 36 | single_flowline_glacier_directory, 37 | single_flowline_glacier_directory_with_calving, 38 | ) 39 | 40 | 41 | def getparser(): 42 | """ 43 | Use argparse to add arguments from the command line 44 | """ 45 | parser = argparse.ArgumentParser(description='distrube PyGEM simulated ice thickness to a 2D grid') 46 | # add arguments 47 | parser.add_argument( 48 | '-simpath', 49 | action='store', 50 | type=str, 51 | nargs='+', 52 | help='path to PyGEM binned simulation (can take multiple, or can be a directory to process)', 53 | ) 54 | parser.add_argument( 55 | '-ncores', 56 | action='store', 57 | type=int, 58 | default=1, 59 | help='number of simultaneous processes (cores) to use', 60 | ) 61 | parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') 62 | 63 | return parser 64 | 65 | 66 | # method to convert pygem output to oggm flowline diagnostic output, in the format expected by oggm.distribute_2d 67 | def pygem_to_oggm(pygem_simpath, oggm_diag=None, debug=False): 68 | """ 69 | take PyGEM model output and temporarily store it in a way that OGGM distribute_2d expects 70 | this will be a netcdf file named fl_diagnostics.nc within the glacier directory - which contains 71 | the following coordinates: 72 | dis_along_flowline (dis_along_flowline): float64, along-flowline distance in m 73 | time (time): float64, model time in years 74 | and the following data variables: 75 | volume_m3 (time, dis_along_flowline): float64 76 | area_m2(time, dis_along_flowline): float64 77 | thickness_m (time, dis_along_flowline): float64 78 | """ 79 | pygem_ds = xr.open_dataset(pygem_simpath) 80 | time = pygem_ds.coords['year'].values.flatten().astype(float) 81 | distance_along_flowline = pygem_ds['bin_distance'].values.flatten().astype(float) 82 | area = pygem_ds['bin_area_annual'].values[0].astype(float).T 83 | thick = pygem_ds['bin_thick_annual'].values[0].astype(float).T 84 | vol = area * thick 85 | 86 | diag_ds = xr.Dataset() 87 | diag_ds.coords['time'] = time 88 | diag_ds.coords['dis_along_flowline'] = distance_along_flowline 89 | diag_ds['area_m2'] = (('time', 'dis_along_flowline'), area) 90 | diag_ds['area_m2'].attrs['description'] = 'Section area' 91 | diag_ds['area_m2'].attrs['unit'] = 'm 2' 92 | diag_ds['thickness_m'] = (('time', 'dis_along_flowline'), thick * np.nan) 93 | diag_ds['thickness_m'].attrs['description'] = 'Section thickness' 94 | diag_ds['thickness_m'].attrs['unit'] = 'm' 95 | diag_ds['volume_m3'] = (('time', 'dis_along_flowline'), vol) 96 | diag_ds['volume_m3'].attrs['description'] = 'Section volume' 97 | diag_ds['volume_m3'].attrs['unit'] = 'm 3' 98 | # diag_ds.to_netcdf(oggm_diag, 'w', group='fl_0') 99 | if debug: 100 | # plot volume 101 | vol = diag_ds.sum(dim=['dis_along_flowline'])['volume_m3'] 102 | f, ax = plt.subplots(1, figsize=(5, 5)) 103 | (vol / vol[0]).plot(ax=ax) 104 | plt.show() 105 | 106 | return diag_ds 107 | 108 | 109 | def plot_distributed_thickness(ds): 110 | f, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4)) 111 | vmax = round(np.nanmax(ds.simulated_thickness.sel(time=ds.coords['time'].values[0])) / 25) * 25 112 | ds.simulated_thickness.sel(time=ds.coords['time'].values[0]).plot(ax=ax1, vmin=0, vmax=vmax, add_colorbar=False) 113 | ds.simulated_thickness.sel(time=ds.coords['time'].values[-1]).plot(ax=ax2, vmin=0, vmax=vmax) 114 | ax1.axis('equal') 115 | ax2.axis('equal') 116 | plt.tight_layout() 117 | plt.show() 118 | 119 | 120 | def run(simpath, debug=False): 121 | if os.path.isfile(simpath): 122 | pygem_path, pygem_fn = os.path.split(simpath) 123 | pygem_fn_split = pygem_fn.split('_') 124 | f_suffix = '_'.join(pygem_fn_split[1:])[:-3] 125 | glac_no = pygem_fn_split[0] 126 | glacier_rgi_table = modelsetup.selectglaciersrgitable(glac_no=[glac_no]).loc[0, :] 127 | glacier_str = '{0:0.5f}'.format(glacier_rgi_table['RGIId_float']) 128 | # ===== Load glacier data: area (km2), ice thickness (m), width (km) ===== 129 | try: 130 | if glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_tidewater']: 131 | gdir = single_flowline_glacier_directory(glacier_str) 132 | gdir.is_tidewater = False 133 | else: 134 | # set reset=True to overwrite non-calving directory that may already exist 135 | gdir = single_flowline_glacier_directory_with_calving(glacier_str) 136 | gdir.is_tidewater = True 137 | except Exception as err: 138 | print(err) 139 | 140 | # create OGGM formatted flowline diagnostic dataset from PyGEM simulation 141 | pygem_fl_diag = pygem_to_oggm(os.path.join(pygem_path, pygem_fn), debug=debug) 142 | 143 | ### 144 | ### OGGM preprocessing steps before redistributing ice thickness form simulation 145 | ### 146 | # This is to add a new topography to the file (smoothed differently) 147 | workflow.execute_entity_task(distribute_2d.add_smoothed_glacier_topo, gdir) 148 | # This is to get the bed map at the start of the simulation 149 | workflow.execute_entity_task(tasks.distribute_thickness_per_altitude, gdir) 150 | # This is to prepare the glacier directory for the interpolation (needs to be done only once) 151 | workflow.execute_entity_task(distribute_2d.assign_points_to_band, gdir) 152 | ### 153 | # distribute simulation to 2d 154 | ds = workflow.execute_entity_task( 155 | distribute_2d.distribute_thickness_from_simulation, 156 | gdir, 157 | fl_diag=pygem_fl_diag, 158 | concat_input_filesuffix='_spinup_historical', # concatenate with the historical spinup 159 | output_filesuffix=f'_pygem_{f_suffix}', # filesuffix added to the output filename gridded_simulation.nc, if empty input_filesuffix is used 160 | )[0] 161 | print( 162 | '2D simulated ice thickness created: ', 163 | gdir.get_filepath('gridded_simulation', filesuffix=f'_pygem_{f_suffix}'), 164 | ) 165 | if debug: 166 | plot_distributed_thickness(ds) 167 | 168 | return 169 | 170 | 171 | def main(): 172 | time_start = time.time() 173 | args = getparser().parse_args() 174 | if (len(args.simpath) == 1) and (os.path.isdir(args.simpath[0])): 175 | sims = glob.glob(args.simpath[0] + '/*.nc') 176 | else: 177 | sims = args.simpath 178 | # number of cores for parallel processing 179 | if args.ncores > 1: 180 | ncores = int(np.min([len(args.simpath), args.ncores])) 181 | else: 182 | ncores = 1 183 | 184 | # set up partial function with debug argument 185 | run_with_debug = partial(run, debug=args.debug) 186 | # parallel processing 187 | print(f'Processing with {ncores} cores... \n{sims}') 188 | with multiprocessing.Pool(ncores) as p: 189 | p.map(run_with_debug, sims) 190 | 191 | print('Total processing time:', time.time() - time_start, 's') 192 | 193 | 194 | if __name__ == '__main__': 195 | main() 196 | -------------------------------------------------------------------------------- /pygem/utils/_funcs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2018 David Rounce 5 | 6 | Distributed under the MIT license 7 | 8 | Functions that didn't fit into other modules 9 | """ 10 | 11 | import argparse 12 | import json 13 | 14 | import numpy as np 15 | import pandas as pd 16 | from scipy.interpolate import interp1d 17 | 18 | from pygem.setup.config import ConfigManager 19 | 20 | # instantiate ConfigManager 21 | config_manager = ConfigManager() 22 | # read the config 23 | pygem_prms = config_manager.read_config() 24 | 25 | 26 | def str2bool(v): 27 | """ 28 | Convert a string to a boolean. 29 | 30 | Accepts: "yes", "true", "t", "1" → True; 31 | "no", "false", "f", "0" → False. 32 | 33 | Raises an error if input is unrecognized. 34 | """ 35 | if isinstance(v, bool): 36 | return v 37 | if v.lower() in ('yes', 'true', 't', '1', 'y'): 38 | return True 39 | elif v.lower() in ('no', 'false', 'f', '0', 'n'): 40 | return False 41 | else: 42 | raise argparse.ArgumentTypeError('Boolean value expected.') 43 | 44 | 45 | def parse_period(period_str, date_format=None, delimiter=None): 46 | """ 47 | parse a period string (e.g. '2000-01-01_2001-01-01') into two datetimes. 48 | requires a user-specified date_format (e.g. 'YYYY-MM-DD'). 49 | 50 | Parameters 51 | ---------- 52 | period_str : str 53 | period string to parse 54 | date_format : str, optional 55 | the date format to use for parsing (default: None, i.e., try to infer automatically) 56 | delimiter : str, optional 57 | the delimiter to use for splitting the period string (default: None, i.e., try common delimiters) 58 | Returns 59 | ------- 60 | t1, t2 : pd.Timestamp 61 | the two parsed datetimes 62 | """ 63 | 64 | if not date_format: 65 | raise ValueError("Period date_format must be provided (e.g. 'YYYY-MM-DD').") 66 | if not delimiter: 67 | raise ValueError("Period delimiter must be provided (e.g. '_').") 68 | 69 | # split and validate 70 | parts = [p.strip() for p in period_str.split(delimiter)] 71 | if len(parts) != 2: 72 | raise ValueError(f"Could not split '{period_str}' into two valid dates using '{delimiter}'.") 73 | 74 | # parse both parts 75 | try: 76 | t1 = pd.to_datetime(parts[0], format=date_format) 77 | t2 = pd.to_datetime(parts[1], format=date_format) 78 | except Exception as e: 79 | raise ValueError(f"Failed to parse '{period_str}' with format '{date_format}'") from e 80 | 81 | # ensure t2 > t1 82 | if t2 <= t1: 83 | raise ValueError(f"Invalid period '{period_str}': t2 ({t2.date()}) must be later than t1 ({t1.date()}).") 84 | 85 | return t1, t2 86 | 87 | 88 | def annualweightedmean_array(var, dates_table): 89 | """ 90 | Calculate annual mean of variable according to the timestep. 91 | 92 | Monthly timestep will group every 12 months, so starting month is important. 93 | 94 | Parameters 95 | ---------- 96 | var : np.ndarray 97 | Variable with monthly or daily timestep 98 | dates_table : pd.DataFrame 99 | Table of dates, year, month, days_in_step, wateryear, and season for each timestep 100 | Returns 101 | ------- 102 | var_annual : np.ndarray 103 | Annual weighted mean of variable 104 | """ 105 | if pygem_prms['time']['timestep'] == 'monthly': 106 | dayspermonth = dates_table['days_in_step'].values.reshape(-1, 12) 107 | # creates matrix (rows-years, columns-months) of the number of days per month 108 | daysperyear = dayspermonth.sum(axis=1) 109 | # creates an array of the days per year (includes leap years) 110 | weights = (dayspermonth / daysperyear[:, np.newaxis]).reshape(-1) 111 | # computes weights for each element, then reshapes it from matrix (rows-years, columns-months) to an array, 112 | # where each column (each monthly timestep) is the weight given to that specific month 113 | var_annual = (var * weights[np.newaxis, :]).reshape(-1, 12).sum(axis=1).reshape(-1, daysperyear.shape[0]) 114 | # computes matrix (rows - bins, columns - year) of weighted average for each year 115 | # explanation: var*weights[np.newaxis,:] multiplies each element by its corresponding weight; .reshape(-1,12) 116 | # reshapes the matrix to only have 12 columns (1 year), so the size is (rows*cols/12, 12); .sum(axis=1) 117 | # takes the sum of each year; .reshape(-1,daysperyear.shape[0]) reshapes the matrix back to the proper 118 | # structure (rows - bins, columns - year) 119 | # If averaging a single year, then reshape so it returns a 1d array 120 | if var_annual.shape[1] == 1: 121 | var_annual = var_annual.reshape(var_annual.shape[0]) 122 | elif pygem_prms['time']['timestep'] == 'daily': 123 | var_annual = var.mean(1) 124 | else: 125 | # var_annual = var.mean(1) 126 | assert 1 == 0, 'add this functionality for weighting that is not monthly or daily' 127 | return var_annual 128 | 129 | 130 | def haversine_dist(grid_lons, grid_lats, target_lons, target_lats): 131 | """ 132 | Compute haversine distances between each (lon_target, lat_target) 133 | and all (grid_lons, grid_lats) positions. 134 | 135 | Parameters: 136 | - grid_lons: (ncol,) array of longitudes from data 137 | - grid_lats: (ncol,) array of latitudes from data 138 | - target_lons: (n_targets,) array of target longitudes 139 | - target_lats: (n_targets,) array of target latitudes 140 | 141 | Returns: 142 | - distances: (n_targets, ncol) array of distances in km to each grid location for each target 143 | """ 144 | R = 6371.0 # Earth radius in kilometers 145 | 146 | # Convert degrees to radians 147 | grid_lons = np.radians(grid_lons)[np.newaxis, :] # (1, ncol) 148 | grid_lats = np.radians(grid_lats)[np.newaxis, :] 149 | target_lons = np.radians(target_lons)[:, np.newaxis] # (n_targets, 1) 150 | target_lats = np.radians(target_lats)[:, np.newaxis] 151 | 152 | dlon = grid_lons - target_lons 153 | dlat = grid_lats - target_lats 154 | 155 | a = np.sin(dlat / 2.0) ** 2 + np.cos(target_lats) * np.cos(grid_lats) * np.sin(dlon / 2.0) ** 2 156 | c = 2 * np.arcsin(np.sqrt(a)) 157 | 158 | return R * c # (n_targets, ncol) 159 | 160 | 161 | def interp1d_fill_gaps(x): 162 | """ 163 | Interpolate valid (non-NaN) values in a 1D array using linear interpolation, 164 | without extrapolating from NaNs at the edges. 165 | 166 | Parameters: 167 | ---------- 168 | x : ndarray 169 | A 1D array with possible NaN values to interpolate. 170 | 171 | Returns: 172 | ------- 173 | x : ndarray 174 | The 1D array with interpolated values for the NaN entries, leaving the valid values unchanged. 175 | 176 | Notes: 177 | ------ 178 | This function assumes that the input array `x` has evenly spaced data. It interpolates within the valid range of 179 | data and does not extrapolate beyond the first and last valid data points. 180 | """ 181 | # Find valid (non-NaN) indices 182 | mask = ~np.isnan(x) 183 | 184 | # If there are fewer than 2 valid values, return the array as is (no interpolation possible) 185 | if mask.sum() < 2: 186 | return x 187 | 188 | # Indices of valid (non-NaN) values 189 | valid_indices = np.where(mask)[0] 190 | first, last = valid_indices[0], valid_indices[-1] # Boundaries for valid range 191 | 192 | # Create the interpolation function based on valid indices 193 | interp_func = interp1d( 194 | valid_indices, 195 | x[mask], 196 | kind='linear', 197 | bounds_error=False, 198 | fill_value='extrapolate', 199 | ) 200 | 201 | # Interpolate only within the valid range (avoid extrapolation beyond valid indices) 202 | x[first : last + 1] = interp_func(np.arange(first, last + 1)) 203 | 204 | return x 205 | 206 | 207 | def append_json(file_path, new_key, new_value): 208 | """ 209 | Opens a JSON file, reads its content, adds a new key-value pair, 210 | and writes the updated data back to the file. 211 | 212 | :param file_path: Path to the JSON file 213 | :param new_key: The key to add 214 | :param new_value: The value to add 215 | """ 216 | try: 217 | # Read the existing data 218 | with open(file_path, 'r') as file: 219 | data = json.load(file) 220 | 221 | # Ensure the JSON data is a dictionary 222 | if not isinstance(data, dict): 223 | raise ValueError('JSON file must contain a dictionary at the top level.') 224 | 225 | # Add the new key-value pair 226 | data[new_key] = new_value 227 | 228 | # Write the updated data back to the file 229 | with open(file_path, 'w') as file: 230 | json.dump(data, file) 231 | 232 | except FileNotFoundError: 233 | print(f"Error: The file '{file_path}' was not found.") 234 | except json.JSONDecodeError: 235 | print('Error: The file does not contain valid JSON.') 236 | except Exception as e: 237 | print(f'An unexpected error occurred: {e}') 238 | -------------------------------------------------------------------------------- /pygem/shop/debris.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2018 David Rounce 5 | 6 | Distributed under the MIT license 7 | """ 8 | 9 | import logging 10 | import os 11 | import warnings 12 | 13 | import numpy as np 14 | import rasterio 15 | import xarray as xr 16 | from oggm import cfg 17 | from oggm.core.gis import rasterio_to_gdir 18 | from oggm.utils import entity_task, ncDataset 19 | 20 | # pygem imports 21 | from pygem.setup.config import ConfigManager 22 | 23 | # instantiate ConfigManager 24 | config_manager = ConfigManager() 25 | # read the config 26 | pygem_prms = config_manager.read_config() 27 | 28 | """ 29 | To-do list: 30 | - Add binned debris-covered area to flowlines 31 | - Fabien may have better way of processing debris rasters to gridded data without exporting .tif 32 | """ 33 | 34 | # Module logger 35 | log = logging.getLogger(__name__) 36 | 37 | # Add the new name "hd" to the list of things that the GlacierDirectory understands 38 | if 'debris_hd' not in cfg.BASENAMES: 39 | cfg.BASENAMES['debris_hd'] = ('debris_hd.tif', 'Raster of debris thickness data') 40 | if 'debris_ed' not in cfg.BASENAMES: 41 | cfg.BASENAMES['debris_ed'] = ( 42 | 'debris_ed.tif', 43 | 'Raster of debris enhancement factor data', 44 | ) 45 | 46 | 47 | @entity_task(log, writes=['debris_hd', 'debris_ed']) 48 | def debris_to_gdir( 49 | gdir, 50 | debris_dir=f'{pygem_prms["root"]}/{pygem_prms["mb"]["debris_relpath"]}', 51 | add_to_gridded=True, 52 | hd_max=5, 53 | hd_min=0, 54 | ed_max=10, 55 | ed_min=0, 56 | ): 57 | """Reproject the debris thickness and enhancement factor files to the given glacier directory 58 | 59 | Variables are exported as new files in the glacier directory. 60 | Reprojecting debris data from one map proj to another is done. 61 | We use bilinear interpolation to reproject the velocities to the local glacier map. 62 | 63 | Parameters 64 | ---------- 65 | gdir : :py:class:`oggm.GlacierDirectory` 66 | where to write the data 67 | """ 68 | 69 | assert os.path.exists(debris_dir), 'Error: debris directory does not exist.' 70 | 71 | hd_dir = debris_dir + 'hd_tifs/' + gdir.rgi_region + '/' 72 | ed_dir = debris_dir + 'ed_tifs/' + gdir.rgi_region + '/' 73 | 74 | glac_str_nolead = str(int(gdir.rgi_region)) + '.' + gdir.rgi_id.split('-')[1].split('.')[1] 75 | 76 | # If debris thickness data exists, then write to glacier directory 77 | if os.path.exists(hd_dir + glac_str_nolead + '_hdts_m.tif'): 78 | hd_fn = hd_dir + glac_str_nolead + '_hdts_m.tif' 79 | elif os.path.exists(hd_dir + glac_str_nolead + '_hdts_m_extrap.tif'): 80 | hd_fn = hd_dir + glac_str_nolead + '_hdts_m_extrap.tif' 81 | else: 82 | hd_fn = None 83 | 84 | if hd_fn is not None: 85 | rasterio_to_gdir(gdir, hd_fn, 'debris_hd', resampling='bilinear') 86 | if add_to_gridded and hd_fn is not None: 87 | output_fn = gdir.get_filepath('debris_hd') 88 | 89 | # append the debris data to the gridded dataset 90 | with rasterio.open(output_fn) as src: 91 | grids_file = gdir.get_filepath('gridded_data') 92 | with ncDataset(grids_file, 'a') as nc: 93 | # Mask values 94 | glacier_mask = nc['glacier_mask'][:] 95 | data = src.read(1) * glacier_mask 96 | data[data > hd_max] = 0 97 | data[data < hd_min] = 0 98 | 99 | # Write data 100 | vn = 'debris_hd' 101 | if vn in nc.variables: 102 | v = nc.variables[vn] 103 | else: 104 | v = nc.createVariable( 105 | vn, 106 | 'f8', 107 | ( 108 | 'y', 109 | 'x', 110 | ), 111 | zlib=True, 112 | ) 113 | v.units = 'm' 114 | v.long_name = 'Debris thicknness' 115 | v[:] = data 116 | 117 | # If debris enhancement factor data exists, then write to glacier directory 118 | if os.path.exists(ed_dir + glac_str_nolead + '_meltfactor.tif'): 119 | ed_fn = ed_dir + glac_str_nolead + '_meltfactor.tif' 120 | elif os.path.exists(ed_dir + glac_str_nolead + '_meltfactor_extrap.tif'): 121 | ed_fn = ed_dir + glac_str_nolead + '_meltfactor_extrap.tif' 122 | else: 123 | ed_fn = None 124 | 125 | if ed_fn is not None: 126 | rasterio_to_gdir(gdir, ed_fn, 'debris_ed', resampling='bilinear') 127 | if add_to_gridded and ed_fn is not None: 128 | output_fn = gdir.get_filepath('debris_ed') 129 | # append the debris data to the gridded dataset 130 | with rasterio.open(output_fn) as src: 131 | grids_file = gdir.get_filepath('gridded_data') 132 | with ncDataset(grids_file, 'a') as nc: 133 | # Mask values 134 | glacier_mask = nc['glacier_mask'][:] 135 | data = src.read(1) * glacier_mask 136 | data[data > ed_max] = 1 137 | data[data < ed_min] = 1 138 | # Write data 139 | vn = 'debris_ed' 140 | if vn in nc.variables: 141 | v = nc.variables[vn] 142 | else: 143 | v = nc.createVariable( 144 | vn, 145 | 'f8', 146 | ( 147 | 'y', 148 | 'x', 149 | ), 150 | zlib=True, 151 | ) 152 | v.units = '-' 153 | v.long_name = 'Debris enhancement factor' 154 | v[:] = data 155 | 156 | 157 | @entity_task(log, writes=['inversion_flowlines']) 158 | def debris_binned(gdir, ignore_debris=False, fl_str='inversion_flowlines', filesuffix=''): 159 | """Bin debris thickness and melt enhancement factors. 160 | 161 | Parameters 162 | ---------- 163 | gdir : :py:class:`oggm.GlacierDirectory` 164 | where to write the data 165 | 166 | fl_str : str 167 | The name of the flowline file to read. Default is 'inversion_flowlines'. 168 | 169 | filesuffix : str 170 | The filesuffix to use when reading the flowline file. Default is ''. 171 | """ 172 | # Nominal glaciers will throw error, so make sure inversion_flowlines exist 173 | try: 174 | flowlines = gdir.read_pickle(fl_str, filesuffix=filesuffix) 175 | fl = flowlines[0] 176 | 177 | assert len(flowlines) == 1, 'Error: binning debris only works for single flowlines at present' 178 | 179 | except: 180 | flowlines = None 181 | 182 | if flowlines is not None: 183 | # Add binned debris thickness and enhancement factors to flowlines 184 | if os.path.exists(gdir.get_filepath('debris_hd')): 185 | ds = xr.open_dataset(gdir.get_filepath('gridded_data')) 186 | glacier_mask = ds['glacier_mask'].values 187 | topo = ds['topo_smoothed'].values 188 | hd = ds['debris_hd'].values 189 | ed = ds['debris_ed'].values 190 | 191 | # Only bin on-glacier values 192 | idx_glac = np.where(glacier_mask == 1) 193 | topo_onglac = topo[idx_glac] 194 | hd_onglac = hd[idx_glac] 195 | ed_onglac = ed[idx_glac] 196 | 197 | # Bin edges 198 | nbins = len(fl.dis_on_line) 199 | z_center = (fl.surface_h[0:-1] + fl.surface_h[1:]) / 2 200 | z_bin_edges = np.concatenate( 201 | ( 202 | np.array([topo[idx_glac].max() + 1]), 203 | z_center, 204 | np.array([topo[idx_glac].min() - 1]), 205 | ) 206 | ) 207 | # Loop over bins and calculate the mean debris thickness and enhancement factor for each bin 208 | hd_binned = np.zeros(nbins) 209 | ed_binned = np.ones(nbins) 210 | for nbin in np.arange(0, len(z_bin_edges) - 1): 211 | bin_max = z_bin_edges[nbin] 212 | bin_min = z_bin_edges[nbin + 1] 213 | bin_idx = np.where((topo_onglac < bin_max) & (topo_onglac >= bin_min))[0] 214 | # Debris thickness and enhancement factors for on-glacier bins 215 | if len(bin_idx) > 0: 216 | with warnings.catch_warnings(): 217 | warnings.simplefilter('ignore', category=RuntimeWarning) 218 | hd_binned[nbin] = np.nanmean(hd_onglac[bin_idx]) 219 | ed_binned[nbin] = np.nanmean(ed_onglac[bin_idx]) 220 | hd_terminus = hd_binned[nbin] 221 | ed_terminus = ed_binned[nbin] 222 | # Debris thickness and enhancement factors for bins below the present-day glacier 223 | # assume an advancing glacier will have debris thickness equal to the terminus 224 | elif np.mean([bin_min, bin_max]) < topo[idx_glac].min(): 225 | hd_binned[nbin] = hd_terminus 226 | ed_binned[nbin] = ed_terminus 227 | else: 228 | hd_binned[nbin] = 0 229 | ed_binned[nbin] = 1 230 | 231 | fl.debris_hd = hd_binned 232 | fl.debris_ed = ed_binned 233 | 234 | else: 235 | nbins = len(fl.dis_on_line) 236 | fl.debris_hd = np.zeros(nbins) 237 | fl.debris_ed = np.ones(nbins) 238 | 239 | # Overwrite pickle 240 | gdir.write_pickle(flowlines, fl_str, filesuffix=filesuffix) 241 | -------------------------------------------------------------------------------- /pygem/bin/postproc/postproc_subannual_mass.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2024 Brandon Tober David Rounce 5 | 6 | Distributed under the MIT license 7 | 8 | derive sub-annual glacierwide mass for PyGEM simulation using annual glacier mass and sub-annual total mass balance 9 | """ 10 | 11 | # Built-in libraries 12 | import argparse 13 | import collections 14 | import glob 15 | import json 16 | import multiprocessing 17 | import os 18 | import time 19 | from functools import partial 20 | 21 | import matplotlib.pyplot as plt 22 | import numpy as np 23 | import pandas as pd 24 | 25 | # External libraries 26 | import xarray as xr 27 | 28 | # pygem imports 29 | from pygem.setup.config import ConfigManager 30 | 31 | # instantiate ConfigManager 32 | config_manager = ConfigManager() 33 | # read the config 34 | pygem_prms = config_manager.read_config() 35 | 36 | 37 | # ----- FUNCTIONS ----- 38 | def getparser(): 39 | """ 40 | Use argparse to add arguments from the command line 41 | """ 42 | parser = argparse.ArgumentParser( 43 | description='process sub-annual glacierwide mass from annual mass and total sub-annual mass balance' 44 | ) 45 | # add arguments 46 | parser.add_argument( 47 | '-simpath', 48 | action='store', 49 | type=str, 50 | nargs='+', 51 | help='path to PyGEM simulation (can take multiple)', 52 | ) 53 | parser.add_argument( 54 | '-simdir', 55 | action='store', 56 | type=str, 57 | default=None, 58 | help='directory with glacierwide simulation outputs for which to process sub-annual mass', 59 | ) 60 | parser.add_argument( 61 | '-ncores', 62 | action='store', 63 | type=int, 64 | default=1, 65 | help='number of simultaneous processes (cores) to use', 66 | ) 67 | parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') 68 | return parser 69 | 70 | 71 | def get_subannual_mass(df_annual, df_sub, debug=False): 72 | """ 73 | funciton to calculate the sub-annual glacier mass 74 | from annual glacier mass and sub-annual total mass balance 75 | 76 | Parameters 77 | ---------- 78 | glac_mass_annual : float 79 | ndarray containing the annual glacier mass for each year computed by PyGEM 80 | shape: [#glac, #years] 81 | unit: kg 82 | glac_massbaltotal : float 83 | ndarray containing the total mass balance computed by PyGEM 84 | shape: [#glac, #steps] 85 | unit: kg 86 | 87 | Returns 88 | ------- 89 | glac_mass: float 90 | ndarray containing the running glacier mass 91 | shape : [#glac, #steps] 92 | unit: kg 93 | 94 | """ 95 | 96 | # ensure datetime and sorted 97 | df_annual['time'] = pd.to_datetime(df_annual['time']) 98 | df_sub['time'] = pd.to_datetime(df_sub['time']) 99 | df_annual = df_annual.sort_values('time').reset_index(drop=True) 100 | df_sub = df_sub.sort_values('time').reset_index(drop=True) 101 | 102 | # year columns 103 | df_annual['year'] = df_annual['time'].dt.year 104 | df_sub['year'] = df_sub['time'].dt.year 105 | 106 | # map annual starting mass to sub rows 107 | annual_by_year = df_annual.set_index('year')['mass'] 108 | df_sub['annual_mass'] = df_sub['year'].map(annual_by_year) 109 | 110 | # shift massbaltotal within each year so the Jan value doesn't affect Jan mass itself 111 | # i.e., massbaltotal at Jan-01 contributes to Feb-01 mass 112 | df_sub['mb_shifted'] = df_sub.groupby('year')['massbaltotal'].shift(1).fillna(0.0) 113 | 114 | # cumulative sum of shifted values within each year 115 | df_sub['cum_mb_since_year_start'] = df_sub.groupby('year')['mb_shifted'].cumsum() 116 | 117 | # compute sub-annual mass 118 | df_sub['mass'] = df_sub['annual_mass'] + df_sub['cum_mb_since_year_start'] 119 | 120 | if debug: 121 | # --- Quick plot of Jan start points (sub vs annual) --- 122 | # Plot all sub-annual masses as a line 123 | plt.figure(figsize=(12, 5)) 124 | plt.plot(df_sub['time'], df_sub['mass'], label='Sub-annual mass', color='blue') 125 | 126 | # Overlay annual masses as points/line 127 | plt.plot(df_annual['time'], df_annual['mass'], 'o--', label='Annual mass', color='orange', markersize=6) 128 | 129 | # Labels and legend 130 | plt.xlabel('Time') 131 | plt.ylabel('Glacier Mass') 132 | plt.title('Sub-annual Glacier Mass vs Annual Mass') 133 | plt.legend() 134 | plt.tight_layout() 135 | plt.show() 136 | 137 | return df_sub['mass'].values 138 | 139 | 140 | def update_xrdataset(input_ds, glac_mass, timestep): 141 | """ 142 | update xarray dataset to add new fields 143 | 144 | Parameters 145 | ---------- 146 | xrdataset : xarray Dataset 147 | existing xarray dataset 148 | newdata : ndarray 149 | new data array 150 | description: str 151 | describing new data field 152 | 153 | output_ds : xarray Dataset 154 | empty xarray dataset that contains variables and attributes to be filled in by simulation runs 155 | encoding : dictionary 156 | encoding used with exporting xarray dataset to netcdf 157 | """ 158 | # coordinates 159 | glac_values = input_ds.glac.values 160 | time_values = input_ds.time.values 161 | 162 | output_coords_dict = collections.OrderedDict() 163 | output_coords_dict['glac_mass'] = collections.OrderedDict([('glac', glac_values), ('time', time_values)]) 164 | 165 | # Attributes dictionary 166 | output_attrs_dict = {} 167 | output_attrs_dict['glac_mass'] = { 168 | 'long_name': 'glacier mass', 169 | 'units': 'kg', 170 | 'temporal_resolution': timestep, 171 | 'comment': 'glacier mass', 172 | } 173 | 174 | # Add variables to empty dataset and merge together 175 | count_vn = 0 176 | encoding = {} 177 | for vn in output_coords_dict.keys(): 178 | empty_holder = np.zeros([len(output_coords_dict[vn][i]) for i in list(output_coords_dict[vn].keys())]) 179 | output_ds = xr.Dataset( 180 | {vn: (list(output_coords_dict[vn].keys()), empty_holder)}, 181 | coords=output_coords_dict[vn], 182 | ) 183 | count_vn += 1 184 | # Merge datasets of stats into one output 185 | if count_vn == 1: 186 | output_ds_all = output_ds 187 | else: 188 | output_ds_all = xr.merge((output_ds_all, output_ds)) 189 | # Add attributes 190 | for vn in output_ds_all.variables: 191 | try: 192 | output_ds_all[vn].attrs = output_attrs_dict[vn] 193 | except: 194 | pass 195 | # Encoding (specify _FillValue, offsets, etc.) 196 | encoding[vn] = {'_FillValue': None, 'zlib': True, 'complevel': 9} 197 | output_ds_all['glac_mass'].values = glac_mass[np.newaxis, :] 198 | 199 | return output_ds_all, encoding 200 | 201 | 202 | def run(simpath, debug=False): 203 | """ 204 | create sub-annual mass data product 205 | Parameters 206 | ---------- 207 | simpath : str 208 | patht to PyGEM simulation 209 | """ 210 | if os.path.exists(simpath): 211 | try: 212 | # open dataset 213 | statsds = xr.open_dataset(simpath) 214 | timestep = json.loads(statsds.attrs['model_parameters'])['timestep'] 215 | yvals = statsds.year.values 216 | # convert to pandas dataframe with annual mass 217 | annual_df = pd.DataFrame( 218 | {'time': pd.to_datetime([f'{y}-01-01' for y in yvals]), 'mass': statsds.glac_mass_annual[0].values} 219 | ) 220 | tvals = statsds.time.values 221 | # convert to pandas dataframe with sub-annual mass balance 222 | steps_df = pd.DataFrame( 223 | { 224 | 'time': pd.to_datetime([t.strftime('%Y-%m-%d') for t in tvals]), 225 | 'massbaltotal': statsds.glac_massbaltotal[0].values * pygem_prms['constants']['density_ice'], 226 | } 227 | ) 228 | 229 | # calculate sub-annual mass - pygem glac_massbaltotal is in units of m3, so convert to mass using density of ice 230 | glac_mass = get_subannual_mass(annual_df, steps_df, debug=debug) 231 | statsds.close() 232 | 233 | # update dataset to add sub-annual mass change 234 | output_ds_stats, encoding = update_xrdataset(statsds, glac_mass, timestep) 235 | 236 | # close input ds before write 237 | statsds.close() 238 | 239 | # append to existing stats netcdf 240 | output_ds_stats.to_netcdf(simpath, mode='a', encoding=encoding, engine='netcdf4') 241 | 242 | # close datasets 243 | output_ds_stats.close() 244 | 245 | except: 246 | pass 247 | else: 248 | print('Simulation not found: ', simpath) 249 | 250 | return 251 | 252 | 253 | def main(): 254 | time_start = time.time() 255 | args = getparser().parse_args() 256 | 257 | simpath = None 258 | if args.simdir: 259 | # get list of sims 260 | simpath = glob.glob(args.simdir + '/*.nc') 261 | else: 262 | if args.simpath: 263 | simpath = args.simpath 264 | 265 | if simpath: 266 | # number of cores for parallel processing 267 | if args.ncores > 1: 268 | ncores = int(np.min([len(simpath), args.ncores])) 269 | else: 270 | ncores = 1 271 | 272 | # set up partial function with debug argument 273 | run_partial = partial(run, debug=args.debug) 274 | 275 | # Parallel processing 276 | print('Processing with ' + str(ncores) + ' cores...') 277 | with multiprocessing.Pool(ncores) as p: 278 | p.map(run_partial, simpath) 279 | 280 | print('Total processing time:', time.time() - time_start, 's') 281 | 282 | 283 | if __name__ == '__main__': 284 | main() 285 | -------------------------------------------------------------------------------- /docs/model_structure.md: -------------------------------------------------------------------------------- 1 | (model_structure_and_workflow_target)= 2 | # Model Structure and Workflow 3 | The Python Glacier Evolution Model is written in Python 3. The model is available via [Github](https://github.com/PyGEM-Community/PyGEM) and the [Python Package Index](https://pypi.org/project/pygem/) 4 | 5 | Several command line scripts are set up upon installation for running the model. See the [Install_PyGEM](install_pygem_target) section for more details on installing the model. 6 | 7 | Model parameters are specified via the configuration file (*~/PyGEM/config.yaml*), and a number of key parameters can also be passed to model scripts as command line arguments. The configuration file is [well documented](pygem_config_overview_target). 8 | 9 | ## Spatial and Temporal Resolution 10 | PyGEM models each glacier independently using a monthly timestep and elevation bins. We plan to add options to include daily timesteps in the near future and plan to develop new calibration options that can leverage regional datasets as well. 11 | 12 | (directory_structure_target)= 13 | ## Directory structure 14 | Currently, the model does not have a “required” set of directories. The relative paths for each dataset used by the model are defined within the user's [configuration file](pygem_config_overview_target), and can be modified as desired. For simplicity, we recommend that users implement the same directory structure as the developers (see [sample files](https://drive.google.com/file/d/1Wu4ZqpOKxnc4EYhcRHQbwGq95FoOxMfZ/view?usp=drive_link)): 15 | 16 | ~/pygem_data/
17 | ├ [massbalance_data](input_mb_data_target): geodetic mass balance data derived from DEM differencing that is used for calibration.
18 | ├ [IceThickness_Farinotti](input_thickness_data_target): reference ice thickness estimates (Farinotti et al. 2019). The directory is optional unless you want to match the reference ice thickness estimates
19 | ├ [oggm_gdirs](input_glacier_data_target): glacier directories downloaded from OGGM. This directory will be automatically generated by the pre-processing steps from OGGM.
20 | ├ Output: model output data
21 | ├ [RGI](input_glacier_inventory_target): Randolph Glacier Inventory information
22 | ├ [WGMS](input_mb_data_target): WGMS mass balance data for validation. The directory is optional in case you prefer to validate your model with different data.
23 | ├ [climate_data](climate_data_target): reference and future climate data
24 | └ [debris_data](input_debris_data_target): debris thickness and sub-debris melt enhancement factors.
25 | 26 | ```{warning} 27 | If you use a different file structure and do not update the relative file paths in the *~/PyGEM/config.yaml*, you will get an error and PyGEM will not run! 28 | ``` 29 | 30 | (model_workflow_target)= 31 | ## Model Workflow 32 | The model code itself is heavily commented with the hope that the code is easy to follow and develop further. After [installing PyGEM](install_pygem_target), downloading the required [input files](model_inputs_target), and setting up the [directory structure](directory_structure_target) (or modifying the *~/PyGEM/config.yaml* with your preferred directory structure) you are ready to run the code! Generally speaking, the workflow includes: 33 | * [Pre-process data](preprocessing_target) (optional if including more data) 34 | * [Set up configuration file](config_workflow_target) 35 | * [Calibrate climatic mass balance parameters](workflow_cal_prms_target) 36 | * [Calibrate frontal ablation parameter](workflow_cal_frontalablation_target) (optional for marine-terimating glaciers) 37 | * [Calibrate ice viscosity parameter](workflow_run_inversion_target) 38 | * [Run model simulation](workflow_sim_target) 39 | * [Post-process output](workflow_post_target) 40 | * [Analyze output](workflow_analyze_target) 41 | 42 | 43 | (preprocessing_target)= 44 | ### Pre-processing 45 | We rely heavily on [OGGM's pre-processing modules](https://docs.oggm.org/en/stable/shop.html), which are state-of-the-art. For most people, these glacier directories will be sufficient to get started. However, there are times when we work with different datasets and need to do our own pre-processing. For example, the following script corrects the geodetic mass balance from [Hugonnet et al. (2021)](https://www.nature.com/articles/s41586-021-03436-z) to account for the mass lost below the water level due to frontal ablation from [Kochtitzky et al. (2022)](https://www.nature.com/articles/s41467-022-33231-x). 46 | ``` 47 | run_calibration_frontalablation 48 | ``` 49 | 50 | 51 | (config_workflow_target)= 52 | ### Set up configuration file 53 | *~/PyGEM/config.yaml* is PyGEM's configuration file where the user can specify the glaciers/regions to model; model physics, calibration, and simulation options; relative filepaths for relevant datasets; etc. 54 | 55 | The only critical component of the configuration file which must be set prior to running PyGEM is the user's `root` path to their PyGEM datasets (the absolute path to the root *~/pygem_data/* directory shown [above](directory_structure_target)). 56 | 57 | For more details, see the [configuration file overview](pygem_config_overview_target). 58 | 59 | ```{note} 60 | The first time you process glacier directories (e.g., happens automatically with the run_calibration.py), you want to set overwrite_gdirs=True in *~/PyGEM/config.yaml*. This will add the mass balance and reference ice thickness data to the glacier directories. To avoid redownloading/reprocessing the glacier directories every time, after performed once set overwrite_gdirs=False. 61 | ``` 62 | 63 | (workflow_cal_prms_target)= 64 | ### Calibrate mass balance model parameters 65 | The model parameters (degree-day factor of snow, precipitation factor, and temperature bias) must be calibrated for model results to be meaningful. This is done using *run_calibration.py*. Several options exist (see [Model Calibration](calibration_target) for specific details), but generally speaking the option_calibration will be specified in *~/PyGEM/config.yaml* or passed as a command-line argument as follows: 66 | ``` 67 | run_calibration -option_calibration 68 | ``` 69 | 70 | If successful, the script will run without error and output the following: 71 | * ../Output/calibration/\[RGI Order 1 region\]/\[glac_no\]-model_prms.json 72 | * additional files will be generated if using the emulator 73 | 74 | For more details, see the [run_calibration.py Script Overview](run_calibration_target). 75 | 76 | 77 | (workflow_cal_frontalablation_target)= 78 | ### Calibrate frontal ablation parameter 79 | **(Optional)** If you want to account for frontal ablation associated with marine-terminating glaciers, then the frontal ablation parameter needs to be calibrated. This is done using *run_calibration_frontalablation.py*: 80 | 81 | ``` 82 | run_calibration_frontalablation (optionally pass -rgi_region01 ) 83 | ``` 84 | If successful, the script will run without error and output the following: 85 | * ../frontalablation_data/analysis/all-frontalablation_cal_ind.csv 86 | * ../DEMs/Hugonnet2021/df_pergla_global_20yr-filled-frontalablation-corrected.csv 87 | 88 | For more details, see the [run_calibration_frontalablation.py Script Overview](run_calibration_fa_overview_target). 89 | 90 | ```{warning} 91 | Circularity issues exist in calibrating the frontal ablation parameter as the mass balance model parameters are required to estimate the ice thickness, but the frontal ablation will affect the mass balance estimates and thus the mass balance model parameters. We suggest taking an iterative approach: calibrate the mass balance model parameters, calibrate the frontal ablation parameter, update the glacier-wide climatic mass balance, and recalibrate the mass balance model parameters. 92 | ``` 93 | 94 | 95 | (workflow_run_inversion_target)= 96 | ### Calibrate ice viscosity model parameter 97 | The ice viscosity ("Glen A") model parameter is calibrated such that the ice volume estimated using the calibrated mass balance gradients are consistent with the reference ice volume estimates ([Farinotti et al. (2019)](https://www.nature.com/articles/s41561-019-0300-3)) for each RGI region. This is done by running the following: 98 | ``` 99 | run_inversion 100 | ``` 101 | 102 | For more details, see the [run_inversion.py Script Overview](run_inversion_overview_target). 103 | 104 | 105 | (workflow_sim_target)= 106 | ### Run model simulation 107 | Model simulations are performed using run_simulation.py. If no GCMs are specified in the command line, the default will be to run a model simulation with the reference data (e.g., ERA5). We currently recommend that
**historical simulations** be performed without evolving the glacier geometry; thus, option_dynamics = None in *~/PyGEM/config.yaml* and the ref_startyear and ref_endyear are used to set the length of the simulation. The simulation can then be run using the following: 108 | ``` 109 | run_simulation 110 | ``` 111 | **Future simulations** require specifying a GCM and scenario, which is passed to the script through the argument parser. For example, the following will run a simulation for CESM2 SSP2-4.5: 112 | ``` 113 | run_simulation -gcm_name CESM2 -scenario ssp245 114 | ``` 115 | ```{note} 116 | For future simulations, at a minimum the user should specify the dynamical option (option_dynamics), start year (gcm_startyear), end year (gcm_endyear), bias correction option (option_bias_adjustment). 117 | ``` 118 | If successful, the script will run without error and output the following: 119 | * ../Output/simulation/\[RGI Order 1 region\]/\[GCM name\]/\[Scenario\]/stats/\[glac_no\]_\[GCM name\]_\[Scenario\]_\[Calibration Option\]_ba\[bias adjustment option\]_\[number of simulations\]_\[start year\]_\[end year\]_all.nc 120 | * additional netcdf files may be output based on the user specifications in *~/PyGEM/config.yaml* 121 | 122 | For more details, see the [run_simulation.py Script Overview](run_simulation_target). 123 | 124 | 125 | (workflow_post_target)= 126 | ### Post-process output 127 | There are currently several scripts available to post-process PyGEM simulations. To aggregate simulations by RGI region, climate scenario, and variable, run the *postproc_compile_simulations.py* script. For example to compile all Alaska's glacier mass, area, runoff, etc. for various scenarios we would run the following: 128 | 129 | ``` 130 | postproc_compile_simulations -rgi_region 01 -scenario ssp245 ssp370 ssp585 131 | ``` 132 | 133 | (workflow_analyze_target)= 134 | ### Analyze output 135 | All users will analyze PyGEM output in different ways. Various Jupyter Notebooks are provided for analyzing PyGEM results in a separate [GitHub repository](https://github.com/PyGEM-Community/PyGEM-notebooks). The repositories analysis notebooks (those which have the prefix "analze_") are thus not meant to be an exhaustive list of analyses, but rather to provide some general scripts to produce diagnostic figures of mass, area, runoff, and ice thickness change. These notebooks will also help you get familiar with working with the model and post-processed outputs. 136 | 137 | For more detail, see [Model Output Section](model_output_overview_target). 138 | -------------------------------------------------------------------------------- /pygem/shop/meltextent_and_snowline_1d.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2025 Brandon Tober , David Rounce 5 | 6 | Distributed under the MIT license 7 | """ 8 | 9 | # Built-in libaries 10 | import datetime 11 | import logging 12 | import os 13 | 14 | import pandas as pd 15 | 16 | # External libraries 17 | # Local libraries 18 | from oggm import cfg 19 | from oggm.utils import entity_task 20 | 21 | # pygem imports 22 | from pygem.setup.config import ConfigManager 23 | 24 | # instantiate ConfigManager 25 | config_manager = ConfigManager() 26 | # read the config 27 | pygem_prms = config_manager.read_config() 28 | 29 | 30 | # Module logger 31 | log = logging.getLogger(__name__) 32 | 33 | # Add the new name "snowline_1d" to the list of things that the GlacierDirectory understands 34 | if 'meltextent_1d' not in cfg.BASENAMES: 35 | cfg.BASENAMES['meltextent_1d'] = ( 36 | 'meltextent_1d.json', 37 | '1D snowline data', 38 | ) 39 | if 'snowline_1d' not in cfg.BASENAMES: 40 | cfg.BASENAMES['snowline_1d'] = ( 41 | 'snowline_1d.json', 42 | '1D snowline data', 43 | ) 44 | 45 | 46 | @entity_task(log, writes=['snowline_1d']) 47 | def meltextent_1d_to_gdir( 48 | gdir, 49 | ): 50 | """ 51 | Add 1d melt extent observations to the given glacier directory 52 | 53 | Parameters 54 | ---------- 55 | gdir : :py:class:`oggm.GlacierDirectory` 56 | where to write the data 57 | 58 | expected csv structure: 59 | Columns: 'date', 'z', 'z_min', 'z_max', 'direction' 60 | 'date': Observation date, stored as a string in 'YYYY-MM-DD' format 61 | 'z': Melt extent elevation (meters) 62 | 'z_min': Melt extent elevation minimum (meters) 63 | 'z_max': Melt extent elevation maximum (meters) 64 | 'direction': SAR path direction, stored as a string (e.g., 'ascending' or 'descending') 65 | 'ref_dem': Reference DEM used for elevation values 66 | 'ref_dem_year': Reference DEM year for elevation value of observations (m a.s.l.) (e.g., 2013 if using COP30) 67 | """ 68 | # get dataset file path 69 | meltextent_1d_fp = ( 70 | f'{pygem_prms["root"]}/' 71 | f'{pygem_prms["calib"]["data"]["meltextent_1d"]["meltextent_1d_relpath"]}/' 72 | f'{gdir.rgi_id.split("-")[1]}_melt_extent_elev.csv' 73 | ) 74 | 75 | # check for file 76 | if os.path.exists(meltextent_1d_fp): 77 | meltextent_1d_df = pd.read_csv(meltextent_1d_fp) 78 | else: 79 | log.debug('No melt extent data to load, skipping task.') 80 | raise Warning('No melt extent data to load') # file not found, skip 81 | 82 | validate_meltextent_1d_structure(meltextent_1d_df) 83 | meltextent_1d_dict = meltextent_csv_to_dict(meltextent_1d_df) 84 | gdir.write_json(meltextent_1d_dict, 'meltextent_1d') 85 | 86 | 87 | def validate_meltextent_1d_structure(data): 88 | """Validate that meltextent_1d CSV structure matches expected format.""" 89 | 90 | required_cols = [ 91 | 'date', 92 | 'z', 93 | 'z_min', 94 | 'z_max', 95 | 'direction', 96 | 'ref_dem', 97 | 'ref_dem_year', 98 | ] 99 | for col in required_cols: 100 | if col not in data.columns: 101 | raise ValueError(f"Missing required column '{col}' in melt extent CSV.") 102 | 103 | # Validate dates 104 | dates = data['date'] 105 | if not isinstance(dates, pd.Series) or len(dates) == 0: 106 | raise ValueError("'dates' must be a non-empty series.") 107 | for i, date_str in enumerate(dates): 108 | try: 109 | datetime.datetime.strptime(date_str, '%Y-%m-%d') 110 | except ValueError: 111 | raise ValueError(f"Invalid date format in 'dates[{i}]': {date_str}") from None 112 | 113 | # Validate z 114 | z = data['z'] 115 | if not (isinstance(z, pd.Series) and len(z) == len(dates)): 116 | raise ValueError(f"'z' must be a series of length {len(dates)}.") 117 | if not all(isinstance(x, (int, float)) for x in z): 118 | raise ValueError("All 'z' values must be numeric.") 119 | 120 | # Validate z_min 121 | z_min = data['z_min'] 122 | if not (isinstance(z_min, pd.Series) and len(z_min) == len(dates)): 123 | raise ValueError(f"'z_min' must be a series of length {len(dates)}.") 124 | if not all(isinstance(x, (int, float)) for x in z_min): 125 | raise ValueError("All 'z_min' values must be numeric.") 126 | 127 | # Validate z_max 128 | z_max = data['z_max'] 129 | if not (isinstance(z_max, pd.Series) and len(z_max) == len(dates)): 130 | raise ValueError(f"'z_max' must be a series of length {len(dates)}.") 131 | if not all(isinstance(x, (int, float)) for x in z_max): 132 | raise ValueError("All 'z_max' values must be numeric.") 133 | 134 | # Validate direction 135 | direction = data['direction'] 136 | if not (isinstance(direction, pd.Series) and len(direction) == len(dates)): 137 | raise ValueError(f"'direction' must be a series of length {len(dates)}.") 138 | if not all(isinstance(x, str) for x in direction): 139 | raise ValueError("All 'direction' values must be strings.") 140 | 141 | # Validate reference DEM 142 | ref_dem = data['ref_dem'].dropna().unique() 143 | if not isinstance(ref_dem, (str)): 144 | raise TypeError(f"'ref_dem' must be an string, but got {ref_dem} ({type(ref_dem).__name__}).") 145 | 146 | # Validate reference DEM year 147 | dem_year = data['ref_dem_year'].dropna().unique() 148 | if len(dem_year) != 1: 149 | raise ValueError(f"'ref_dem_year' must have exactly one unique value, but found {len(dem_year)}: {dem_year}") 150 | if not isinstance(dem_year, (int)): 151 | raise TypeError(f"'ref_dem_year' must be an integer, but got {dem_year} ({type(dem_year).__name__}).") 152 | 153 | return True 154 | 155 | 156 | def meltextent_csv_to_dict(data): 157 | """Convert snowline_1d CSV to JSON for OGGM ingestion.""" 158 | dates = data['date'].astype(str).tolist() 159 | z = data['z'].astype(float).tolist() 160 | z_min = data['z_min'].astype(float).tolist() 161 | z_max = data['z_max'].astype(float).tolist() 162 | direction = data['direction'].astype(str).tolist() 163 | ref_dem = data['ref_dem'].astype(str).tolist()[0] 164 | ref_dem_year = data['ref_dem_year'].astype(int).tolist()[0] 165 | 166 | data_dict = { 167 | 'date': dates, 168 | 'z': z, 169 | 'z_min': z_min, 170 | 'z_max': z_max, 171 | 'direction': direction, 172 | 'ref_dem': ref_dem, 173 | 'ref_dem_year': ref_dem_year, 174 | } 175 | return data_dict 176 | 177 | 178 | @entity_task(log, writes=['snowline_1d']) 179 | def snowline_1d_to_gdir( 180 | gdir, 181 | ): 182 | """ 183 | Add 1d snowline observations to the given glacier directory 184 | 185 | Parameters 186 | ---------- 187 | gdir : :py:class:`oggm.GlacierDirectory` 188 | where to write the data 189 | 190 | expected csv structure: 191 | Columns: 'date', 'z', 'z_min', 'z_max', 'direction' 192 | 'date': Observation date, stored as a string in 'YYYY-MM-DD' format 193 | 'z': Snowline elevation (m a.s.l.) 194 | 'z_min': Snowline elevation minimum (m a.s.l.) 195 | 'z_max': Snowline elevation maximum (m a.s.l.) 196 | 'direction': SAR path direction, stored as a string (e.g., 'ascending' or 'descending') 197 | 'ref_dem': Reference DEM used for elevation values 198 | 'ref_dem_year': Reference DEM year for elevation value of observations (m a.s.l.) (e.g., 2013 if using COP30) 199 | """ 200 | # get dataset file path 201 | snowline_1d_fp = ( 202 | f'{pygem_prms["root"]}/' 203 | f'{pygem_prms["calib"]["data"]["snowline_1d"]["snowline_1d_relpath"]}/' 204 | f'{gdir.rgi_id.split("-")[1]}_snowline_elev.csv' 205 | ) 206 | 207 | # check for file 208 | if os.path.exists(snowline_1d_fp): 209 | snowline_1d_df = pd.read_csv(snowline_1d_fp) 210 | else: 211 | log.debug('No snowline data to load, skipping task.') 212 | raise Warning('No snowline data to load') # file not found, skip 213 | 214 | validate_snowline_1d_structure(snowline_1d_df) 215 | snowline_1d_dict = snowline_csv_to_dict(snowline_1d_df) 216 | gdir.write_json(snowline_1d_dict, 'snowline_1d') 217 | 218 | 219 | def validate_snowline_1d_structure(data): 220 | """Validate that snowline_1d CSV structure matches expected format.""" 221 | 222 | required_cols = [ 223 | 'date', 224 | 'z', 225 | 'z_min', 226 | 'z_max', 227 | 'direction', 228 | 'ref_dem', 229 | 'ref_dem_year', 230 | ] 231 | for col in required_cols: 232 | if col not in data.columns: 233 | raise ValueError(f"Missing required column '{col}' in snowline CSV.") 234 | 235 | # Validate dates 236 | dates = data['date'] 237 | if not isinstance(dates, pd.Series) or len(dates) == 0: 238 | raise ValueError("'dates' must be a non-empty series.") 239 | for i, date_str in enumerate(dates): 240 | try: 241 | datetime.datetime.strptime(date_str, '%Y-%m-%d') 242 | except ValueError: 243 | raise ValueError(f"Invalid date format in 'dates[{i}]': {date_str}") from None 244 | 245 | # Validate z 246 | z = data['z'] 247 | if not (isinstance(z, pd.Series) and len(z) == len(dates)): 248 | raise ValueError(f"'z' must be a series of length {len(dates)}.") 249 | if not all(isinstance(x, (int, float)) for x in z): 250 | raise ValueError("All 'z' values must be numeric.") 251 | 252 | # Validate z_min 253 | z_min = data['z_min'] 254 | if not (isinstance(z_min, pd.Series) and len(z_min) == len(dates)): 255 | raise ValueError(f"'z_min' must be a series of length {len(dates)}.") 256 | if not all(isinstance(x, (int, float)) for x in z_min): 257 | raise ValueError("All 'z_min' values must be numeric.") 258 | 259 | # Validate z_max 260 | z_max = data['z_max'] 261 | if not (isinstance(z_max, pd.Series) and len(z_max) == len(dates)): 262 | raise ValueError(f"'z_max' must be a series of length {len(dates)}.") 263 | if not all(isinstance(x, (int, float)) for x in z_max): 264 | raise ValueError("All 'z_max' values must be numeric.") 265 | 266 | # Validate direction 267 | direction = data['direction'] 268 | if not (isinstance(direction, pd.Series) and len(direction) == len(dates)): 269 | raise ValueError(f"'direction' must be a series of length {len(dates)}.") 270 | if not all(isinstance(x, str) for x in direction): 271 | raise ValueError("All 'direction' values must be strings.") 272 | 273 | # Validate reference DEM 274 | ref_dem = data['ref_dem'].dropna().unique() 275 | if not isinstance(ref_dem, (str)): 276 | raise TypeError(f"'ref_dem' must be an string, but got {ref_dem} ({type(ref_dem).__name__}).") 277 | 278 | # Validate reference DEM year 279 | dem_year = data['ref_dem_year'].dropna().unique() 280 | if len(dem_year) != 1: 281 | raise ValueError(f"'ref_dem_year' must have exactly one unique value, but found {len(dem_year)}: {dem_year}") 282 | if not isinstance(dem_year, (int)): 283 | raise TypeError(f"'ref_dem_year' must be an integer, but got {dem_year} ({type(dem_year).__name__}).") 284 | 285 | return True 286 | 287 | 288 | def snowline_csv_to_dict(data): 289 | """Convert snowline_1d CSV to JSON for OGGM ingestion.""" 290 | dates = data['date'].astype(str).tolist() 291 | z = data['z'].astype(float).tolist() 292 | z_min = data['z_min'].astype(float).tolist() 293 | z_max = data['z_max'].astype(float).tolist() 294 | direction = data['direction'].astype(str).tolist() 295 | ref_dem = data['ref_dem'].astype(str).tolist()[0] 296 | ref_dem_year = data['ref_dem_year'].astype(int).tolist()[0] 297 | 298 | data_dict = { 299 | 'date': dates, 300 | 'z': z, 301 | 'z_min': z_min, 302 | 'z_max': z_max, 303 | 'direction': direction, 304 | 'ref_dem': ref_dem, 305 | 'ref_dem_year': ref_dem_year, 306 | } 307 | return data_dict 308 | -------------------------------------------------------------------------------- /pygem/shop/mbdata.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Glacier Evolution Model (PyGEM) 3 | 4 | copyright © 2018 David Rounce 5 | 6 | Distributed under the MIT license 7 | """ 8 | 9 | # Built-in libaries 10 | import logging 11 | import os 12 | 13 | # External libraries 14 | from datetime import timedelta 15 | 16 | import numpy as np 17 | import pandas as pd 18 | from oggm import cfg 19 | from oggm.utils import entity_task 20 | 21 | # pygem imports 22 | from pygem.setup.config import ConfigManager 23 | from pygem.utils._funcs import parse_period 24 | 25 | # instantiate ConfigManager 26 | config_manager = ConfigManager() 27 | # read the config 28 | pygem_prms = config_manager.read_config() 29 | 30 | 31 | # Module logger 32 | log = logging.getLogger(__name__) 33 | 34 | # Add the new name "mb_calib_pygem" to the list of things that the GlacierDirectory understands 35 | if 'mb_calib_pygem' not in cfg.BASENAMES: 36 | cfg.BASENAMES['mb_calib_pygem'] = ( 37 | 'mb_calib_pygem.json', 38 | 'Mass balance observations for model calibration', 39 | ) 40 | 41 | 42 | @entity_task(log, writes=['mb_calib_pygem']) 43 | def mb_df_to_gdir( 44 | gdir, 45 | facorrected=pygem_prms['setup']['include_frontalablation'], 46 | ): 47 | """Select specific mass balance and add observations to the given glacier directory 48 | 49 | Parameters 50 | ---------- 51 | gdir : :py:class:`oggm.GlacierDirectory` 52 | where to write the data 53 | """ 54 | # get dataset filepath 55 | mbdata_fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["massbalance"]["massbalance_relpath"]}' 56 | mbdata_fp_fa = mbdata_fp + pygem_prms['calib']['data']['massbalance']['massbalance_facorrected_fn'] 57 | if facorrected and os.path.exists(mbdata_fp_fa): 58 | mbdata_fp = mbdata_fp_fa 59 | else: 60 | mbdata_fp = mbdata_fp + pygem_prms['calib']['data']['massbalance']['massbalance_fn'] 61 | 62 | assert os.path.exists(mbdata_fp), 'Error, mass balance dataset does not exist: {mbdata_fp}' 63 | 64 | # get column names and formats 65 | rgiid_cn = pygem_prms['calib']['data']['massbalance']['massbalance_rgiid_colname'] 66 | mb_cn = pygem_prms['calib']['data']['massbalance']['massbalance_mb_colname'] 67 | mberr_cn = pygem_prms['calib']['data']['massbalance']['massbalance_mb_error_colname'] 68 | mb_clim_cn = pygem_prms['calib']['data']['massbalance']['massbalance_mb_clim_colname'] 69 | mberr_clim_cn = pygem_prms['calib']['data']['massbalance']['massbalance_mb_clim_error_colname'] 70 | massbalance_period_colname = pygem_prms['calib']['data']['massbalance']['massbalance_period_colname'] 71 | massbalance_period_date_format = pygem_prms['calib']['data']['massbalance']['massbalance_period_date_format'] 72 | massbalance_period_delimiter = pygem_prms['calib']['data']['massbalance']['massbalance_period_delimiter'] 73 | 74 | # read reference mass balance dataset and pull data of interest 75 | mb_df = pd.read_csv(mbdata_fp) 76 | mb_df_rgiids = list(mb_df[rgiid_cn]) 77 | 78 | if gdir.rgi_id in mb_df_rgiids: 79 | # RGIId index 80 | rgiid_idx = np.where(gdir.rgi_id == mb_df[rgiid_cn])[0][0] 81 | 82 | # Glacier-wide mass balance 83 | mb_mwea = mb_df.loc[rgiid_idx, mb_cn] 84 | mb_mwea_err = mb_df.loc[rgiid_idx, mberr_cn] 85 | 86 | if ( 87 | mb_clim_cn is not None 88 | and mberr_clim_cn is not None 89 | and all(col in mb_df.columns for col in [mb_clim_cn, mberr_clim_cn]) 90 | ): 91 | mb_clim_mwea = mb_df.loc[rgiid_idx, mb_clim_cn] 92 | mb_clim_mwea_err = mb_df.loc[rgiid_idx, mberr_clim_cn] 93 | else: 94 | mb_clim_mwea = None 95 | mb_clim_mwea_err = None 96 | 97 | # notmalize user-input formats like YYYY-MM-DD -> %Y-%m-%d 98 | massbalance_period_date_format = ( 99 | massbalance_period_date_format.replace('YYYY', '%Y') 100 | .replace('YY', '%y') 101 | .replace('MM', '%m') 102 | .replace('DD', '%d') 103 | ) 104 | 105 | t1_datetime, t2_datetime = parse_period( 106 | mb_df.loc[rgiid_idx, massbalance_period_colname], 107 | date_format=massbalance_period_date_format, 108 | delimiter=massbalance_period_delimiter, 109 | ) 110 | 111 | # remove one day from t2 datetime for proper indexing (ex. 2001-01-01 want to run through 2000-12-31) 112 | t2_datetime = t2_datetime - timedelta(days=1) 113 | # Number of years 114 | nyears = (t2_datetime + timedelta(days=1) - t1_datetime).days / 365.25 115 | 116 | # Record data 117 | mbdata = { 118 | key: value 119 | for key, value in { 120 | 'mb_mwea': float(mb_mwea), 121 | 'mb_mwea_err': float(mb_mwea_err), 122 | 'mb_clim_mwea': float(mb_clim_mwea) if mb_clim_mwea is not None else None, 123 | 'mb_clim_mwea_err': float(mb_clim_mwea_err) if mb_clim_mwea_err is not None else None, 124 | 't1_str': t1_datetime.strftime('%Y-%m-%d'), 125 | 't2_str': t2_datetime.strftime('%Y-%m-%d'), 126 | 'nyears': nyears, 127 | }.items() 128 | if value is not None 129 | } 130 | 131 | gdir.write_json(mbdata, 'mb_calib_pygem') 132 | 133 | 134 | # @entity_task(log, writes=['mb_obs']) 135 | # def mb_bins_to_glacierwide(gdir, mb_binned_fp=pygem_prms.mb_binned_fp): 136 | # """Convert binned mass balance data to glacier-wide and add observations to the given glacier directory 137 | # 138 | # Parameters 139 | # ---------- 140 | # gdir : :py:class:`oggm.GlacierDirectory` 141 | # where to write the data 142 | # """ 143 | # 144 | # assert os.path.exists(mb_binned_fp), "Error: mb_binned_fp does not exist." 145 | # 146 | # glac_str_nolead = str(int(gdir.rgi_region)) + '.' + gdir.rgi_id.split('-')[1].split('.')[1] 147 | # 148 | # # If binned mb data exists, then write to glacier directory 149 | # if os.path.exists(mb_binned_fp + gdir.rgi_region + '/' + glac_str_nolead + '_mb_bins.csv'): 150 | # mb_binned_fn = mb_binned_fp + gdir.rgi_region + '/' + glac_str_nolead + '_mb_bins.csv' 151 | # else: 152 | # mb_binned_fn = None 153 | # 154 | # if mb_binned_fn is not None: 155 | # mbdata_fn = gdir.get_filepath('mb_obs') 156 | # 157 | # # Glacier-wide mass balance 158 | # mb_binned_df = pd.read_csv(mb_binned_fn) 159 | # area_km2_valid = mb_binned_df['z1_bin_area_valid_km2'].sum() 160 | # mb_mwea = (mb_binned_df['z1_bin_area_valid_km2'] * mb_binned_df['mb_bin_mean_mwea']).sum() / area_km2_valid 161 | # mb_mwea_err = 0.3 162 | # t1 = 2000 163 | # t2 = 2018 164 | # 165 | # # Record data 166 | # mbdata = {'mb_mwea': mb_mwea, 167 | # 'mb_mwea_err': mb_mwea_err, 168 | # 't1': t1, 169 | # 't2': t2, 170 | # 'area_km2_valid': area_km2_valid} 171 | # with open(mbdata_fn, 'wb') as f: 172 | # pickle.dump(mbdata, f) 173 | # 174 | # 175 | ##%% 176 | # def mb_bins_to_reg_glacierwide(mb_binned_fp=pygem_prms.mb_binned_fp, O1Regions=['01']): 177 | # # Delete these import 178 | # mb_binned_fp=pygem_prms.mb_binned_fp 179 | # O1Regions=['19'] 180 | # 181 | # print('\n\n SPECIFYING UNCERTAINTY AS 0.3 mwea for model development - needs to be updated from mb providers!\n\n') 182 | # reg_mb_mwea_err = 0.3 183 | # 184 | # mb_yrfrac_dict = {'01': [2000.419, 2018.419], 185 | # '02': [2000.128, 2012], 186 | # '03': [2000.419, 2018.419], 187 | # '04': [2000.419, 2018.419], 188 | # '05': [2000.419, 2018.419], 189 | # '06': [2000.419, 2018.419], 190 | # '07': [2000.419, 2018.419], 191 | # '08': [2000.419, 2018.419], 192 | # '09': [2000.419, 2018.419], 193 | # '10': [2000.128, 2012], 194 | # '11': [2000.128, 2013], 195 | # '12': [2000.128, 2012], 196 | # 'HMA': [2000.419, 2018.419], 197 | # '16': [2000.128, 2013.128], 198 | # '17': [2000.128, 2013.128], 199 | # '18': [2000.128, 2013]} 200 | # 201 | # for reg in O1Regions: 202 | # reg_fp = mb_binned_fp + reg + '/' 203 | # 204 | # main_glac_rgi = modelsetup.selectglaciersrgitable(rgi_regionsO1=[reg], rgi_regionsO2='all', rgi_glac_number='all') 205 | # 206 | # reg_binned_fns = [] 207 | # for i in os.listdir(reg_fp): 208 | # if i.endswith('_mb_bins.csv'): 209 | # reg_binned_fns.append(i) 210 | # reg_binned_fns = sorted(reg_binned_fns) 211 | # 212 | # print('Region ' + reg + ' has binned data for ' + str(len(reg_binned_fns)) + ' glaciers.') 213 | # 214 | # reg_mb_df_cns = ['RGIId', 'O1Region', 'O2Region', 'area_km2', 'mb_mwea', 'mb_mwea_err', 't1', 't2', 'perc_valid'] 215 | # reg_mb_df = pd.DataFrame(np.zeros((main_glac_rgi.shape[0], len(reg_mb_df_cns))), columns=reg_mb_df_cns) 216 | # reg_mb_df.loc[:,:] = np.nan 217 | # reg_mb_df.loc[:, 'RGIId'] = main_glac_rgi['RGIId'] 218 | # reg_mb_df.loc[:, 'O1Region'] = main_glac_rgi['O1Region'] 219 | # reg_mb_df.loc[:, 'O2Region'] = main_glac_rgi['O2Region'] 220 | # reg_mb_df.loc[:, 'area_km2'] = main_glac_rgi['Area'] 221 | # 222 | # # Process binned files 223 | # for nfn, reg_binned_fn in enumerate(reg_binned_fns): 224 | # 225 | # if nfn%500 == 0: 226 | # print(' ', nfn, reg_binned_fn) 227 | # 228 | # mb_binned_df = pd.read_csv(reg_fp + reg_binned_fn) 229 | # glac_str = reg_binned_fn.split('_')[0] 230 | # glac_rgiid = 'RGI60-' + glac_str.split('.')[0].zfill(2) + '.' + glac_str.split('.')[1] 231 | # rgi_idx = np.where(main_glac_rgi['RGIId'] == glac_rgiid)[0][0] 232 | # area_km2_valid = mb_binned_df['z1_bin_area_valid_km2'].sum() 233 | # mb_mwea = (mb_binned_df['z1_bin_area_valid_km2'] * mb_binned_df['mb_bin_mean_mwea']).sum() / area_km2_valid 234 | # mb_mwea_err = reg_mb_mwea_err 235 | # t1 = mb_yrfrac_dict[reg][0] 236 | # t2 = mb_yrfrac_dict[reg][1] 237 | # perc_valid = area_km2_valid / reg_mb_df.loc[rgi_idx,'area_km2'] * 100 238 | # 239 | # reg_mb_df.loc[rgi_idx,'mb_mwea'] = mb_mwea 240 | # reg_mb_df.loc[rgi_idx,'mb_mwea_err'] = mb_mwea_err 241 | # reg_mb_df.loc[rgi_idx,'t1'] = t1 242 | # reg_mb_df.loc[rgi_idx,'t2'] = t2 243 | # reg_mb_df.loc[rgi_idx,'perc_valid'] = perc_valid 244 | # 245 | # #%% 246 | # # Quality control 247 | # O2Regions = list(set(list(main_glac_rgi['O2Region'].values))) 248 | # O2Regions_mb_mwea_dict = {} 249 | # rgiid_outliers = [] 250 | # for O2Region in O2Regions: 251 | # reg_mb_df_subset = reg_mb_df[reg_mb_df['O2Region'] == O2Region] 252 | # reg_mb_df_subset = reg_mb_df_subset.dropna(subset=['mb_mwea']) 253 | # 254 | # # Use 1.5*IQR to remove outliers 255 | # reg_mb_mwea_25 = np.percentile(reg_mb_df_subset['mb_mwea'], 25) 256 | # reg_mb_mwea_50 = np.percentile(reg_mb_df_subset['mb_mwea'], 50) 257 | # reg_mb_mwea_75 = np.percentile(reg_mb_df_subset['mb_mwea'], 75) 258 | # reg_mb_mwea_iqr = reg_mb_mwea_75 - reg_mb_mwea_25 259 | # 260 | # print(np.round(reg_mb_mwea_25,2), np.round(reg_mb_mwea_50,2), np.round(reg_mb_mwea_75,2), 261 | # np.round(reg_mb_mwea_iqr,2)) 262 | # 263 | # reg_mb_mwea_bndlow = reg_mb_mwea_25 - 1.5 * reg_mb_mwea_iqr 264 | # reg_mb_mwea_bndhigh = reg_mb_mwea_75 + 1.5 * reg_mb_mwea_iqr 265 | # 266 | # # Record RGIIds that are outliers 267 | # rgiid_outliers.extend(reg_mb_df_subset[(reg_mb_df_subset['mb_mwea'] < reg_mb_mwea_bndlow) | 268 | # (reg_mb_df_subset['mb_mwea'] > reg_mb_mwea_bndhigh)]['RGIId'].values) 269 | # # Select non-outliers and record mean 270 | # reg_mb_df_subset_qc = reg_mb_df_subset[(reg_mb_df_subset['mb_mwea'] >= reg_mb_mwea_bndlow) & 271 | # (reg_mb_df_subset['mb_mwea'] <= reg_mb_mwea_bndhigh)] 272 | # 273 | # reg_mb_mwea_qc_mean = reg_mb_df_subset_qc['mb_mwea'].mean() 274 | # O2Regions_mb_mwea_dict[O2Region] = reg_mb_mwea_qc_mean 275 | # 276 | # #%% 277 | # print('CREATE DICTIONARY FOR RGIIDs with nan values or those that are outliers') 278 | # # print(A['mb_mwea'].mean(), A['mb_mwea'].std(), A['mb_mwea'].min(), A['mb_mwea'].max()) 279 | # # print(reg_mb_mwea, reg_mb_mwea_std) 280 | # 281 | # 282 | # #%% 283 | # reg_mb_fn = reg + '_mb_glacwide_all.csv' 284 | # reg_mb_df.to_csv(mb_binned_fp + reg_mb_fn, index=False) 285 | # 286 | # print('TO-DO LIST:') 287 | # print(' - quality control based on 3-sigma filter like Shean') 288 | # print(' - extrapolate for missing or outlier glaciers by region') 289 | --------------------------------------------------------------------------------